【C++20】特性解析 (上)
一:🔥 C++20 介绍及编译器版本⽀持

- https://en.cppreference.com/w/cpp/compiler_support/20.html
- 官⽅⽂档中给出每个新特性对编译器版本的要求
// vs编译运⾏以下程序打印版本
#include <iostream>
int main()
{
std::cout << "_MSC_VER = " << _MSC_VER << "\n"; // 主版本号
std::cout << "_MSC_FULL_VER = " << _MSC_FULL_VER << "\n"; // 完整版本号
return 0;
}
// linux下gcc -v可以查看版本
二:🔥 概念与约束
- https://en.cppreference.com/w/cpp/language/constraints.html
- 概念(concept) 是 C++20 引⼊的模板参数约束机制,它允许程序员明确指定模板参数必须满⾜的条件。概念本质上是⼀个编译时谓词,⽤于验证模板参数是否满⾜特定要求。概念会在模板实例化前检查类型是否满⾜条件,⽽不是在实例化后产⽣难以理解的错误。
- 官⽅库提供的⼀些概念:https://en.cppreference.com/w/cpp/header/concepts.html
🦋 基本概念及定义
- 在C++20中,使⽤ concept 关键字来定义概念。基本语法如下:
// template-parameter-list 是模板参数列表
// concept-name 是概念的名称
// constraint-expression 是⼀个可以被求值为 bool 的常量表达式(通常是各种约束的组合)
template <template-parameter-list>
concept concept-name = constraint-expression;
- 常⻅的⼏种概念定义
// ⽅式1:使⽤标准库类型萃取
template<typename T>
concept Integral = std::is_integral_v<T>;
// ⽅式2:简单的常量表达式
template<typename T>
concept SmallType = sizeof(T) <= 4;
// ⽅式3:简单的 requires ⼦句
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀++是否有效
t++; // 检查后缀++是否有效
};
本质就是转换成真假,cout << Integral<int> 看一下就知道了
- requires 表达式的四种形式
template<typename T>
concept ConceptName = requires (parameter-list) {
requirement-seq
};
#include <concepts>
#include <iostream>
#include <vector>
#include <string>
// 1、简单要求
// 最简单的形式,只检查表达式是否合法(能否编译通过)
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 检查类型T是否⽀持+运算 a.size() 也可以这样子检测 (符不符合)是不是真
};
// 2、类型要求
// 检查某个嵌套类型是否存在
template<typename T>
concept HasValueType = requires {
typename T::iterator; // 检查T是否有iterator嵌套类型
typename T::value_type; // 检查T是否有value_type嵌套类型
};
// 3、复合要求
// 不仅检查表达式是否合法,还可以指定返回类型和异常规范:
// 复合要求的完整语法为:
// { 表达式 }[noexcept]->类型约束;
template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // 要求a+b的结果可转换为T
{ a += b } noexcept -> std::same_as<T&>; // 要求a+=b不抛异常且返回T&
};
// 4、嵌套要求
// 在 requires 内部再使⽤ requires 进⾏更复杂的约束
// requires (参数列表) {
// //... 其他要求 (如类型要求、复合要求等)
// ...requires <常量布尔表达式>;
// }
template<typename T>
concept ComplexConcept = requires(T t) {
requires sizeof(T) <= sizeof(long);
requires std::is_class_v<T>;
};
template<Addable T>
void f1(const T& x1, const T& x2)
{
T ret = x1 + x2;
std::cout << ret << std::endl;
}
template<HasValueType T>
void f2(const T& x)
{}
template<ConvertibleAddable T>
void f3(T& x1, const T& x2)
{
T ret = x1 + x2;
std::cout << ret << std::endl;
x1 += x2;
std::cout << x1 << std::endl;
}
template<ComplexConcept T>
void f4(const T& x)
{}
int main()
{
f1(1, 2);
f1("xxx", "yyy");
f2(std::vector<int>{1, 2, 3});
f2(1);
std::string s1("111");
f3(s1, std::string("222"));
int i = 1;
f3(i, 2);
f4(1);
f4(std::vector<int>{1, 2, 3});
f4(std::less<int>());
return 0;
}
requires 后必须是 <常量布尔表达式>;
扩展写法:
template<typename T>
concept ComplexConcept = requires {
requires sizeof(T) <= sizeof(long);
requires requires(T x) {
x.size();
x + x;
};
};
因为
requires(T x) {
x.size();
x + x;
};
这个意思也是转换成了一个常量表达式,看到这样的用法不要奇怪就行
🦋 约束的使⽤⽅式
#include <iostream>
#include <string>
#include <vector>
#include <concepts>
// 定义⼀个要求T是整形的概念
template< class T >
concept Integral = std::is_integral_v<T>;
template< class T >
concept Float = std::is_floating_point_v<T>;
// 注意: 这里可以进行多个概念的组合
template<class T>
concept Integral_or_Float = Integral<T> || Float<T>;
// 1、模板参数后直接使⽤
template<Integral T>
void f1(T x)
{
std::cout << "有 concepts 约束" << std::endl;
}
// 2、requires⼦句
template<typename T>
requires Integral<T> || Float<T>
void f2(T x) {}
// 3、尾置requires ⼦句
template<typename T>
void f3(T x) requires Integral<T> || Float<T> {}
// 4. 临时约束 - 使⽤ requires requires
template<typename T>
requires requires(T x) { x.size(); x+x; }
void f4(T x) {}
// 5、auto占位符约束
void f5(Integral auto x) {}
int main()
{
// 符合concept要求则实例化调⽤对应的函数模板
f1(1);
// 不符合concept要求,报错
f1("xxx");
return 0;
}
🦋 约束的⼏种类型
- 原⼦约束,是不可再分的约束表达式,如⼀个概念名 std::integral ,⼀个 requires 表达式,⼀个常量布尔表达式等
- 合取,逻辑与,所有约束都必须为 true ,整个合取约束才为 true ,使⽤ && 运算符连接多个原⼦约束。
- 析取,逻辑或,多个约束中⾄少有⼀个为 true ,整个析取约束就为 true ,使⽤ || 运算符连接多个约束。
- 折叠扩展约束,可变参数包的约束扩展,对参数包的多个参数进⾏包扩展的约束,C++26才开始⽀持。
// 1. 逻辑与(&&)
template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
std::cout << t;
};
// 2. 逻辑或(||)
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// 3. 逻辑⾮(!)
template<typename T>
concept NonPointer = !std::is_pointer_v<T>;
// 4. 折叠扩展约束,折叠扩展约束就是可变参数包的扩展,C++26才开始⽀持
template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;
template <class... T>
requires (C<T> && ...) void g(T...) {
cout << "g()->C" << endl;
}
int main()
{
//g(unique_ptr<int>());
g(1.1);
g(1, 2, string());
return 0;
}
🦋 约束的偏序规则
- C++20概念的约束偏序规则⽤于在函数重载或模板特化时,判断哪个约束的模板"更特化"或"更受约束"。编译器使⽤这个规则来选择最合适的模板。
- 偏序规则的核⼼是:如果约束A包含(蕴含)约束B,那么A⽐B"更受约束"。这意味着任何满⾜A的类型也⼀定会满⾜B,但满⾜B的类型不⼀定满⾜A。即 A ⇒ B (A蕴含B)。当两个模板匹配时,编译器会选择"更受约束"的那⼀个。
#include <iostream>
#include <concepts>
// 定义⼀个要求T是整形的概念
template< class T >
concept Integral = std::is_integral_v<T>;
// 1、模板参数后直接使⽤
template<Integral T>
void f(T x)aaaa
{
std::cout << "有 concepts 约束" << std::endl;
}
template<class T>
void f(T x)
{
std::cout << "⽆ concepts 约束" << std::endl;
}
template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;
template <class T>
requires A<T> void g(T x)
{
std::cout << "g()->A" << std::endl;
}
template <class T>
requires C<T> void g(T x)
{
std::cout << "g()->C" << std::endl;
}
int main()
{
// 符合concept要求则实例化调⽤对应的函数模板
f(1);
// 不符合concept要求,查找⽆concept要求的模板实例化调⽤,没找到则报错
f("xxx");
// 这⾥同时满⾜A和B,会优先匹配C
g(1.1);
// 这⾥不满⾜C,只满⾜A,会匹配A
g(std::unique_ptr<int>());
return 0;
}
🦋 综合实践样例
- 实践中解决哈希表对 key 参数要求的问题
#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
// 概念 HashType 的声明可以被符合以下条件的任意类型 T 满⾜:
// 对于 T 类型的值 a,表达式 std::hash<T>{}(a) 可以编译并且它的结果可以转换到
std::size_t
template<typename T>
concept HashType = requires(T a)
{
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
// 受约束的 C++20 类模板
template<HashType K, class HashFunc = std::hash<K>>
class hashmap
{
public:
bool insert(const K& key)
{
size_t hashi = HashFunc()(key);
}
};
int main()
{
hashmap<int> hm1;
hashmap<std::string> hm2;
//hashmap<std::pair<std::string, int>> hm3;
return 0;
}
- 多种复杂条件组合和嵌套的
#include <concepts>
#include <iostream>
#include <vector>
#include <mutex>
template<typename T>
concept PrintableRange = requires(T t) {
{ t.begin() }->std::same_as<typename T::iterator>;
{ t.end() }->std::same_as<typename T::iterator>;
requires std::integral<typename T::value_type>;
// 对T内部的内容的要求,所以要在嵌套定义⼀个requires的参数
requires requires(typename T::value_type e) { std::cout << e; };
};
template<PrintableRange R>
void print_range(const R& r) {
for (const auto& e : r) {
std::cout << e << ' ';
}
std::cout << '\n';
}
int main() {
std::vector<int> v{ 1, 2, 3 };
print_range(v); // OK
std::vector<std::mutex> vm;
print_range(vm); // 错误: 不满⾜PrintableRange
return 0;
}
- 使⽤概念解决⼀些模板匹配问题
#include <concepts>
#include <iostream>
template<typename T>
concept IteratorType = requires(T it) {
// 要求只⽀持前置++和解引⽤
++it;
*it;
};
namespace bit
{
template<class T>
class vector
{
public:
typedef T* iterator;
// 强制编译器⽣成默认构造
vector() = default;
// 函数模板
// 任意类型容器迭代器初始化
//template <IteratorType InputIterator>
template <typename InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(size_t n, const T& val = T())
{}
void push_back(const T& x)
{}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
int main()
{
bit::vector<int> v7(10, 1);
bit::vector<size_t> v9(10, 1);
return 0;
}
🦋 SFINAE 和 Concepts
- SFINAE 是 SubstitutionFailureIsNotAnError 的缩写,翻译为替换失败不是错误,通过模板替换失败静默排除候选函数,实现编译期条件分⽀。本质是技巧性机制,依赖模板实例化规则如 std::enable_if、decltype 等
- Concepts 直接声明模板参数的语义约束(如“类型必须可⽐较”),提供显式接⼝描述。⽬标是提升代码可读性和编译错误信息
- SFINAE 代码冗⻓且晦涩,Concepts 是 SFINAE 的官⽅替代⽅案,解决了后者在可维护性和表达⼒上的缺陷。
// C++17:使⽤enable_if约束整型参数
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {}
// C++20:使⽤concept约束
template<std::integral T>
void foo(T x) {}
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
-
typename = std::enable_if_t<std::is_integral_v<T>>:- 这是一个未命名的模板参数,默认值为
std::enable_if_t<std::is_integral_v<T>> - 只有当条件满足时,这个默认类型才有效
- 这是一个未命名的模板参数,默认值为
-
std::is_integral_v<T> -
这是一个类型特征(type trait),用于检查类型T是否为整数类型
std::enable_if_t<std::is_integral_v<T>>
std::enable_if是一个模板,根据条件决定是否定义类型- 当条件为
true时,std::enable_if<true, T>::type等于T - 当条件为
false时,std::enable_if<false, T>::type未定义
整体工作原理
// 当T是整数类型时,例如T=int:
// std::is_integral_v<int> => true
// std::enable_if_t<true> => void (默认类型)
// 模板变为: template<typename int, typename = void>
// 合法,函数可实例化
// 当T不是整数类型时,例如T=double:
// std::is_integral_v<double> => false
// std::enable_if_t<false> => 未定义
// 模板替换失败,SFINAE原则导致此重载被丢弃
| 特性 | enable_if方法 | concept方法 |
|---|---|---|
| C++版本要求 | C++11+ | C++20+ |
| 可读性 | 较差,需要理解SFINAE | 很好,语义清晰 |
| 错误信息 | 复杂难懂 | 清晰明确 |
| 灵活性 | 高,可组合复杂条件 | 高,可通过定义新concept扩展 |
| 推荐程度 | 历史代码中常见 | 现代C++首选 |
两种方法功能相同,但 concept 提供了更好的可读性和错误诊断能力。
三:🔥 协程
🦋 什么是协程?
简单来说,协程是⼀个可以暂停执⾏并在之后恢复执⾏的函数。
- 普通函数:调⽤->执⾏到 return ->销毁所有栈帧,返回调⽤者。⼀⽣只返回⼀次。
- 协程:调⽤->执⾏到 co_await , co_yield , co_return ->暂停(挂起),保存当前状态
(局部变量、执⾏位置等)->之后某个时刻被恢复,从暂停点继续执⾏->最终执⾏到 co_return 结束。

🦋 协程和线程的关系
- C++20协程和线程是不同维度的概念,它们之间是协作与互补的关系,⽽不是替代关系,线程是劳动⼒(CPU核⼼),协程是⾼效的任务(Task)。
- 线程:像是⼯⼚⾥的⼯⼈。操作系统是⼚⻓,负责分配⼯⼈(线程)到不同的流⽔线(CPU核⼼)上⼯作。⼯⼈是执⾏任务的基本单位。
- 协程:像是⼯⼈⼿中的⼀套可暂停、可换模的任务,⼀个⼯⼈可以在⼀个任务做到⼀半时暂停(挂起协程),换上另⼀套模具做另⼀个任务(恢复另⼀个协程),之后再回来继续第⼀个任务。
- 协程不是线程的替代品,⽽是增强⼯具,最佳实践是将协程与线程池结合使⽤,⽤线程池提供并⾏计算能⼒,⽤协程来⾼效管理⼤量的并发I/O任务。
- 线程切换的代价很⾼,需要上下⽂切换,陷⼊内核态,保存所有寄存器、更新⻚表等。⽽C++20 协程的上下⽂切换代价极低,纯粹在⽤⼾态进⾏,只需保存/恢复少量寄存器。
🦋 C++20协程
- C++20的协程是⽆栈协程,这意味着暂停时只保存协程帧 (程序计数器、局部对象、Promise 对象等)(保存在堆上),⽽不是整个调⽤栈。这使得它们⾮常轻量,但意味着在协程内暂停时,其调⽤者也会被暂停。
- C++20协程它本⾝不是⼀个开箱即⽤的并发⼯具,⽽是⼀个⽤于构建异步编程的强⼤底层语⾔设施,项⽬要使⽤协程,通常要使⽤⼀些上层封装抽象的协程库或者结合使⽤的库,如 libunifex/cppcoro/Boost.Asio 等等。
- C++20协程学习曲线陡峭,且当前直接使⽤很⿇烦困难,但是学习它对于我们使⽤和理解协程的运⾏机制和⾼效使⽤协程库有很⼤的帮助,当前 C++23 提供了 std::generator 相关⽀持,C++26 也在进⼀步⽀持 task 等,C++26 当前还没正式发布,我们需要静等后续 C++ 标准库的完善和编译器的⽀持。
当⼀个函数包含 co_await/co_yield/co_return 任意⼀个关键字时,它就是协程。
- co_await expression ⽤于暂停执⾏直到恢复
- co_yield expression ⽤于暂停执⾏并返回⼀个值
- co_return expression ⽤于完成执⾏并返回⼀个值
编译器会将这个函数转换为⼀个状态机,关键在于,协程的⾏为由其返回类型(为Promise类型)控制。
// C++23⽀持的generator
#include <generator>
#include <iostream>
std::generator<long long> fibonacci(int count) {
long long a = 0, b = 1;
for (int i = 0; i < count; ++i) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
int main() {
for (auto num : fibonacci(10)) {
std::cout << num << " "; // Output: 0 1 1 2 3 5 8 13 21 34
}
}
🦋 协程的控制三个核⼼组件
🎀 Promise对象
由编译器在协程帧内创建的对象,它是协程的⼤脑,控制着协程的⾏为,Promise类型必须提供以下核⼼接⼝,⽤来控制协程的⾏为。
-
get_return_object()
a. 作⽤:创建并返回给调⽤者协程的返回值。这是在协程函数体执⾏之前被调⽤的。
b. 返回值类型:协程的返回类型(例如外层的 Task、Generator )。
典型实现:通常会⽤ coroutine_handle::from_promise(*this) 来创建⼀个指向当前 Promise对象的句柄,并⽤这个句柄来构造外层的返回值对象。
Generator get_return_object() {
return Generator{std::coroutine_handle<Promise>::from_promise(*this)};
}
-
initial_suspend()
a. 作⽤:决定协程在开始执⾏函数体之前是否应该先挂起。
b. 返回值:⼀个满⾜ Awaitable 概念的对象(通常是 std::suspend_always 或 std::suspend_never )。
c. std::suspend_always{} :总是挂起。这意味着协程⼀创建就是惰性的,不会⾃动开始执⾏,需要⼿动 resume() ,这是“懒汉式”协程的常⻅选择。
d. std::suspend_never{} :从不挂起。协程创建后会⽴即开始执⾏,直到遇到第⼀个挂起点。这是“饿汉式”协程的常⻅选择。
-
final_suspend()
a. 作⽤:决定协程在执⾏完函数体(或通过 co_return 返回)后,在彻底销毁⾃⾝之前是否应该挂起。
b. 返回值:同样是⼀个 Awaitable 概念对象。(通常是 std::suspend_always 或 std::suspend_never )。
c. 重要提⽰:如果返回 std::suspend_always ,你必须在协程外部某处⼿动调⽤ .destroy() 来销毁协程帧,否则会发⽣内存泄漏。如果返回 std::suspend_never ,协程会在返回后⾃动清理⾃⼰。
d. 典型⽤途,使⽤返回 std::suspend_always ,在 final_suspend() 中挂起,可以让你在协程外部有机会在其销毁前读取最终状态或结果。
-
return_void() 和 return_value(…)
a. 作⽤:处理 co_return 语句。
b. Promise类型必须实现这两个的其中之⼀,但不能同时实现。
c. void return_void() :⽤于处理 co_return; (⽆返回值)。
d. void return_value(type value) :⽤于处理 co_return value; (有返回值)。value 的类型由你指定,通常你会把它存储到 Promise 的⼀个成员变量中,以便外部获取。
-
yield_value(…)
a. 作⽤:处理 co_yield 语句。 co_yield expression 本质上被编译器翻译为
co_await promise.yield_value(expression) 。b. 返回值:⼀个 Awaitable 概念对象,⽤于决定在产出值之后是否要挂起协程(通常返回std::suspend_always{} 来挂起,让调⽤者有机会处理产出的值)。
c. 典型实现:接收产出的值,将其存储到Promise类型的⼀个成员变量中,然后返回⼀个挂起指⽰器。
std::suspend_always yield_value(int value) {
current_value = value; // 存储值到current_value成员变量,供外部访问
return {};
}
-
unhandled_exception()
a. 作⽤:当协程体内发⽣异常,并且该异常未被协程体内的 try/catch 块捕获时,这个异常会被⾃动传递给 unhandled_exception() ⽅法。
b. unhandled_exception() :Promise类型的核⼼异常处理函数,协程内任何未捕获的异常都会调⽤此函数, std::current_exception() :捕获当前异常并转换为 std::exception_ptr,std::rethrow_exception() :重新抛出存储的异常。
- 当协程中抛出未捕获的异常时:
- 异常被捕获:协程运行时会包装执行代码,捕获所有未处理的异常
- 调用 unhandled_exception:将捕获的异常传递给这个函数
- 协程终止:协程立即进入终止状态,不会回到挂起状态
- 控制权返回:控制权返回给调用 resume() 的地方
c. RAII模式在协程中依然有效,即使协程抛出异常,栈上的对象也会正确析构,协程帧本⾝的内存由 coroutine_handle 管理
#include <coroutine>
#include <iostream>
#include <stdexcept>
// 最简单的协程类型
struct SimpleTask {
struct promise_type {
int value = 0;
std::exception_ptr exception; // 存储异常的地⽅
SimpleTask get_return_object() {
return SimpleTask{
std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
// 关键:异常处理函数
void unhandled_exception() {
std::cout << "捕获到异常,存储起来\n";
exception = std::current_exception(); // 捕获异常
}
void return_value(int val) {
value = val;
}
};
std::coroutine_handle<promise_type> handle;
// 获取结果,如果有异常就抛出
int get_result() {
if (!handle.done()) {
handle.resume();
}
if (handle.promise().exception) {
std::cout << "重新抛出存储的异常\n";
std::rethrow_exception(handle.promise().exception);
}
return handle.promise().value;
}
~SimpleTask() {
if (handle) handle.destroy();
}
};
// 会抛出异常的协程函数
SimpleTask throwing_coroutine() {
std::cout << "异常协程开始执⾏\n";
co_await std::suspend_always{};
std::cout << "准备抛出异常...\n";
throw std::runtime_error("协程内部发⽣错误!"); // 抛出异常
co_return -1; // 这⾏不会执⾏
}
// ⽴即抛出异常的协程
SimpleTask immediate_throw_coroutine() {
std::cout << "⽴即抛出异常的协程\n";
throw std::logic_error("⽴即发⽣的错误!");
co_return 100; // 不会执⾏
}
int main() {
std::cout << "=== 测试异常协程 ===\n";
try {
auto task2 = throwing_coroutine();
int result = task2.get_result(); // 这⾥会抛出异常
std::cout << "结果: " << result << "\n\n";
}
catch (const std::runtime_error& e) {
std::cout << "捕获到 runtime_error: " << e.what() << "\n\n";
}
std::cout << "=== 测试⽴即异常 ===\n";
try {
auto task3 = immediate_throw_coroutine();
int result = task3.get_result(); // 这⾥会抛出异常
std::cout << "结果: " << result << "\n";
}
catch (const std::logic_error& e) {
std::cout << "捕获到 logic_error: " << e.what() << "\n";
}
return 0;
}
-
await_transform()
a. await_transform 是 Promise 类型的⼀个可选成员函数,它允许在 co_await 表达式上应⽤⾃定义转换。当编译器遇到 co_await expression 时,会查找 Promise 类型的 await_transform(type expression) ⽅法,找不到就编译报错,本质上就是⽀持把⼀个 expression 变成Awaitable对象。
b. 返回值:⼀个 Awaitable 概念对象
struct FileReadAwaiter {
int fd_;
FileReadAwaiter(int fd) : fd_(fd) {}
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// ... 使⽤异步 I/O 库(如 io_uring, libuv)来异步读取 fd_
// 当读取完成时,恢复协程 h
async_read(fd_, h);
}
std::string await_resume() {
// ... 返回读取到的数据
return read_buffer;
}
};
struct MyPromise {
// ... 其他 Promise 成员函数同上
// 当协程内 co_await ⼀个 int 时,我们将其视为⽂件描述符
// 并返回⼀个⾃定义的 FileReadAwaiter
auto await_transform(int fd) {
std::cout << "Interpreting int as file descriptor for async read\n";
return FileReadAwaiter{fd};
}
};
MyCoroutine my_coroutine() {
int socket_fd = open_socket(); // 假设返回⼀个⽂件描述符
std::string data = co_await socket_fd; // 神奇地变成了异步读取!
std::cout << "Received: " << data << std::endl;
}
🎀 协程句柄
- 协程句柄是⼀个⾮拥有的、轻量级的句柄,它代表并允许你从外部直接操作⼀个特定的协程帧,它是你与协程运⾏时交互的主要接⼝,你可以把它想象成⼀个指向特定协程的遥控器,有点类似于系统 API 的线程句柄,⽂件句柄,或者理解成类似指向 Promise 对象的智能指针,但是功能跟智能指针就不搭边了,智能指针的核⼼是要控制资源的释放,这⾥是要管理 Promise 对象。
- 通常我们在协程返回类型中定义 std::coroutine_handle handle 的句柄成员变量。
- 协程句柄的核⼼操作API主要有以下⼏个,更多的API请看⽂档
- https://en.cppreference.com/w/cpp/coroutine/coroutine_handle.html
-
from_promise() / promise()a. static coroutine_handle from_promise(Promise& p ); ,⽤于使⽤⼀个
Promise 对象创建⼀个指向 Promise 对象的句柄,通常在 Promise 类型的
get_return_object 成员函数中使⽤。b. Promise& promise() const; ⽅便我们使⽤句柄可以获取管理的 Promise 对象。
-
resume() / operator()a. 作⽤:让⼀个处于挂起状态的协程继续执⾏。
b. ⾏为:调⽤后,协程从上次挂起的地⽅开始执⾏,直到遇到下⼀个挂起点、结束或抛出异常。
c. 注意:不能在⾮挂起状态的协程上调⽤(如已运⾏完成或尚未开始的协程,⾏为未定义)
-
done()a. 作⽤:查询协程是否已执⾏完成(即是否已经到达函数体末尾或执⾏了 co_return )。
b. 返回值: true :协程已执⾏完成, false :协程仍在运⾏或处于挂起状态。
-
operator bool()a. 作⽤:查询是否是⼀个有效的协程句柄。
b. 返回值: true :指向⼀个有效的协程, false :协程句柄内存指向⼀个 nullptr 指针。
-
destroy()a. 作⽤:显式销毁协程帧,释放其内存。
b. 何时调⽤:通常在协程返回类型的析构中调⽤,只有当协程在 final_suspend() 返回std::suspend_always 挂起时,你才需要(且必须)⼿动调⽤ .destroy() 。如果 final_suspend() 返回 std::suspend_never ,协程会⾃动销毁⾃⼰,你再调⽤.destroy() 就是未定义⾏为。
c. 重要:忘记调⽤ .destroy() 会导致内存泄漏。
-
operator coroutine_handle<>()a. 作⽤:该转换函数将 std::coroutine_handle 值转换为包含相同底层address的 std::coroutine_handle<> 值。
b. 何时调⽤: void await_suspend(std::coroutine_handle<> h) 中作为形参接收
各协程的句柄。
🎀 Awaitable对象
-
在C++20协程中,任何可以出现在 co_await 表达式右边的对象,都是⼀个Awaitable对象。它的核⼼作⽤是:定义当协程执⾏到 co_await expression 的⾏为,具体来说它控制三个⽅⾯的问题:
a. 是否需要暂停当前协程?(await_ready)
b. 在暂停之前,需要安排什么异步操作?(await_suspend)
c. 在恢复之后,如何获取异步操作的结果?(await_resume)
-
⼀个类型要成为Awaitable,它必须实现三个特定的成员函数 await_ready/await_suspend/await_resume ,或者通过 operator co_await 重载返回⼀个实现了这些函数的对象
-
执行顺序:
await_ready()→ 如果返回false →await_suspend()→ 协程暂停 → (稍后)→await_resume()
-
await_suspend在暂停前执行:
- 这允许你在协程暂停之前做一些准备工作
- 比如注册回调、启动线程、设置定时器等
-
协程暂停的实际时刻:
- 是在
await_suspend函数返回之后
- 是在
-
用途:
- 在
await_suspend中可以安排如何以及何时恢复协程 - 这是实现异步操作的关键机制
- 在
await_suspend 是在协程暂停之前执行的,给你机会去安排协程的恢复时机和方式。
- 编译器会将 co_await expression 转换为⼀系列对这些函数的调⽤,让我们分解这个过程。
{
auto&& awaitable = <expression>; // 1. 获取 awaitable 对象
if (!awaitable.await_ready()) { // 2. 检查是否 ready
// 3. 准备暂停
// 4. 调⽤ await_suspend,传⼊当前协程的句柄
// 5. 根据 await_suspend 的返回值决定⾏为(⻅下⽂)
// ...
<suspend coroutine> // 协程在此处暂停
// ... 异步操作在后台进⾏ ...
// ... 当异步操作完成时,通过某种⽅式恢复协程 ...
// 6. 协程在此时恢复执⾏
}
// 7. 获取结果
return awaitable.await_resume(); // 整个 co_await 表达式的结果
}
-
bool await_ready() consta. ⽬的:性能优化。在尝试暂停之前,检查异步操作是否已经完成。
b. 返回值: true :表⽰结果已就绪,⽆需暂停协程。编译器将跳过 await_suspend 和暂停步骤,直接调⽤ await_resume 并继续执⾏。 false :表⽰结果未就绪,需要暂停协程,继续执⾏ await_suspend 逻辑。
c. 最佳实践通常实现为 noexcept 。对于⽴即完成的操作(如缓存命中),返回 true 可以避免不必要的暂停开销
-
type await_suspend( std::coroutine_handle<>) const
a. 这是最强⼤也是最复杂的部分,它在协程即将暂停但尚未暂停时被调⽤。
b. ⽬的:安排异步操作的回调,它的职责是获取当前协程的句柄 handle ,并将其传递给某个异步操作,以便在操作完成后恢复协程。
c. 参数: std::coroutine_handle<> ,代表当前正在执⾏的协程,你可以保存它、传递它。
d. 返回值和⾏为:返回值类型⾄关重要,它决定了控制流的⾛向
e. 返回值⾏为 void ,默认⾏为,协程暂停,控制权返回到当前协程调⽤者。这是最常⻅的情况。
f. 返回类型为 bool , true 表⽰协程已挂起到后台(可能在另⼀个线程恢复); false 表⽰不挂起,⽴即恢复,⽤于在最后关头发现需要等待的操作已完成的情况。
g. 返回类型为 std::coroutine_handle<> ,另⼀个协程句柄 handle 协程暂停,然后⽴即恢复 handle 所代表的协程,这实现了对称转移,是⽆栈协程链式调⽤的关键。
h. 最佳实践: await_suspend 通常实现为 noexcept ,在这⾥你通常会调⽤某个异步API,并将 handle 作为其完成回调,绝对不要在此函数内阻塞。
struct promise_type {
std::coroutine_handle<> handle;
// ...
};
struct Awaiter {
bool await_ready() noexcept { return false; }
// 这⾥就是返回另⼀个协程句柄的地⽅!
template<typename Promise>
std::coroutine_handle<> await_suspend(
std::coroutine_handle<Promise> current) noexcept {
// 获取当前协程的promise
auto& promise = current.promise();
// 返回另⼀个协程的句柄(如果存在)
if (promise.handle) {
return promise.handle; // 对称转移到另⼀个协程
}
else {
// 没有⽗协程,返回空句柄(回到主调线程)
return std::noop_coroutine();
}
}
void await_resume() noexcept {}
};
-
type await_resume() const
a. ⽬的:获取结果或处理错误。在协程恢复后或 await_ready 返回 true 时,此函数被调⽤以产⽣ co_await 表达式的结果。
b. 返回值:可以是 void 或任何其他类型,这个返回值就是整个 co_await 表达式的结果。
c. 如果异步操作产⽣⼀个值, await_resume() 返回这个值。
d. 如果异步操作可能失败, await_resume() 可以检查错误码并选择返回⼀个值或抛出⼀个异常。
e. 最佳实践:可以抛出异常,不抛异常时标记为 noexcept 。
-
C++20在标准库中提供了两个最简单的Awaitable类型
-
std::suspend_always
a. await_ready() :返回 false ,总是暂停。
b. await_suspend(…) :⽆操作,直接返回 void 。
c. await_resume() :返回 void 。
d. ⽤途:⽤于 initial_suspend() 和 final_suspend() ,表⽰请在此处暂停
-
std::suspend_never
a. await_ready() :返回 true ,从不暂停。
b. await_suspend(…) :永远不会被调⽤。
c. await_resume() :返回 void 。
d. ⽤途:⽤于 initial_suspend() ,表⽰请不要暂停,⽴即开始执⾏协程体。
-
·operator co_await()·
operator co_await 是⼀个⽤⼾定义的转换函数,它允许将⾃定义类型转换为可以被co_await 使⽤的Awaitable类型。
auto operator co_await() const & noexcept;
auto operator co_await() && noexcept;
auto operator co_await() & noexcept;
#include <coroutine>
class AdvancedTask {
int value_;
public:
AdvancedTask(int value) : value_(value) {}
// 对于 const左值 的 operator co_await
auto operator co_await() const & {
struct ConstLvalueAwaiter {
const AdvancedTask& task_;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<>) const {}
int await_resume() const {
return task_.value_ * 2; // 某种转换
}
};
return ConstLvalueAwaiter{*this};
}
// 对于 右值 的 operator co_await
auto operator co_await() && {
struct RvalueAwaiter {
AdvancedTask&& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
int result = task_.value_;
task_.value_ = -1; // 可以修改,因为是右值
return result;
}
};
return RvalueAwaiter{std::move(*this)};
}
// 对于 ⾮const左值 的 operator co_await
auto operator co_await() & {
struct LvalueAwaiter {
AdvancedTask& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
task_.value_ += 10; // 可以修改
return task_.value_;
}
};
return LvalueAwaiter{*this};
}
};
co_await AdvancedTask(10);
🦋 协程学习样例之⽣成器
- 以下样例主要实现⼀个 co_yield 的协程,主要通过 Promise 对象和协程句柄控制协程,⼏乎是最简单的协程程序。
#include <coroutine>
#include <exception>
#include <iostream>
// 1. 协程的返回类型
struct Generator
{
// 2. 核⼼:promise_type
struct promise_type
{
int current_value; // ⽤于存储产出的值
// 2a. 创建返回值对象
Generator get_return_object()
{
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 2b. 初始挂起:选择挂起,让协程惰性执⾏ std::suspend_always initial_suspend() { return {}; }
// 2c. 最终挂起:选择挂起,我们需要⼿动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2d. 处理 co_yield
std::suspend_always yield_value(int value)
{
current_value = value;
return {}; // 产出后总是挂起
}
// 2e. 处理 co_return; (⽆值返回)
void return_void() {}
// 2f. 处理异常
void unhandled_exception() { std::terminate(); }
};
// 3. Generator 类本⾝的成员
// 协程句柄,⽤于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数和析构函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator()
{
if (handle)
handle.destroy(); // 负责销毁协程帧
}
// 4. 提供给外部的API
// 获取当前值
int value() const
{
return handle.promise().current_value;
}
// 恢复执⾏直到下⼀个co_yield或结束
bool next()
{
if (!handle.done())
{
handle.resume();
}
return !handle.done();
}
};
// 使⽤这个⽣成器的协程函数
Generator range(int start, int end)
{
for (int i = start; i < end; ++i)
{
co_yield i; // 这会调⽤ promise.yield_value(i)
}
// 协程结束,调⽤ promise.return_void()
}
int main()
{
auto gen = range(1, 5); // 创建协程,initial_suspend挂起,此时协程未执⾏
while (gen.next())
{ // 恢复协程,协程执⾏到co_yield处挂起
std::cout << gen.value() << " "; // 从promise中获取产出的值
}
// 输出: 1 2 3 4
}
各阶段详细拆解
-
协程创建阶段(触发: auto gen = range(1,5) )
- 核⼼逻辑:调⽤ range 协程函数时,编译器⾃动创建协程帧(存储局部变量 i 、参数
start=1/end=5 、 promise_type 实例),并通过 promise 衔接 Generator 。 - 代码映射:
- 编译器⾃动构造 promise_type 实例(初始化 current_value );
- 调⽤ promise.get_return_object() :创建 Generator 对象,绑定
coroutine_handle<promise_type> (通过 from_promise(*this) 关联 promise ); - Generator 构造函数执⾏:保存 handle (后续控制协程的“⼿柄”)。
- 核⼼逻辑:调⽤ range 协程函数时,编译器⾃动创建协程帧(存储局部变量 i 、参数
-
初始挂起状态(触发: initial_suspend() )
- 核⼼逻辑:协程创建后不⽴即执⾏,⽽是先挂起( suspend_always 表⽰“总是挂起”),实现惰性执⾏(按需启动)。
- 代码映射:
- promise.initial_suspend() 返回 std::suspend_always{} ,协程暂停在 “执⾏前”状态;
- 此时 main 中拿到的 gen 已就绪,但 range 的 for 循环尚未开始执⾏。
-
恢复执⾏阶段(触发: gen.next() )
- 核⼼逻辑:外部通过 Generator 的 next() 接⼝,⽤ handle 唤醒协程,执⾏到下⼀个 co_yield 或协程结束。
- 代码映射:
- gen.next() 判断 !handle.done() (当前协程未结束),调⽤ handle.resume() ;
- 协程从挂起点恢复,执⾏ range 的逻辑:
- 第⼀次恢复:进⼊ for 循环, i=start=1 ,执⾏ co_yield 1 ;
- 后续恢复:从 co_yield 的挂起点继续, i++ (如 i=2 ),再次执⾏ co_yield 2 ,直到 i>=end ( i=4 后 i++=5 ,退出循环)。
-
产出值挂起状态(触发: co_yield )
- 核⼼逻辑: co_yield 是协程“产出值”的关键字,本质是调⽤ promise.yield_value() ,并挂起协程,让外部获取产出值。
- 代码映射:
- co_yield value →调⽤ promise.yield_value(value) ,将 value 存⼊ promise.current_value ;
- yield_value() 返回 std::suspend_always{} ,协程再次挂起;
- 此时 gen.next() 返回 !handle.done() ( true ),外部通过 gen.value() 读取 promise.current_value (如第⼀次读取 1 ),并打印。
-
协程结束状态(触发: for 循环退出)
- 核⼼逻辑: range 的 for 循环执⾏完毕( i=5 >= end=5 ),协程⽆更多代码可执⾏,进⼊结束流程。
- 代码映射:
- 协程执⾏到函数末尾,编译器⾃动调⽤ promise.return_void() (处理 co_return; ⽆值返回的场景);
- 此时协程标记为“已结束”( handle.done() 变为 true )。
-
最终挂起状态(触发: final_suspend() )◦ 核⼼逻辑:协程结束后,通过 final_suspend() 挂起,等待外部⼿动销毁(避免协程帧提前释放)。
- 代码映射:
- promise.final_suspend() 返回 std::suspend_always{} ,协程保持挂起状态;
- 此时 gen.next() 再次调⽤时, handle.done() 为 true , next() 返回 false , main 的 while 循环退出。
- 代码映射:
-
协程销毁阶段(触发: gen 析构)
- 核⼼逻辑: main 函数结束, Generator 对象 gen 出作⽤域,析构函数释放协程帧内存。
- 代码映射:
- Generator::~Generator() 判断 handle 有效,调⽤ handle.destroy() ;
- 销毁协程帧(释放 i 、 start 、 end 、 promise_type 实例的内存),协程⽣命周期完全结束。
- 换⼀种⽅式,跟上⾯代码功能类似,实现⽀持迭代器的协程返回对象。
#include <coroutine>
#include <iostream>
template <typename T>
struct Generator
{
struct promise_type
{
T current_value;
Generator get_return_object()
{
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T value)
{
current_value = value;
return {};
}
void return_void() {}
};
struct iterator
{
std::coroutine_handle<promise_type> handle;
iterator(std::coroutine_handle<promise_type> h = nullptr) : handle(h)
{
}
iterator &operator++()
{
if (handle && !handle.done())
{
handle.resume();
}
return *this;
}
T operator*() const
{
return handle.promise().current_value;
}
bool operator!=(const iterator &other) const
{
return !handle.done();
}
};
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator()
{
if (handle)
handle.destroy();
}
iterator begin()
{
if (handle)
handle.resume();
return iterator{handle};
}
iterator end() { return iterator{}; }
private:
std::coroutine_handle<promise_type> handle;
};
// 使⽤⽰例
Generator<int> range(int start, int end)
{
for (int i = start; i < end; ++i)
{
co_yield i; // 产⽣值并挂起
}
}
Generator<int> fibonacci(int limit)
{
int a = 0, b = 1;
while (a <= limit)
{
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
int main()
{
std::cout << "Range 1-5: ";
for (auto i : range(1, 6))
{
std::cout << i << " ";
}
std::cout << std::endl;
/* 等价写法
auto gen = range(1, 6);
auto it = gen.begin();
while (it != gen.end()) {
std::cout << *it << std::endl;
++it;
}*/
std::cout << "Fibonacci below 50: ";
for (auto num : fibonacci(50))
{
std::cout << num << " ";
}
std::cout << std::endl;
}
🦋 协程学习样例之异步线程恢复
这个样例展⽰了co_await的使⽤⽅法,其次就是展⽰了,协程可以在另⼀个线程中恢复执⾏后续逻辑
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 增强的可等待对象,添加更多⽇志
struct AsyncOperation
{
int value;
bool ready = false;
const char *name;
AsyncOperation(int v, bool r, const char *n = "")
: value(v), ready(r), name(n)
{
std::cout << "[" << name << "] AsyncOperation 构造: value=" << value << ", ready=" << ready << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
bool await_ready() const noexcept
{
std::cout << "[" << name << "] await_ready() 调⽤, 返回: " << ready << "(线程 : " << std::this_thread::get_id() << ") " << std::endl;
return ready;
}
void await_suspend(std::coroutine_handle<> handle) noexcept
{
std::cout << "[" << name << "] await_suspend() 调⽤, 协程句柄: " << handle.address() << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 启动异步操作
std::thread([this, handle, name = this->name]() {
std::cout << "[" << name << "] 异步线程开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
this->ready = true;
std::cout << "[" << name << "] 异步操作完成, 准备恢复协程" << " (线程:" << std::this_thread::get_id() << ")"<< std::endl;
// 恢复协程执⾏
handle.resume();
std::cout << "[" << name << "] 异步线程结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl; }).detach();
std::cout << "[" << name << "] await_suspend() 返回, 协程即将挂起" << "(线程 : " << std::this_thread::get_id() << ") " << std::endl;
}
int await_resume() noexcept
{
std::cout << "[" << name << "] await_resume() 调⽤, 返回结果: " << value << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return value;
}
};
struct Task
{
struct promise_type
{
int current_value;
Task get_return_object()
{
std::cout << "promise_type::get_return_object() 调⽤" << std::endl;
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() noexcept
{
std::cout << "promise_type::initial_suspend() 调⽤ - ⽴即开始执⾏"
<< std::endl;
return {};
}
std::suspend_always final_suspend() noexcept
{
std::cout << "promise_type::final_suspend() 调⽤ - 协程结束" << std::endl;
return {};
}
void unhandled_exception()
{
std::cout << "promise_type::unhandled_exception() 调⽤" << std::endl;
}
void return_void()
{
std::cout << "promise_type::return_void() 调⽤ - 协程正常返回" << std::endl;
}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h)
{
std::cout << "Task 对象构造, 句柄: " << handle.address() << std::endl;
}
~Task()
{
if (handle)
{
std::cout << "Task 析构, 销毁协程句柄" << std::endl;
handle.destroy();
}
}
};
Task detailed_async_example()
{
std::cout << "=== 协程函数开始执⾏ ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第⼀次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result1 = co_await AsyncOperation{100, false, "操作1"};
std::cout << "第⼀次 co_await 后继续执⾏, 结果: " << result1 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第⼆次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result2 = co_await AsyncOperation{200, false, "操作2"};
std::cout << "第⼆次 co_await 后继续执⾏, 结果: " << result2 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第三次 co_await (就绪状态) ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result3 = co_await AsyncOperation{300, true, "操作3"};
std::cout << "第三次 co_await 后继续执⾏, 结果: " << result3 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n=== 协程函数执⾏完成 ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
int main()
{
std::cout << "主函数开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
{
std::cout << "\n********** 创建协程任务 **********" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
auto task = detailed_async_example();
std::cout << "协程任务已创建,控制权返回主函数" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 给异步操作时间完成
std::cout << "\n主线程等待 3 秒..." << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "主线程等待结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
} // task 析构
std::cout << "\n主函数结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return 0;
}
这样子除了第一次挂起是和主线程相关的,后续的协程恢复和执行都是在其他线程上执行的,不会影响到主线程
协程 co_await 创建了一个恢复线程然后回到主线程,主线程就开始了自己的独立运行了,此时恢复线程恢复了协程,协程继续执行到下一个 co_await 又创建了一个恢复线程2,然后挂起回到恢复线程1,恢复线程1销毁,然后恢复线程2恢复协程执行
🦋 协程学习样例之echo服务器
以下代码实现了⼀个简单 epoll+ 协程的回声服务器,帮助我们理解协程⼀些使⽤场景,需要时 gcc10 及以后版本上运⾏,因为这个版本开始才⽀持协程。
// g++ -std=c++20 test.cpp
// 编译运⾏以下代码以后,客⼾端使⽤⼀个telnet localhost 9000就可以进⾏测试
#include <iostream>
#include <coroutine>
#include <thread>
#include <vector>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <memory>
#include <array>
// 将⽂件描述符设置为⾮阻塞模式
static void set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0); // 获取当前⽂件状态标志
if (flags == -1)
{
perror("fcntl F_GETFL");
return;
}
// 添加⾮阻塞标志 if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
perror("fcntl F_SETFL O_NONBLOCK");
}
}
// I/O 等待器:⽤于协程等待 epoll 事件
struct IoAwaiter
{
int fd; // 要等待的⽂件描述符
int events; // 要等待的事件类型:EPOLLIN(可读) 或 EPOLLOUT(可写)
epoll_event ev{}; // epoll 事件结构
// 静态成员,保存 epoll 实例的⽂件描述符
static inline int epfd = -1;
// 协程等待开始前调⽤
// 返回 false 表⽰需要挂起等待
bool await_ready() const noexcept
{
return false;
}
// 挂起协程,并将事件注册到 epoll
void await_suspend(std::coroutine_handle<> h) noexcept
{
// 将协程句柄存到 epoll_event 的 data.ptr 中
ev.data.ptr = h.address();
// 设置事件类型,并使⽤边缘触发(EPOLLET)
ev.events = events | EPOLLET;
// 将 fd 和事件注册到 epoll
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
// 协程恢复后调⽤
// 从 epoll 中移除该事件
void await_resume() noexcept
{
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
}
};
// 等待 accept 事件的协程等待器
IoAwaiter co_accept(int fd)
{
return IoAwaiter{fd, EPOLLIN};
}
// 等待读事件的协程等待器
IoAwaiter co_read(int fd)
{
return IoAwaiter{fd, EPOLLIN};
}
// 等待写事件的协程等待器
IoAwaiter co_write(int fd)
{
return IoAwaiter{fd, EPOLLOUT};
}
// 协程任务类型
struct Task
{
struct promise_type
{
// 返回协程对象
Task get_return_object() { return {}; }
// 初始不挂起
std::suspend_never initial_suspend() noexcept { return {}; }
// 结束不挂起
std::suspend_never final_suspend() noexcept { return {}; }
// 协程返回 void
void return_void() {}
// 异常处理
void unhandled_exception() { std::terminate(); }
};
};
// 每个客⼾端连接的处理协程
Task echo_session(int sock)
{
try
{
std::array<char, 4096> buf; // 缓冲区
for (;;)
{
// 等待 socket 可读
co_await co_read(sock);
// 读取数据
ssize_t n = read(sock, buf.data(), buf.size());
if (n <= 0)
{
// 连接关闭或出错
if (n < 0)
perror("read");
break;
}
// 等待 socket 可写
co_await co_write(sock);
// 回写数据
write(sock, buf.data(), n);
}
}
catch (...)
{
std::cerr << "Session error\n";
}
close(sock); // 关闭连接
}
// 监听协程:接受新连接并创建会话协程
Task listener(int port)
{
// 创建监听 socket
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (s < 0)
{
perror("socket");
co_return;
}
// 设置 socket 选项:允许地址重⽤
int opt = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址和端⼝
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有⽹卡
addr.sin_port = htons(port); // 端⼝号
if (bind(s, (sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("bind");
co_return;
}
// 开始监听
if (listen(s, SOMAXCONN) < 0)
{
perror("listen");
co_return;
}
std::cout << "Listening on port " << port << std::endl;
// 循环接受新连接
for (;;)
{
// 等待监听 socket 可读(有新连接)
co_await co_accept(s);
// 接受连接
sockaddr_in peer{};
socklen_t len = sizeof(peer);
int client = accept4(s, (sockaddr *)&peer, &len, SOCK_NONBLOCK);
if (client < 0)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("accept");
}
continue;
}
std::cout << "New connection from " << inet_ntoa(peer.sin_addr) << std::endl;
// 为新连接创建会话协程
echo_session(client);
}
}
int main()
{
// 创建 epoll 实例
IoAwaiter::epfd = epoll_create1(0);
if (IoAwaiter::epfd < 0)
{
perror("epoll_create1");
return 1;
}
// 启动监听协程
listener(9000);
// 事件循环
std::vector<epoll_event> events(64); // 事件数组
for (;;)
{
// 等待事件发⽣
int nfds = epoll_wait(IoAwaiter::epfd, events.data(), events.size(), -1);
if (nfds < 0)
{
perror("epoll_wait");
break;
}
// 处理每个就绪事件 for (int i = 0; i < nfds; ++i)
{
// 从事件中取出协程句柄并恢复执⾏
auto h = std::coroutine_handle<>::from_address(events[i].data.ptr);
h.resume();
}
}
}
以下代码是使⽤ Boost.Asio从1.74.0版本及以后版本(这个版本开始⽀持C++20协程),实现的⼀个简单的回声服务,展⽰了如果库⽀持以后,协程和⽹络编程配合实现,不需要像上⾯这么⿇烦,由我们去控制协程三⼤组件的细节。
// sudo apt update
// sudo apt install libboost-all-dev
// g++ -std=c++20 echo_server.cpp -o echo_server -lboost_system
#include <iostream>
#include <array>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
// 会话协程:处理⼀个客⼾端连接
boost::asio::awaitable<void> echo_session(tcp::socket socket)
{
try
{
std::array<char, 4096> buf;
for (;;)
{
// 异步读数据
std::size_t n = co_await socket.async_read_some(
boost::asio::buffer(buf), boost::asio::use_awaitable);
// 异步写回数据
co_await boost::asio::async_write(socket, boost::asio::buffer(buf, n), boost::asio::use_awaitable);
}
}
catch (std::exception &e)
{
std::cerr << "Session error: " << e.what() << std::endl;
}
}
// 监听协程:接受新连接
boost::asio::awaitable<void> listener(unsigned short port)
{
tcp::acceptor acc(co_await boost::asio::this_coro::executor,
tcp::endpoint(tcp::v4(), port));
std::cout << "Listening on port " << port << std::endl;
for (;;)
{
tcp::socket socket = co_await acc.async_accept(boost::asio::use_awaitable);
std::cout << "New connection from "
<< socket.remote_endpoint() << std::endl;
// 启动新会话(不等待它完成)
boost::asio::co_spawn(acc.get_executor(),
echo_session(std::move(socket)),
boost::asio::detached);
}
}
int main()
{
try
{
boost::asio::io_context io;
// 启动监听协程
boost::asio::co_spawn(io, listener(9000), boost::asio::detached);
// 运⾏事件循环
io.run();
}
catch (std::exception &e)
{
std::cerr << "Server error: " << e.what() << std::endl;
}
}
🦋 调用流程总结

四:🔥 模块
C++20 引⼊的模块 (Modules) 是 C++ 语⾔的⼀项重⼤⾰新,旨在解决传统头⽂件包含机制(#include) 的诸多问题。下⾯我将全⾯讲解这⼀特性。
传统头⽂件包含的问题:
- 编译时间⻓:每次包含头⽂件时,编译器都需要重新解析其内容,导致编译时间⼤幅增加。
- 依赖管理复杂:头⽂件的依赖关系可能导致复杂的包含顺序问题,容易引发错误。
- 命名空间污染:头⽂件中声明的符号可能会意外地进⼊全局命名空间,导致命名冲突。
模块是⼀种新的代码组织⽅式,具有以下特点:
- 编译⼀次,多次使⽤:模块接⼝只编译⼀次,⽣成⼆进制表⽰供后续使⽤
- 隔离性:模块内部细节默认对外不可⻅
- ⽆宏泄漏:模块内的宏定义不会影响导⼊者
- 更快的编译:避免了重复解析相同的头⽂件
- 更清晰的语义:明确区分接⼝和实现
模块化的实践案例:
- https://xie.infoq.cn/article/5c81d38dcb3949dc0ebb58fa1
- https://help.aliyun.com/zh/alinux/getting-started/install-and-use-alibaba-cloudcompiler#e186cc2a7a7kd
🦋 模块的基本语法
- https://en.cppreference.com/w/cpp/language/modules.html
- 跟之前类似模块分为声明⽂件和实现⽂件,声明⽂件后缀⼀般为.ixx或者.cppm,C++标准没有对这个后缀给严格的规定,⼀般是编译器⾃⼰定义的,MSVC(VisualStudio)系列下⾯⼀般为.ixx,但是使⽤ .cppm 也是可以的;实现⽂件⼀般为 .cpp。
- 模块声明⽂件开头添加 export module 模块名 。声明⽂件中需要给外部使⽤的类型/函数/变量等需要⽤ export 声明 导出,否则外部不能使⽤。
- 模块实现⽂件开头添加 module 模块名 。
- 模块使⽤⽂件中 import 模块名 。
- C++20模块的设计⽬标是让代码的导⼊和导出更加⾼效和清晰,为了实现这⼀点,模块单元在语法上受到严格限制,在⼀个模块单元中,你只能有 module 和 import 指令,不能有#include 或其他预处理指令。但是这样也就导致了另⼀个很⿇烦的问题,很多标准库或者第三⽅库等还没有模块化,那么我们的项⽬模块化了,但是就⽆法跟他们兼容使⽤,全局模块⽚段就是为了解决这个⽭盾⽽⽣的。它为你提供了⼀个“安全区”,可以在模块单元正式内容开始之前,放置这些必须的预处理指令。
module; // 开启全局模块⽚段
// 只能有预处理指令。任何⾮预处理指令的 C++ 代码都会导致编译错误。
#include <iostream>
#include "third_party/opengl.h"
#define OLD_MACRO 42
// 全局模块⽚段到此结束
export module MyModule; // 模块声明,标志着全局模块⽚段的结束,这⾥也可以不加export
模块声明
MyModule/
├── math.ixx # 主模块接⼝
├── math.cpp # 主模块实现
└── test.cpp # 主程序
// math.ixx 模块声明⽂件,类似于之前的.h
// vs编译器⼀般喜欢⽤.ixx为后缀的⽂件,也可以使⽤.cppm为后缀
// 全局模块⽚段的⽅式包含库头⽂件/第三⽅头⽂件
module;
#include<iostream>
export module math; // 声明模块名称
// 导出函数hello
export char const* hello() { return "hello"; }
// 函数world未被导出,外部不能使⽤,内部才能使⽤
char const* world() { return "world"; }
// 多个函数⼀起导出
export
{
int one() { return 1; }
int zero() { return 0; }
}
// 导出命名空间中的内容
export namespace bit {
int add(int a, int b);
}
// 模板类似以前的处理,⼀般定义到声明⽂件
// 声明和定义分离,定义部分放到
// 实现⽂件会导致链接错误,需要显⽰实例化解决
export template<class T>
void TFunc(const T& x)
{
std::cout << x << std::endl;
}
// 导出类
export class A
{
public:
void f1();
private:
int _a1 = 1;
};
模块实现
// math.cpp 模块实现⽂件,类似于之前的.cpp
// 全局模块⽚段的⽅式包含库头⽂件/第三⽅头⽂件
module;
#include<iostream>
// math.cpp 模块实现⽂件
module math;
namespace bit {
int add(int a, int b)
{
return a + b;
}
}
//template<class T>
//void TFunc(const T& x)
//{
// std::cout << x << std::endl;
//}
void A::f1()
{
std::cout << world() << std::endl;
std::cout << "void A::f1()->" << _a1 << std::endl;
}
使⽤模块
// test.cpp
import math; // 导⼊⾃定义模块
#include<iostream>
#include<vector>
int main()
{
std::cout << hello() << std::endl;
// world未导出外部不能使⽤
//std::cout << world() << std::endl;
std::cout << bit::add(2, 3) << std::endl;
TFunc(1);
A aa;
aa.f1();
std::vector<int> v = { 1,2,3,4 };
for (auto e : v)
{
std::cout << e << " ";
}
std::cout << std::endl;
return 0;
}
🦋 模块的分区
模块分区 (ModulePartitions) 是 C++20 模块系统中的⼀个重要特性,它允许将⼤型模块拆分为多个逻辑单元,同时保持模块的封装性和⼀致性。
- 为什么需要模块分区?
- 管理⼤型模块:当模块变得庞⼤时,分区可以将其拆分为更易管理的部分
- 逻辑分组:将相关功能组织在⼀起
- 减少编译依赖:可以独⽴编译分区
- 保持封装性:分区仍然是模块内部实现细节,对外不可⻅
- 分区特性
- 分区是模块的内部实现细节
- 外部⽤⼾⽆法直接导⼊分区
- 分区之间可以相互引⽤
- 主模块接⼝负责导出分区内容
🎀 基本分区语法
// 分区声明
export module 模块名:分区名;
// ⽰例:
export module math:algebra; // 声明math模块的algebra分区
// 分区实现
module 模块名:分区名;
// ⽰例:
module math:algebra; // 实现math模块的algebra分区
// 分区导⼊
import :分区名; // 导⼊同⼀模块的其他分区
// ⽰例:
export module math:geometry;
import :algebra; // 导⼊同⼀模块的algebra分区
// 分区导出(主模块中)
export import :分区名; // 导出分区内容
// ⽰例:
export module math;
export import :algebra; // 导出algebra分区
🎀 分区关键规则
-
命名规则:
- 分区名必须是有效的C++标识符
- 不能以数字开头
- 区分⼤⼩写
-
可⻅性规则:
- 分区内容默认对模块外不可⻅
- 只有通过主模块 export import 导出的分区内容才对外可⻅
-
依赖规则:
- 分区可以循环依赖
- 分区可以导⼊同⼀模块的其他分区
-
⽂件扩展名:
- 通常使⽤ .cppm 或 .ixx 表⽰模块接⼝⽂件
- 实现⽂件通常使⽤ .cpp
🎀 完整语法结构⽰例
MyModule/
├── time_library.ixx # 主模块接⼝
├── time_core.ixx # 分区1接⼝
├── time_formatting.ixx # 分区2接⼝
├── time_core.cpp # 分区1实现
├── time_formatting.cpp # 分区2实现
├── time_library.cpp # 主模块实现
└── main.cpp # 主程序
// time_library.ixx
// 导出主模块
export module time_library;
// 导出分区
export import :formatting;
export import :core;
// 主模块也可以直接添加内容
export namespace bit {
Time now_time(); // 获取当前时间
Date now_date(); // 获取当前⽇期
}
// time_core.ixx
export module time_library:core;
export namespace bit {
class Date
{
public:
Date(int y, int m, int d);
// 其他成员函数
//private:
int _y;
int _m;
int _d;
};
class Time {
public:
Time(int h, int m, int s);
// 其他成员函数
//private:
int _h;
int _m;
int _s;
};
}
// time_formatting.ixx
module;
#include <string>
export module time_library:formatting;
import :core; // 导⼊核⼼分区
export namespace bit {
std::string format_time1(const Date& d, const Time& t);
std::string format_time2(const Date& d, const Time& t);
}
// time_library.cpp
module;
#define _CRT_SECURE_NO_WARNINGS 1
#include <chrono>
module time_library;
namespace bit {
Time now_time()
{
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::tm* local_time = std::localtime(&now_time);
int hour = local_time->tm_hour;
int minute = local_time->tm_min;
int second = local_time->tm_sec;
return { hour, minute, second };
}
Date now_date()
{
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::tm* local_time = std::localtime(&now_time);
int year = 1900 + local_time->tm_year;
int month = 1 + local_time->tm_mon;
int day = local_time->tm_mday;
return { year, month, day };
}
}
// time_core.cpp
module time_library:core;
namespace bit {
Date::Date(int y, int m, int d)
:_y(y)
, _m(m)
, _d(d)
{}
Time::Time(int h, int m, int s)
:_h(h)
, _m(m)
, _s(s)
{}
}
// time_formatting.cpp
module;
#include <string>
module time_library:formatting;
import :core; // 导⼊核⼼分区
namespace bit {
std::string format_time1(const Date& d, const Time& t)
{
std::string s;
s += std::to_string(d._y);
s += "年";
s += std::to_string(d._m);
s += "⽉";
s += std::to_string(d._m);
s += "⽇";
s += std::to_string(t._h);
s += "时";
s += std::to_string(t._m);
s += "分";
s += std::to_string(t._s);
s += "秒";
return s;
}
std::string format_time2(const Date& d, const Time& t)
{
std::string s;
s += std::to_string(d._y);
s += "/";
s += std::to_string(d._m);
s += "/";
s += std::to_string(d._m);
s += " ";
s += std::to_string(t._h);
s += ":";
s += std::to_string(t._m);
s += ":";
s += std::to_string(t._s);
return s;
}
}
// main.cpp - 使⽤时间库
import time_library;
#include<iostream>
int main() {
bit::Date d = bit::now_date();
bit::Time t = bit::now_time();
std::cout << bit::format_time1(d, t) << std::endl;
std::cout << bit::format_time2(d, t) << std::endl;
return 0;
}
🦋 标准库的模块化
- C++20 引⼊模块核⼼语⾔特性,但未规定标准库模块化
- C++23 正式引⼊标准库模块规范,定义 std 和 std.compat 基本模块,C++26 预计进⼀步完善模块化,可能增加更多细分模块
- https://en.cppreference.com/w/cpp/standard_library.html#Importing_modules
- 主流的编译器中 MSVC(VisualStudio) 对标准库的模块化 importstd 及细分模块均⽀持,MSVC19.35 以后就开始相对稳定的⽀持,不过需要进⾏⼀系列的配置,具体操作下⾯部分会讲解。
- gcc 对标准库模块化的⽀持相对缓慢⼀些,截⽌最新的 gcc15 已经开始⽀持importstd,但需注意实现可能尚未完全覆盖所有⼦模块
- 截⽌最新的 clang18 ⽀持C++23的importstd(完整标准库模块),但需注意实现可能尚未完全覆盖所有⼦模块
- 整体⽽⾔标准库模块化⽀持进展中,MSVC(VisualStudio)是⽀持最好最快的,clang 次之,最慢的是 gcc。
- C++23⽬前主要标准化了 std (C++标准库的主要组件但不包括C兼容库)和 std.compat (C标准库兼容),但未来可能会进⼀步拆分模块,可能需要等待C++26或更⾼版本,例如:
- std.core (标准库核⼼部分)
- std.filesystem (⽂件系统操作)
- std.threading (并发和线程)
- std.ranges (范围库)
- std.coroutine (协程⽀持)
- https://help.aliyun.com/zh/alinux/getting-started/install-and-use-alibabacloudcompiler#e186cc2a7a7kd 和 https://xie.infoq.cn/article/5c81d38dcb3949dc0ebb58fa1这⾥可以看到国内阿⾥云⾃⼰基于基于 Clang/LLVM-13 社区开源版本开发开发了⼀个 C++ 编译器,⽀持了 C++ 标准库模块化和内部项⽬的模块化。
- 下⾯的程序已经使⽤了importstd 和 importstd.compat,那么在 MSVC(VisualStudio) 下直接创建⼀个项⽬是⽆法编译通过的,需要对项⽬进⾏⼀系列配置,具体细节如下:
import std;
import std.compat;
int main() {
// 使⽤range算法
std::vector<int> numbers = { 5, 2, 8, 1, 9 };
std::ranges::sort(numbers);
for (auto& num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 格式化输出 (C++20)
std::cout << std::format("Sorted numbers: {}\n", numbers);
// 使⽤span (C++20)
std::span<int> numberView{ numbers };
for (auto& num : numberView) {
num *= 2;
}
for (auto& num : numberView) {
std::cout << num << " ";
}
std::cout << std::endl;
printf("hello world");
return 0;
}
- 使⽤本章节开头的程序检查⾃⼰的vs2022版本>19.35,若版本不够,点击⼯具 -> 获取⼯具和功能 -> VisualStudioInstaller -> 更新到最新的版本。
- 下载安装对应的 C++ 标准库模块,点击⼯具 -> 获取⼯具和功能 -> VisualStudioInstaller -> 修改 -> 单组组件 -> 搜索框输⼊模块 -> 适⽤于 v143 ⽣成⼯具的 C++ 模块 (x64.x86-实验性)勾选上 -> 右下⻆修改
- ⼯程属性中进⾏配置,右键⼯程属性 -> C/C++ -> 常规 -> 扫描源以查找模块依赖关系 -> 选择是
- ⼯程属性中进⾏配置,右键⼯程属性 -> C/C++ -> 语⾔ -> C++ 语⾔标准 -> 选择std:c++latest或 C++23
- 以上操作如果还有疑问,可以在下⾯找⼀下对应的图。





🦋 模块化的性能提升及优势
下⾯是使⽤ deepseek 搜索 C++ 模块化和传统的包含头⽂件相⽐能快多少
C++20引⼊的模块(Modules)相⽐传统头⽂件(#include)在编译速度上有显著提升,具体优势取决于项⽬规模和使⽤⽅式。以下是关键对⽐和量化分析:
- 编译速度提升
- 头⽂件的问题:
- 重复解析:每个翻译单元(.cpp⽂件)包含头⽂件时,都需要重新解析和编译头⽂件内容。例如, 在100个⽂件中被包含,就会被解析100次。
- 宏和模板膨胀:头⽂件中的宏和模板会在每个包含它的翻译单元中展开,增加编译开销。
- 模块的优势:
- ⼀次性编译:模块接⼝( .ixx 或 .cppm )只需编译⼀次,⽣成⼆进制模块接⼝( .pcm ⽂件),后续导⼊( import )直接复⽤预处理结果。
- 隔离性:模块只导出显式声明的内容,避免宏污染和隐式依赖。
- 实测数据:
- ⼩型项⽬:可能快20%-50%(因模块化减少冗余解析)。◦ ⼤型项⽬(如>10万⾏代码):编译速度可提升2-5倍(微软报告某些案例减少40%-70%的编译时间)。
- 其他优势
- 更快的增量编译:仅修改模块实现时,依赖它的⽂件⽆需重新编译(头⽂件修改会导致所有包含它的⽂件重新编译)。
- 更好的代码隔离:模块隐藏私有实现,减少名称冲突。
- 更少的预处理开销:⽐如之前⼀个header.h中有很多宏和条件编译,包含在10个.cpp,预处理展开处理100次次,现在模块化header,只需要预处理⼀次,然后import使⽤即可。
五:🔥 范围
C++20引⼊了Ranges库,这是标准库的⼀个重要扩展,提供了更强⼤、更直观的⽅式来处理序列和范围操作。Ranges旨在替代传统的迭代器模式,使代码更简洁、更安全、更具表现⼒。
🦋 Ranges的基本概念
什么是Range?
在C++20中,Range是指可以迭代的元素序列,它可以是:
- 容器(如 std::vector , std::list )
- 原⽣数组
- 定义了begin和end迭代器的序列
- ⽣成器⽣成的序列
- 任何满⾜ std::ranges::range 概念的类型都是Range。
https://en.cppreference.com/w/cpp/header/ranges.html
template<typename T>concept range = requires(T& t) {
std::ranges::begin(t);
std::ranges::end(t);};
Ranges的核⼼优势
- 更简洁的语法:不再需要显式使⽤begin/end
- 惰性求值:视图操作不会⽴即执⾏
- 可组合性:可以链式调⽤多个操作
- 安全性增强:减少迭代器失效等问题
🦋 Range视图
视图是范围的⼀种特殊形式,它代表对另⼀个范围的转换或过滤,但通常不拥有数据。视图的特点是:
- 时间复杂度为O(1)的构造/复制/移动
- ⾮拥有语义(不管理内存)
- 惰性求值(只在需要时计算)
C++20标准库提供了多种视图,主要分为以下⼏类:
适配器视图(Adaptors)
- 这些视图通过管道操作符(|)组合:
auto result = range | view1 | view2 | view3 - https://en.cppreference.com/w/cpp/header/ranges.html 的 Adaptors 部分
- 常⻅的适配器视图
- filter:过滤元素
- transform:转换元素
- take:取前N个元素
- drop:跳过前N个元素
- reverse:反转序列
#include <ranges>
#include <vector>
#include <iostream>
int main()
{
std::vector<int> numbers = { 1, 2, 3, 4, 5 };
auto even = numbers | std::views::filter([](int x) { return x % 2 == 0; });
//auto even = std::views::filter(numbers, [](int x) { return x % 2 == 0;
});
//auto even = std::ranges::filter_view(numbers, [](int x) { return x % 2吗 == 0; });
//std::cout << typeid(even).name() << std::endl;
auto squared = numbers | std::views::transform([](int x) { return x * x;
});
auto first3 = numbers | std::views::take(3);
auto after2 = numbers | std::views::drop(2);
auto reversed = numbers | std::views::reverse;
// 将这⾥范围for遍历视图对象依次替换为上⾯的视图对象
for (auto e : even)
{
std::cout << e << " ";
}
std::cout << '\n';
// 视图可以链式组合:
auto result = numbers
| std::views::filter([](int x) { return x > 2; })
| std::views::transform([](int x) { return x * 2; })
| std::views::take(2);
for (auto e : result)
{
std::cout << e << " ";
}
std::cout << '\n';
return 0;
}
⼯⼚视图 (Factories)
范围⼯⼚视图 (RangeFactoryViews) 是 C++20Ranges 库中⼀类特殊的视图,它们不依赖于现有数据容器,⽽是能够直接⽣成元素序列。这些视图提供了创建各种序列的便捷⽅式,是函数式编程⻛格的重要⼯具。
常⻅的⼯⼚试图:
- iota-⽣成⼀个⽆限或有限的整数序列
- repeat-创建⼀个重复元素视图
- empty-创建⼀个空范围
- single-创建包含单个元素的范围
- istream-它允许将输⼊流(如⽂件流或标准输⼊)转换为⼀个惰性求值的范围视图
#include <ranges>
#include <vector>
#include <iostream>
#include <sstream>
#include <fstream>
#include <cmath>
#include <thread>
#include <iomanip>
// ⽣成⼀个fibonacci数列的范围
auto fibonacci() {
// ⾃动⽣成⽆限序列的使⽤场景
return std::views::iota(0) | std::views::transform([](int i) {
static int a = 0, b = 1;
int next = a + b;
a = b;
b = next;
return a;
});
}
void read_file_lines(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
std::cerr << "Failed to open file\n";
return;
}
// 将⽂件视为⾏的范围
auto lines = std::views::istream<std::string>(file);
// 处理每⾏内容
for (const auto& line : lines) {
std::cout << "Line: " << line << '\n';
}
}
int main()
{
// ⽆限序列从0开始
auto infinite = std::views::iota(0);
// 有限序列从1到10
auto finite = std::views::iota(1, 11);
// 将这⾥范围for遍历视图对象依次替换为上⾯的视图对象
for (auto e : finite)
{
std::cout << e << " ";
}
std::cout << '\n';
// 创建⼀个空范围
auto e = std::views::empty<int>;
// 创建包含单个元素的范围
auto s = std::views::single(42);
// 创建包含42个重复元素的范围
auto r = std::views::repeat(42, 10);
// 这⾥也体现了什么是惰性求职,只有我们去遍历这个范围时,才会运⾏计算⽣成具体的数列值
for (int i : fibonacci() | std::views::take(10)) {
std::cout << i << " "; // 1 1 2 3 5 8 13 21 34 55
}
std::cout << '\n';
// ⽤流对象⽣成⼀个流数据范围
auto words = std::istringstream{ "today is yesterday’s tomorrow" };
for (const auto& s : std::views::istream<std::string>(words))
std::cout << std::quoted(s, '/') << ' ';
std::cout << '\n';
read_file_lines("main.cpp");
return 0;
}
🦋 Range算法
https://en.cppreference.com/w/cpp/algorithm/ranges.html
在C++20之前,STL算法(如 std::sort , std::copy , std::find_if 等)通常接受两个迭代器(begin和end)来定义⼀个序列的范围。从C++20开始,Range算法直接接受⼀个Range对象作为输⼊,⽽不是⼀对迭代器。⼀个Range是任何可以返回begin迭代器和end迭代器的对象,⽐如所有的标准容器( vector , list , map ,或者是前⾯我们给的范围视图等),这些算法在<algorithm> 头⽂件中定义,通常位于 std::ranges 命名空间下。标准库中的算法基本都提供了range版本的对应算法
常⻅的 range 算法:
- ranges::sort-排序
- ranges::find-查找
- ranges::count-统计次数
- ranges::transform-数据转换
- ranges::copy-数据复制保存
- ranges::for_each-它⽤于对容器(或范围)中的每个元素执⾏指定的操作。
- 更多 range 算法,请查看官⽅库⽂档。
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums{ 1, 3, 2, 5, 4 };
// 查找值为3的元素
// stl中的算法
auto it1 = std::find(nums.begin(), nums.end(), 3);
if (it1 != nums.end()) {
std::cout << "Found: " << *it1 << '\n'; // 输出: Found: 3
}
std::sort(nums.begin(), nums.end());
std::for_each(nums.begin(), nums.end(), [](int& x) {x *= 2; });
// C++20中range算法
auto it2 = std::ranges::find(nums, 3);
if (it2 != nums.end()) {
std::cout << "Found: " << *it2 << '\n'; // 输出: Found: 3
}
std::ranges::sort(nums);
std::ranges::for_each(nums, [](int& x) {x *= 2; });
return 0;
}
Range 算法投影
- 在C++20之前,如果你想⽤标准库算法对复杂对象(如结构体、类)进⾏排序或查找,通常需要编写⼀个⾃定义的⽐较函数(如Lambda表达式),在这个函数⾥⼿动指定要⽐较的成员。
- 投影(Projection)是C++20范围库(RangesLibrary)中引⼊的⼀个强⼤特性,算法投影可以简化上述处理过程,简单来说,投影是⼀个可调⽤对象,它能在算法⽐较或处理元素之前,先对元素进⾏⼀次“转换”或“提取”。你可以把它想象成⼀个投影仪:算法真正“看到”的,不是原始数据本⾝,⽽是原始数据通过投影仪投射出来的那个“影像”。算法所有的⽐较、排序等操作,都是基于这个“影像”来进⾏的,但最终作⽤或排序的还是原始数据。
- https://en.cppreference.com/w/cpp/algorithm/ranges/sort.html
- https://en.cppreference.com/w/cpp/algorithm/ranges/max.html
// range: 要处理的输⼊范围
// comparator: ⽐较函数(可选,默认为 std::ranges::less)
// projection: 应⽤于每个元素的投影函数
std::ranges::algorithm(range, comparator, projection);
template< ranges::random_access_range R, class Comp = ranges::less, class Proj = std::identity >
requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
constexpr ranges::borrowed_iterator_t<R>
sort( R&& r, Comp comp = {}, Proj proj = {} );
- 成员变量指针投影
- 成员函数指针投影
- ⾃由函数/函数指针投影
- 函数对象(如lambda)投影
#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>
#include <string>
struct Person {
int getAge() const { return age; }
std::string name;
int age;
};
int getAgeFunc(const Person& p) { return p.age; }
int main()
{
std::vector<Person> people = { {"Alice", 30}, {"Bob", 25}, {"Charlie", 35}
};
// 1. 成员变量指针投影
// 原理:std::invoke(&Person::age, person) 等价于 person.age
const auto& [name1, age1] = std::ranges::max(people, {}, &Person::age);
std::cout << name1 << ":" << age1 << '\n';
// 2. 成员函数指针投影
// 原理:std::invoke(&Person::getAge, person) 等价于 person.getAge()
const auto& [name2, age2] = std::ranges::max(people, {}, &Person::getAge);
std::cout << name2 << ":" << age2 << '\n';
// 3. ⾃由函数/函数指针投影
// 原理:std::invoke(getAge, person) 等价于 getAgeFunc(person)
const auto& [name3, age3] = std::ranges::max(people, {}, getAgeFunc);
std::cout << name3 << ":" << age3 << '\n';
// 4. 函数对象(如lambda)投影
// 原理:std::invoke(lambda, person) 等价于 lambda(person)
const auto& [name4, age4] = std::ranges::max(people, {}, [](const Person& p) { return p.name.size(); });
std::cout << name4 << ":" << age4 << '\n';
// 排序算法的使⽤也可以看到,⽤成员变量指针投影⽅便了很多。
// 年龄降序
std::ranges::sort(people, std::ranges::greater{}, &Person::age);
// 名字升序
std::ranges::sort(people, {}, &Person::name);
return 0;
}
原理:
当使⽤带有投影的算法时,实际执⾏流程如下:原始元素→投影函数→投影后值→⽐较器⽐较以 std::ranges::max_element 为例,其简化实现可能如下:
// 伪代码:max_element 的实现
template<typename It, typename Comp, typename Proj>
It max_element(It first, It last, Comp comp, Proj proj) {
if (first == last) return last;
It largest = first;
++first;
for (; first != last; ++first) {
if (std::invoke(comp, std::invoke(proj, *largest), std::invoke(proj, *first))) {
largest = first;
}
}
return largest;
}
// std::invoke 是投影实现的核⼼,它能统⼀处理各种可调⽤对象:
// 可以调⽤以下任何类型的投影:
// 1. 函数指针
// 2. 成员函数指针
// 3. 成员变量指针
// 4. 函数对象(如lambda)
// auto projected_value = std::invoke(proj, element);
std::invoke的关键作⽤
std::invoke 是C++17引⼊的⼀个核⼼⼯具,它提供了统⼀的调⽤机制,可以以⼀致的⽅式调⽤各种可调⽤对象。这是实现C++20范围算法中投影 (Projection) 功能的基础。
template< class F, class... Args >
constexpr std::invoke_result_t<F, Args...> invoke(F&& f, Args&&... args);
// 当 f 是函数指针或可调⽤对象时:
return std::forward<F>(f)(std::forward<Args>(args)...);
// 当 f 是成员函数指针时(假设 args... 的第⼀个参数是类对象):
return (std::forward<Arg0>(arg0).*f)(std::forward<Args>(args)...);
// 当 f 是成员变量指针时:
return std::forward<Arg0>(arg0).*f;
invoke标准库的可能的⼀种实现(简化版):
namespace detail {
// 处理成员指针的情况
template<class T>
struct is_member_pointer_helper : std::false_type {};
template<class T, class U>
struct is_member_pointer_helper<T U::*> : std::true_type {};
template<class T>
inline constexpr bool is_member_pointer_v =
is_member_pointer_helper<std::decay_t<T>>::value;
// 调⽤实现
template <class F, class... Args>
constexpr auto invoke_impl(F &&f, Args &&...args)
{
if constexpr (is_member_pointer_v<F>)
{
if constexpr (sizeof...(Args) == 0)
{
static_assert(sizeof...(Args) > 0, "member pointer needs an object");
}
else if constexpr (std::is_function_v<std::remove_pointer_t<F>>)
{
// 成员函数指针
return (std::forward<Args>(args).*f...);
}
else
{
// 成员变量指针
return std::forward<Args>(args).*f...;
}
}
else
{
// 普通可调⽤对象
return std::forward<F>(f)(std::forward<Args>(args)...);
}
}
template<class F, class... Args>
constexpr std::invoke_result_t<F, Args...> invoke(F&& f, Args&&... args) {
return detail::invoke_impl(std::forward<F>(f), std::forward<Args>
(args)...);
}
🦋 Range 应⽤的综合样例
#include <algorithm>
#include <vector>
#include <iostream>
#include <unordered_map>
#include <ranges>
struct Employee
{
std::string name;
std::string department;
double salary;
};
void analyze_employees(const std::vector<Employee> &employees)
{
// 1. 按部⻔分组统计平均薪资
std::unordered_map<std::string, std::pair<double, int>> dept_stats;
std::ranges::for_each(employees, [&](const Employee &e) {
auto& [sum, count] = dept_stats[e.department];
sum += e.salary;
++count;
});
auto it = dept_stats.begin();
for (auto &[department, sum_count] : dept_stats)
{
std::cout << department << ":" << sum_count.first / sum_count.second << '\n';
}
std::cout << '\n';
// 2. 找出薪资最⾼的员⼯
const auto &[name, department, salary] = std::ranges::max(employees, {}, &Employee::salary);
std::cout << name << ":" << department << ":" << salary << "\n\n";
// 3. 筛选技术部员⼯并加薪10%
auto eng_employees = employees
| std::views::filter([](const Employee &e) {
return e.department == "Engineering"; })
| std::views::transform([](Employee e) {
e.salary *= 1.1; return e; });
for (const auto &e : eng_employees)
{
std::cout << e.name << '\n';
std::cout << e.department << '\n';
std::cout << e.salary << '\n';
}
std::cout << '\n';
}
int main()
{
std::vector<Employee> v = {
{"张三", "Engineering", 10000},
{"李四", "saler", 8000},
{"王五", "Engineering", 15000},
{"赵六", "saler", 9000},
};
analyze_employees(v);
return 0;
}
六:🔥 共勉
😋 以上就是我对 【C++20】特性解析 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
更多推荐


所有评论(0)