语法糖
语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
C++也有很多语法糖,比如运算符重载、lambda表达式、auto类型推导等。这些语法糖可以让我们的代码更简洁、更易读、更高效。例如,下面两种写法是等价的:
1 2 3 4 5 6 7 8
| int sum = 0; for (int i = 0; i < 10; i++) { sum += i; } int sum = 0; for (auto i : {0,1,2,3,4,5,6,7,8,9}) { sum += i; }
|
c++11、20新特性大多数都是语法糖
c++20
C++20有很多新的特性,其中最重要的四个是概念、范围、协程和模块。概念可以让我们定义泛型函数或类的约束条件,范围可以让我们更方便地操作容器和迭代器,协程可以让我们编写异步代码,模块可以让我们更高效地组织代码。除此之外,C++20还有一些其他的新特性,比如三向比较运算符、指定初始化、日历和时区功能等。
概念
概念是一种用来约束模板类型的语法糖。我们可以用concept关键字来定义一个概念,然后用requires关键字来指定一个模板参数必须满足某个概念。例如,我们可以定义一个Integral概念,表示一个类型必须是整数类型
1 2 3 4 5 6 7
| template<typename T> concept Integral = std::is_integral_v<T>;
template<Integral T> T add(T a, T b) { return a + b; }
|
这样,如果我们传入非整数类型的参数,就会在编译时报错。
概念可以自定义,使用requires关键字
1 2 3 4 5
| template<typename T> concept Sortable = requires(T a) { { std::sort(a.begin(), a.end()) } -> std::same_as<void>; };
|
标准库中提供了上百种常用的概念,放在和等头文件中。比较常用的一些有:std::same_as, std::derived_from, std::convertible_to, std::floating_point等
1 2 3 4 5
| #include <concepts> template<std::integral T> T add(T a, T b) { return a + b; }
|
范围
范围是C++20加入的一个重要的库功能,它提供了描述范围和对范围的操作的统一接口。一个范围是可以循环访问的任何东西,比如一个容器或者一个数组。我们可以用begin()和end()函数来获取一个范围的起始和终止位置。我们也可以用基于范围的for语句来遍历一个范围中的所有元素。例如,我们可以这样打印一个vector中的所有元素:
1 2 3 4 5 6 7 8
| #include <iostream> #include <vector> int main() { std::vector<int> v = {1, 2, 3, 4, 5}; for (auto x : v) { std::cout << x << " "; } }
|
自定义的类型,满足range概念,都可以使用范围的特性。即它可以用begin()和end()函数来获取其起始和终止位置。这两个函数返回的对象必须是迭代器或者哨兵。迭代器是可以用++和*操作符来遍历元素的对象,哨兵是可以用==操作符来判断是否到达范围的末尾的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <ranges> #include <iostream>
class IntRange { public: IntRange(int a, int b) : a_(a), b_(b) {} class Iterator { public: Iterator(int x) : x_(x) {} int operator*() const { return x_; } Iterator& operator++() { ++x_; return *this; } bool operator==(const Iterator& other) const { return x_ == other.x_; } bool operator!=(const Iterator& other) const { return !(*this == other); } private: int x_; }; class Sentinel { public: Sentinel(int y) : y_(y) {} bool operator==(const Iterator& iter) const { return *iter == y_; } bool operator!=(const Iterator& iter) const { return !(*this == iter); } private: int y_; }; Iterator begin() const { return Iterator(a_); } Sentinel end() const { return Sentinel(b_); }
private: int a_, b_; };
int main() { IntRange r(1,5); for (auto x : r) { std::cout << x << " "; } }
|
协程
协程是一种可以在执行过程中被挂起和恢复的函数。它可以用来实现异步编程,提高性能和并发度。
C++20中引入了三个新的关键字,co_await,co_yield和co_return,用来标记一个函数是协程。这些关键字只是语法糖,编译器会将协程的上下文打包成一个对象,并让未执行完的协程先返回给调用者。要实现一个C++20协程,还需要提供两个鸭子类型,promise type和awaiter type,分别用来管理协程的生命周期和等待机制。
例如,我们可以实现一个简单的生成器协程,它每次产生一个整数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <coroutine> #include <iostream>
struct Generator { struct promise_type { int current_value; std::suspend_always yield_value(int value) { this->current_value = value; return {}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; } void unhandled_exception() {} };
bool move_next() { p.resume(); return !p.done(); } int current_value() { return p.promise().current_value; }
private: std::coroutine_handle<promise_type> p; };
Generator generator(int start = 0) { int i = start; while (true) { co_yield i++; } }
int main() { auto g = generator(1); for (int i = 0; i < 10; ++i) { g.move_next(); std::cout << g.current_value() << " "; } }
|
使用协程实现异步网络编程的主要优点是可以用同步的语法写出异步的代码,提高代码的可读性和可维护性1。要使用协程实现异步网络编程,需要以下几个步骤:
使用标准库中提供的std::jthread或std::thread创建一个或多个工作线程,用来执行协程任务。
使用标准库中提供的std::coroutine_handle或自定义的协程句柄类型,管理协程的生命周期和调度。
使用标准库中提供的std::future或自定义的awaiter类型,等待异步操作完成并获取结果。
使用标准库中提供的std::sync_wait或自定义的同步等待函数,等待所有协程任务完成后退出程序。
例如,我们可以使用一个简单的网络框架ZED3,它提供了一些基本的异步IO操作,并封装了协程句柄和awaiter类型。我们可以用以下代码实现一个简单的回显服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <zed/net.hpp> #include <iostream>
using namespace zed;
int main() { io_context ctx; std::jthread th([&ctx]() { ctx.run(); }); tcp_server server(ctx); server.bind(8080); server.listen(); while (true) { try { auto socket = co_await server.accept(); std::cout << "New connection from " << socket.remote_endpoint() << "\n"; while (true) { auto n = co_await socket.recv(); if (n == 0) break; std::cout << "Received " << n << " bytes\n"; auto m = co_await socket.send(n); std::cout << "Sent " << m << " bytes\n"; } std::cout << "Connection closed\n"; } catch (const std::exception& e) { std::cerr << e.what() << "\n"; } } }
|
模块
C++20模块是一种新的代码组织和重用的方式,它可以替代传统的头文件和翻译单元。#include 多个头文件时编译很慢,使用 module 相当于直接调用编译好的二进制文件,这个二进制文件中描述了这个 module 导出的函数、类、模板等。模块可以提高编译速度,避免宏污染,隐藏实现细节,简化依赖关系等优点。要使用模块,需要以下几个步骤:
在源文件中使用module关键字声明一个模块,并指定模块名。
在源文件中使用export关键字导出需要对外提供的符号。
在其他源文件中使用import关键字导入需要使用的模块。
使用支持模块的编译器编译源文件,并生成相应的模块接口文件和目标文件。
例如,我们可以用以下代码定义一个名为hello的模块:
1 2 3 4 5 6
| module hello; export void say_hello(); void say_hello() { std::cout << "Hello, world!\n"; }
|
然后我们可以在另一个源文件中导入并使用这个模块:
1 2 3 4 5
| import hello; int main() { say_hello(); }
|
子模块是一种在逻辑上划分模块的方法,它可以让用户选择性地导入模块的一部分或全部内容。子模块的命名规则中允许点存在于模块名字当中,但点并不代表语法上的从属关系,而只是帮助程序员理解模块间的逻辑关系。
例如,我们可以用以下代码定义一个名为hello.sub_a的子模块:
1 2 3 4 5 6
| export module hello.sub_a; export void say_hello_sub_a(); void say_hello_sub_a() { std::cout << "Hello, sub a!\n"; }
|
然后我们可以在另一个源文件中定义一个名为hello.sub_b的子模块:
1 2 3 4 5 6
| export module hello.sub_b; export void say_hello_sub_b(); void say_hello_sub_b() { std::cout << "Hello, sub b!\n"; }
|
最后我们可以在另一个源文件中定义一个名为hello的父模块,它导出了两个子模块:
1 2 3 4
| export module hello; export import hello.sub_a; export import hello.sub_b;
|
这样,用户就可以根据需要导入不同的子模块或父模块:
1 2 3 4 5 6
| import hello; int main() { say_hello_sub_a(); say_hello_sub_b(); }
|
命名空间冲突是指不同的模块或源文件中定义了相同的名称,导致编译器无法区分它们的含义。C++20 模块提供了一些方法来避免或解决命名空间冲突:
使用不同的模块名字来区分不同的模块,例如 hello.sub_a 和 hello.sub_b 就是两个不同的模块,即使它们都定义了 say_hello 函数,也不会发生冲突。
使用限定名字来指定模块中的名称,例如 hello.sub_a::say_hello 和 hello.sub_b::say_hello 就可以明确地区分两个模块中的函数。
使用 using 声明或 using 指令来引入需要的名称,但要注意避免引入重复或冲突的名称。例如:
1 2 3 4 5 6 7
| import hello; using hello.sub_a::say_hello; int main() { say_hello(); hello.sub_b::say_hello(); }
|
- 使用 export 关键字来控制哪些名称被导出到其他模块或源文件,以减少暴露给外部的名称。例如:
1 2 3 4 5 6 7 8 9 10 11 12
| export module math; namespace detail { int add(int x, int y) { return x + y; } } export int sum(int x, int y) { return detail::add(x, y); }
import math; int main() { int s = math::sum(1, 2); int a = math::detail::add(1, 2);
|