写在前面
C++20 是 C++ 语言发展史上一个里程碑式的版本。如果说 C++11 让这门语言焕然一新,那么 C++20 则是一次更加深远的能力扩展。在这个版本中,有两个特性尤为引人注目,它们分别是概念(Concept) 和协程(Coroutine)。
概念是 C++ 模板编程领域几十年来最重大的改进。在 C++20 之前,模板参数的约束只能依靠复杂的 SFINAE 技巧和一堆难以阅读的 std::enable_if 来"曲线救国",而模板实例化失败时抛出的错误信息动辄几百行,让人看了头皮发麻 c++概念。概念的引入,从根本上改变了这一切。
协程则是 C++ 异步编程领域的一次革命。在 C++20 之前,编写异步代码要么依赖回调函数(导致"回调地狱"),要么依赖线程池(上下文切换开销大)。协程提供了一种全新的思路------让函数可以暂停执行,在之后的某个时刻再恢复运行,而且这个暂停和恢复的开销比线程上下文切换要低得多 c++协程。
那么在本篇博客中,我们就要来学习C++20中推出的概念以及协程。
第一章 概念与约束:模板编程的进阶之路
1.1 为什么需要概念
在 C++20 之前,模板的约束能力源自 SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)。这个机制的核心思想是:当模板参数替换导致某个表达式无效时,编译器不会报错,而是静默地排除这个候选函数,继续寻找其他匹配的版本。
听起来很强大,但实际写起来却非常痛苦。我们来看一个典型的例子:
cpp
// C++17 及之前:使用 SFINAE 约束模板参数
// 下面这段代码的作用是:只允许整型参数调用 foo 函数
// 如果传入的是浮点类型或字符串类型,则编译失败
template<typename T,
// enable_if 是 SFINAE 的核心工具
// std::is_integral_v<T> 检查 T 是否为整型
// 如果 T 是整型,enable_if_t 就是 void,否则替换失败(但不算错误)
typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {
std::cout << "整型参数: " << x << std::endl;
}
// 如果传入一个浮点数,编译器会报出几百行的模板实例化错误信息
// 错误信息中充斥着"substitution failure"、"enable_if"等词汇
// 普通开发者根本看不懂到底错在哪里
foo(3.14); // 编译错误:几百行难以理解的错误信息
这段代码能工作,但它有几个致命的问题。第一,语法极其晦涩,typename = std::enable_if_t<...> 这种写法对于不熟悉 SFINAE 的人来说简直是天书。第二,错误信息糟糕透顶,编译器不会告诉你"这个函数要求整型参数,但你传了浮点数",而是会展开一堆模板实例化的内部细节。第三,当需要组合多个约束条件时,代码会变得非常冗长和难以维护。
C++20 的概念就是为了解决这些问题而生的。它用一种直观、声明式的语法,让模板参数的约束变得像写普通函数参数类型一样自然。
1.2 概念的基本定义
概念(Concept)是 C++20 引入的一种编译时谓词(compile-time predicate)。所谓"谓词",通俗地说就是一个返回布尔值的表达式。概念本质上就是一组约束条件的集合,它规定了模板参数必须满足的条件。
定义一个概念使用 concept 关键字,其基本语法如下:
cpp
// 概念定义的基本语法模板
// template-parameter-list:模板参数列表,可以是一个或多个类型参数,其实就是我们之前使用模版的方式啦
// concept-name:概念的名称,遵循标识符命名规则,这个是可以我们自己自由取的,但是还是要易懂
// constraint-expression:约束表达式,必须是一个可以求值为 bool 的常量表达式
template <template-parameter-list>
concept concept-name = constraint-expression;
这个语法乍一看有点像变量定义,但它是模板级别的。等号右边是一个编译期可以求值的布尔表达式,这个表达式可以是各种约束的组合。
我们来看几个具体的概念定义例子:
cpp
#include <concepts> // 标准库概念头文件
#include <iostream>
#include <type_traits> // 类型萃取(type traits)工具库
// 方式一:使用标准库类型萃取
// 概念 Integral 表示:类型 T 必须是整型
// std::is_integral_v<T> 是 C++17 引入的编译期布尔常量
// 当 T 是 int、char、short、long、long long 等整型时,值为 true
template<typename T>
concept Integral = std::is_integral_v<T>;
// 方式二:使用简单的常量表达式
// 概念 SmallType 表示:类型 T 的大小(sizeof)不超过 4 个字节
// 这意味着 T 可以是 int、float、char 等小型类型
// 但 double(8字节)或 std::string 就不满足
template<typename T>
concept SmallType = sizeof(T) <= 4;
// 方式三:使用 requires 子句
// 概念 Incrementable 表示:类型 T 必须支持自增操作
// requires(T t) 是一个 requires 表达式,用于声明对类型 T 的要求
// 大括号内是要求序列,每条要求都必须满足
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀自增 ++t 是否合法
t++; // 检查后缀自增 t++ 是否合法
};
参数解析:
template<typename T>:这里的T是一个类型模板参数,概念定义时使用。当概念被应用时,使用者传入的实际类型会被绑定到T上。concept Integral:Integral是概念的名字,之后可以在模板声明中直接使用它。= std::is_integral_v<T>:赋值号右边的表达式是约束表达式(constraint-expression)。这个表达式在编译期被求值,其结果必须是bool类型。true表示满足约束,false表示不满足。requires(T t):这是 requires 表达式(注意不是 requires 子句),它接受一个参数列表,大括号内是要求序列。T t表示声明一个类型为T的参数t,但注意------这里并不会真的创建对象,它只是在编译期进行检查。
使用返回值说明:
概念本身是一个编译期常量表达式,它的"返回值"就是 bool。当概念被用作模板约束时,编译器会根据这个 bool 值来决定是否启用该模板。true 表示启用,false 表示禁用(并产生编译错误)。
1.3 标准库中预定义的概念
C++20 标准库在 <concepts> 头文件中提供了大量预定义的概念,覆盖了绝大多数常见的类型约束场景,也就是我们可以直接使用已经定义好的概念来大大减少我们重复造轮子 。这些概念分为几个大的类别,我们来逐一了解。
核心语言概念:
cpp
#include <concepts>
// same_as:检查两个类型是否完全相同
// 用法:std::same_as<T, U>
// 返回:如果 T 和 U 是完全相同的类型,则满足约束
// 举例:std::same_as<int, int> → true
// std::same_as<int, const int> → false(因为 const 是类型修饰符)
// derived_from:检查一个类型是否派生自另一个类型
// 用法:std::derived_from<Derived, Base>
// 返回:如果 Derived 是 Base 的派生类,则满足约束
// 注意:private 继承和 protected 继承也会被检测到
// convertible_to:检查一个类型是否可以转换为另一个类型
// 用法:std::convertible_to<From, To>
// 返回:如果 From 可以隐式转换为 To,则满足约束
// 举例:std::convertible_to<int, double> → true
// std::convertible_to<std::string, int> → false
// common_reference_with:检查两个类型是否有公共引用类型
// common_with:检查两个类型是否有公共类型
数值概念:
cpp
// integral:检查是否为整型(int, char, short, long, long long 等)
// signed_integral:检查是否为有符号整型
// unsigned_integral:检查是否为无符号整型
// floating_point:检查是否为浮点型(float, double, long double)
可比较概念:
cpp
// equality_comparable:检查类型是否支持 == 和 != 比较
// totally_ordered:检查类型是否支持 <, >, <=, >= 全套比较
// 这些概念确保了类型之间的比较操作是合法的并且语义一致
可调用概念:
cpp
// invocable:检查一个可调用对象是否可以被调用
// regular_invocable:更强的可调用约束(保证可调用且不修改自身状态)
// predicate:检查一个可调用对象是否返回 bool 值
需要注意的是我们要访问这些都是需要访问std命名空间域的哦,我们可以直接将它们当作类型进行使用,比如std::integral。
用法示例:
cpp
#include <concepts>
#include <iostream>
#include <string>
// 定义一个函数,只接受整型参数
// 这里的 std::integral 是标准库概念
// 它等价于我们前面定义的 Integral 概念
void process_int(std::integral auto x) {
std::cout << "处理整型: " << x << std::endl;
}
// 定义一个函数,只接受浮点型参数
void process_float(std::floating_point auto x) {
std::cout << "处理浮点型: " << x << std::endl;
}
// 定义一个函数,要求两个类型是可转换的
template<typename From, typename To>
requires std::convertible_to<From, To>
void convert_and_print(From value) {
To result = value; // 这种转换是安全的
std::cout << "转换结果: " << result << std::endl;
}
参数解析:
std::integral auto x:这里的std::integral是一个概念,auto是 C++20 的简写模板语法。整个表达式表示x的类型必须满足std::integral概念,即必须是整型。这种写法称为"约束占位符"(constrained placeholder)。std::convertible_to<From, To>:这是一个二元概念,接受两个类型参数。From必须可以隐式转换为To。requires std::convertible_to<From, To>:这里是 requires 子句(requires clause),用于在函数模板上施加约束。
这里还需要我们注意的是,我们也可以自己自定义概念然后用于后面模版的使用,示例如下:
cpp
// 定义一个整型概念
template<typename T>
concept Integral = std::is_integral_v<T>;
// 场景一:概念约束 vs 无约束
// 两个同名的函数模板 f,一个有约束,一个没有约束
// 受约束的版本:只接受整型参数
// 使用我们上面定义好的整型概念。
template<Integral T>
void f(T x) {
std::cout << "有 concepts 约束" << std::endl;
}
// 无约束的版本:接受任何类型参数
template<typename T>
void f(T x) {
std::cout << "无 concepts 约束(通用版本)" << std::endl;
}
1.4 约束的使用方式
C++20 提供了五种不同的方式来使用概念和约束,每种方式适用于不同的场景。我们来逐一解析。
方式一:模板参数后直接使用
这是最直观、最简洁的方式,概念名直接替代 typename 关键字:
cpp
#include <iostream>
#include <concepts>
// 定义一个概念,要求 T 是整型
template<typename T>
concept Integral = std::is_integral_v<T>;
// 方式一:模板参数后直接使用概念
// 这里的 Integral T 等价于 "typename T,要求 T 满足 Integral 概念"
// 这种写法最简洁,语义也最清晰
template<Integral T>
void f1(T x) {
std::cout << "有 concepts 约束,值为: " << x << std::endl;
}
参数解析:
template<Integral T>:这里的Integral是概念名,T是模板参数名。这行代码等价于template<typename T> requires Integral<T>。编译器会检查模板参数T是否满足Integral概念。- 这种写法最大的优点是简洁,一眼就能看出模板参数需要满足什么条件。
方式二:requires 子句
这种方式更加灵活,可以组合多个概念:
cpp
#include <iostream>
#include <concepts>
template<typename T>
concept Integral = std::is_integral_v<T>;
template<typename T>
concept Float = std::is_floating_point_v<T>;
// 方式二:requires 子句
// 这里的 requires 关键字后面跟一个布尔常量表达式
// 可以使用 ||(逻辑或)来组合多个概念
// 表示 T 必须是整型或者浮点型
template<typename T>
requires Integral<T> || Float<T>
void f2(T x) {
std::cout << "数值类型,值为: " << x << std::endl;
}
参数解析:
requires Integral<T> || Float<T>:这是 requires 子句(requires clause),它出现在函数模板的声明中。requires关键字后面跟一个约束表达式,这个表达式必须是bool类型。||运算符用于连接多个原子约束,形成"析取"(disjunction)。只要其中一个约束为true,整个表达式就为true。- 这种方式的优势在于可以灵活组合多个约束条件。
方式三:尾置 requires 子句
这种方式将约束放在函数声明的最末尾,适用于约束依赖于函数参数类型的情况:
cpp
// 方式三:尾置 requires 子句
// 将约束放在函数声明的末尾
// 这种方式在约束依赖于多个模板参数时特别有用
template<typename T>
void f3(T x) requires Integral<T> || Float<T> {
std::cout << "尾置约束,值为: " << x << std::endl;
}
参数解析:
void f3(T x) requires Integral<T> || Float<T>:这里的requires子句放在函数参数列表和返回值之后。当约束表达式需要引用函数参数时,这种方式非常有用。- 尾置约束的优势在于,它可以让函数的声明和定义更加清晰,特别是在约束条件依赖于多个参数时。
方式四:临时约束(requires requires)
当需要临时定义一个约束,而不想创建一个独立的概念时,可以使用双重 requires:
cpp
// 方式四:临时约束 - 使用 requires requires
// 第一个 requires 是 requires 子句的关键字
// 第二个 requires 是 requires 表达式(定义一个内联的约束)
// 大括号内是要求序列
template<typename T>
requires requires(T x) {
x.size(); // 要求 T 支持 size() 方法
x + x; // 要求 T 支持加法运算
}
void f4(T x) {
std::cout << "临时约束,值为: " << x << std::endl;
}
参数解析:
requires requires(T x) { ... }:第一个requires引入 requires 子句,第二个requires引入 requires 表达式。(T x)是 requires 表达式的参数列表,声明一个类型为T的参数x。- 大括号内的
x.size();和x + x;是"简单要求"(simple requirement),只检查表达式是否合法(能否编译通过),不关心返回值类型。 - 这种方式的优势是"即用即写",不需要预先定义概念,适合简单的、一次性使用的约束。
方式五:auto 占位符约束
C++20 允许在函数参数中使用 auto 作为占位符,结合概念使用可以简化模板声明:
cpp
// 方式五:auto 占位符约束
// Integral auto 表示:参数 x 的类型必须是整型,并且使用 auto 推导
// 这种方式实际上定义了一个"约束的模板函数"
// 每个使用约束 auto 的参数都相当于一个独立的模板参数
void f5(Integral auto x) {
std::cout << "auto 约束,值为: " << x << std::endl;
}
参数解析:
Integral auto x:这里的Integral是概念,auto是类型占位符。编译器会推导出x的实际类型,并检查它是否满足Integral概念。Integral auto等价于template<Integral T> void f5(T x)。- 当有多个参数都使用
约束 auto时,每个参数可以是不同的类型,只要各自满足约束即可。
完整示例代码:
cpp
#include <iostream>
#include <concepts>
#include <string>
#include <vector>
// 定义一个整型概念
template<typename T>
concept Integral = std::is_integral_v<T>;
// 定义一个浮点型概念
template<typename T>
concept Float = std::is_floating_point_v<T>;
int main() {
// 方式一:模板参数后直接使用概念
f1(42); // 正确:int 满足 Integral
// f1("hello"); // 错误:const char* 不满足 Integral
// 方式二/三:requires 子句和尾置 requires
f2(42); // 正确:int 满足 Integral
f2(3.14); // 正确:double 满足 Float
// f2("hello"); // 错误:const char* 既不满足 Integral 也不满足 Float
// 方式四:临时约束
std::string s = "hello";
f4(s); // 正确:string 支持 size() 和 +
// f4(42); // 错误:int 不支持 size()
// 方式五:auto 占位符约束
f5(42); // 正确:int 满足 Integral
// f5(3.14); // 错误:double 不满足 Integral
return 0;
}
1.5 requires 表达式的四种形式
requires 表达式是概念定义中最核心、最强大的部分。它允许我们声明对类型的一系列要求,这些要求在编译期被检查。requires 表达式有四种形式,每种形式解决不同的问题 。
基本语法:
cpp
// requires 表达式的基本语法
// parameter-list: 可选参数列表,声明供检查使用的参数
// requirement-seq: 要求序列,由多个要求组成
template<typename T>
concept ConceptName = requires (parameter-list) {
requirement-seq // 要求序列
};
第一种:简单要求(Simple Requirement)
简单要求是最基础的形式,它只检查一个表达式是否合法,即能否通过编译。不关心表达式的返回值类型,也不关心是否有异常抛出。
cpp
#include <concepts>
#include <iostream>
// 概念:可相加的类型
// 使用简单要求检查 T 是否支持加法运算
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 简单要求:检查 a + b 是否合法(能否编译通过)
// 不关心 + 返回什么类型,只关心 + 运算符是否存在
};
// 概念:支持前置和后置自增的类型
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀自增
t++; // 检查后缀自增
--t; // 检查前缀自减
t--; // 检查后缀自减
};
参数解析:
requires(T a, T b):声明了两个参数a和b,类型都是T。注意,这里不会真的创建对象,只是在编译期"假装"有这些参数。a + b;:这是一个简单要求,它检查表达式a + b在语法上是否合法。如果T没有重载+运算符,或者+运算符不可访问,则要求不满足。
适用场景:
简单要求适用于只需要检查"这个操作是否存在"的场景,比如检查一个类型是否支持加法、自增、下标访问等基本操作。
第二种:类型要求(Type Requirement)
类型要求检查某个嵌套类型是否存在。它使用 typename 关键字来声明一个类型要求。
cpp
#include <concepts>
#include <vector>
#include <string>
// 概念:具有迭代器类型的容器
// 检查类型 T 是否定义了 iterator 和 value_type 嵌套类型
template<typename T>
concept HasValueType = requires {
typename T::iterator; // 类型要求:检查 T 是否有 iterator 嵌套类型
typename T::value_type; // 类型要求:检查 T 是否有 value_type 嵌套类型
typename T::const_iterator; // 检查 T 是否有 const_iterator 嵌套类型
};
// 概念:可迭代的类型
template<typename T>
concept Iterable = requires(T t) {
typename T::iterator; // 必须有 iterator 类型
typename T::value_type; // 必须有 value_type 类型
{ t.begin() } -> std::same_as<typename T::iterator>;
// begin() 必须返回 iterator 类型
{ t.end() } -> std::same_as<typename T::iterator>;
// end() 必须返回 iterator 类型
};
参数解析:
typename T::iterator:这是一个类型要求,它检查T::iterator是否是一个有效的类型。如果T没有定义iterator这个嵌套类型,则要求不满足。- 类型要求以
typename关键字开头,后面跟一个类型名称。这个名称可以是嵌套类型,也可以是一个模板实例化结果。
适用场景:
类型要求适用于检查类型是否具有特定的嵌套类型(如 iterator、value_type、size_type 等),这在容器类型和迭代器类型中非常常见。
第三种:复合要求(Compound Requirement)
复合要求是最强大的形式,它不仅检查表达式是否合法,还可以指定返回类型和异常规范。复合要求的完整语法比较丰富:
cpp
// 复合要求的完整语法
// { 表达式 } noexcept(可选) -> 类型约束;
//
// 各部分含义:
// { 表达式 } - 要检查的表达式,用大括号括起来
// noexcept - 可选,要求表达式不抛出异常
// -> 类型约束 - 可选,要求表达式的返回值满足某个概念
#include <concepts>
#include <iostream>
#include <string>
// 概念:可转换相加的类型
// 要求 a + b 的结果可以转换为 T 类型
// 要求 a += b 不抛异常且返回 T& 类型
template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
// 复合要求:检查 a + b 的结果是否可以转换为 T 类型
{ a + b } -> std::convertible_to<T>;
// 这里 { a + b } 是表达式,std::convertible_to<T> 是返回类型约束
// 整个要求等价于:a + b 合法,且结果可以隐式转换为 T
// 复合要求:检查 a += b 不抛异常且返回 T& 类型
{ a += b } noexcept -> std::same_as<T&>;
// noexcept 关键字要求 a += b 不会抛出异常
// std::same_as<T&> 要求返回值类型必须是 T&(完全相同)
};
参数解析:
{ a + b }:大括号括起来的表达式,这是复合要求要检查的表达式。-> std::convertible_to<T>:->后面的部分是返回类型约束。std::convertible_to<T>是一个概念,它检查表达式的结果是否可以隐式转换为T类型。noexcept:可选关键字,如果出现,则要求表达式不抛出任何异常。std::same_as<T&>:std::same_as是一个概念,它要求两个类型完全相同。这里要求a += b的返回值类型必须是T&(注意是引用类型)。
返回值说明:
复合要求的"返回值"实际上是通过 -> 后面的类型约束来间接指定的。编译器会检查表达式的结果类型是否满足指定的概念。如果满足,则复合要求通过;否则不通过。
适用场景:
复合要求适用于需要精确控制返回类型的场景,比如要求 begin() 返回迭代器类型、要求 size() 返回可转换为 size_t 的类型等。
第四种:嵌套要求(Nested Requirement)
嵌套要求允许在 requires 表达式内部再使用 requires 关键字,进行更复杂的约束检查。
cpp
#include <concepts>
#include <iostream>
#include <type_traits>
// 概念:复杂概念
// 使用嵌套要求组合多个条件
template<typename T>
concept ComplexConcept = requires(T t) {
// 简单要求:检查 T 是否支持自增
++t;
// 嵌套要求:使用 requires 关键字进行更复杂的约束
// 要求 sizeof(T) 必须小于等于 long 类型的大小
requires sizeof(T) <= sizeof(long);
// 嵌套要求:要求 T 必须是类类型(class 或 struct)
requires std::is_class_v<T>;
// 嵌套要求:要求 T 不是指针类型
requires !std::is_pointer_v<T>;
};
参数解析:
requires sizeof(T) <= sizeof(long):这是嵌套要求,它以requires关键字开头,后面跟一个布尔常量表达式。这个表达式在编译期被求值,如果为false,则整个概念不满足。- 嵌套要求与普通简单要求的区别在于:嵌套要求使用
requires关键字引导,后面跟的是布尔表达式,而不是表达式语句。换句话说,嵌套要求可以检查"条件的值",而不仅仅是"条件是否合法"。
适用场景:
嵌套要求适用于需要检查编译期常量的场景,比如类型的大小、类型的属性(是否是类、是否是枚举等)、类型的对齐要求等。
完整示例代码:
cpp
#include <concepts>
#include <iostream>
#include <vector>
#include <string>
#include <list>
// 定义四种 requires 表达式形式的示例概念
// 1. 简单要求:检查是否支持加法
template<typename T>
concept Addable = requires(T a, T b) {
a + b;
};
// 2. 类型要求:检查是否具有嵌套类型
template<typename T>
concept HasValueType = requires {
typename T::iterator;
typename T::value_type;
};
// 3. 复合要求:检查返回类型
template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a += b } noexcept -> std::same_as<T&>;
};
// 4. 嵌套要求:组合复杂条件
template<typename T>
concept ComplexConcept = requires(T t) {
requires sizeof(T) <= sizeof(long);
requires std::is_class_v<T>;
};
// 使用概念的模板函数
template<Addable T>
void func1(const T& x1, const T& x2) {
// 这里的 T 保证支持 + 运算
T ret = x1 + x2;
std::cout << "相加结果: " << ret << std::endl;
}
template<HasValueType T>
void func2(const T& x) {
// 这里的 T 保证有 iterator 和 value_type 嵌套类型
std::cout << "容器类型,元素个数: " << x.size() << std::endl;
}
template<ConvertibleAddable T>
void func3(T& x1, const T& x2) {
// 这里的 T 保证 + 的结果可转换为 T
// 且 += 不抛异常,返回 T&
T ret = x1 + x2;
std::cout << "相加结果: " << ret << std::endl;
x1 += x2;
std::cout << "自加后: " << x1 << std::endl;
}
template<ComplexConcept T>
void func4(const T& x) {
std::cout << "复杂约束满足,类型大小: " << sizeof(T) << std::endl;
}
int main() {
// 测试简单要求
func1(1, 2); // 正确:int 支持加法
func1(std::string("a"), std::string("b")); // 正确:string 支持加法
// 测试类型要求
func2(std::vector<int>{1, 2, 3}); // 正确:vector 有 iterator 和 value_type
// func2(42); // 错误:int 没有 iterator 或 value_type
// 测试复合要求
std::string s1("Hello");
func3(s1, std::string(" World")); // 正确:string 满足复合要求
int i = 10;
func3(i, 20); // 正确:int 也满足复合要求
// 测试嵌套要求
// func4(42); // 错误:int 不是类类型
func4(std::vector<int>{1, 2, 3}); // 正确:vector 是类类型且大小合适
func4(std::less<int>()); // 正确:less 是类类型
return 0;
}
1.6 约束的几种类型
在 C++20 中,约束可以通过不同的逻辑运算符组合起来,形成更复杂的约束表达式。约束的组合方式主要有以下几种:
原子约束(Atomic Constraint):
原子约束是最基本的约束单位,不可再分。它可以是:
- 一个概念名,如
std::integral<T> - 一个 requires 表达式
- 一个常量布尔表达式,如
sizeof(T) <= 4
cpp
// 原子约束示例
template<typename T>
concept Integral = std::is_integral_v<T>; // 整个定义就是一个原子约束
template<typename T>
concept SmallType = requires(T t) { // requires 表达式也是原子约束
requires sizeof(T) <= 4;
};
合取(Conjunction):
合取使用 && 运算符连接多个原子约束。所有条件都必须满足,整个合取约束才为 true。
cpp
#include <concepts>
#include <iostream>
// 合取约束:使用 && 连接多个原子约束
// 表示 T 必须同时满足三个条件:
// 1. 是整型(std::integral<T>)
// 2. 支持流输出(requires 表达式)
// 3. 大小不超过 4 字节
template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
std::cout << t; // 检查 T 是否支持流输出
};
解析:
std::integral<T> && requires(T t) { std::cout << t; }:&&连接了两个原子约束。只有当T既是整型,又支持流输出时,IntegralAndPrintable才为true。- 合取约束的求值顺序是短路求值(short-circuit evaluation):如果第一个约束不满足,不会检查第二个约束。
析取(Disjunction):
析取使用 || 运算符连接多个原子约束。只要有一个条件满足,整个析取约束就为 true。
cpp
// 析取约束:使用 || 连接多个原子约束
// 表示 T 只要满足其中任意一个条件即可:
// 1. 是整型,或者
// 2. 是浮点型
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
逻辑非(Negation):
使用 ! 运算符对约束取反:
cpp
// 逻辑非:使用 ! 对约束取反
// 表示 T 不能是指针类型
template<typename T>
concept NonPointer = !std::is_pointer_v<T>;
折叠扩展约束(Fold Expansion Constraint):
这是 C++26 开始支持的特性,用于对可变参数模板的包展开进行约束:
cpp
// 折叠扩展约束:对可变参数包进行约束
// C++26 开始支持
#include <concepts>
#include <iostream>
#include <memory>
#include <string>
// 基础概念:可移动构造
template<typename T>
concept A = std::is_move_constructible_v<T>;
// 基础概念:可拷贝构造
template<typename T>
concept B = std::is_copy_constructible_v<T>;
// 组合概念:可移动且可拷贝
template<typename T>
concept C = A<T> && B<T>;
// 折叠扩展约束:约束可变参数包中的所有类型
// 要求所有参数类型都满足 C 概念
// (C<T> && ...) 是折叠表达式,展开后为 C<T1> && C<T2> && C<T3> && ...
template<typename... T>
requires (C<T> && ...)
void g(T... args) {
std::cout << "所有参数都满足 C 概念" << std::endl;
}
int main() {
// g(std::unique_ptr<int>()); // 错误:unique_ptr 不可拷贝,不满足 C
g(1.1); // 正确:double 可移动且可拷贝
g(1, 2, std::string("hello")); // 正确:int 和 string 都满足 C
return 0;
}
参数解析:
(C<T> && ...):这是折叠表达式(fold expression),使用&&运算符对参数包进行展开。如果传入g(1, 2, "hello"),则展开为C<int> && C<int> && C<std::string>。只有当所有条件都满足时,整个约束才为true。template<typename... T>:typename...是可变参数模板(variadic template),可以接受任意数量的模板参数。
1.7 约束的偏序规则
约束的偏序规则(Partial Ordering of Constraints)是 C++20 概念系统中一个非常重要的机制 。它用于在函数重载或模板特化时,判断哪个约束的模板"更特化"或"更受约束"。
核心思想:
偏序规则的核心是:如果约束 A 蕴含(包含)约束 B,那么 A 比 B "更受约束"。这意味着任何满足 A 的类型也一定会满足 B,但满足 B 的类型不一定满足 A。用数学符号表示就是:A ⇒ B(A 蕴含 B)。
当两个模板都匹配时,编译器会选择"更受约束"的那一个。如果两个约束互不蕴含,则会产生二义性错误。
cpp
#include <iostream>
#include <concepts>
#include <memory>
// 定义一个整型概念
template<typename T>
concept Integral = std::is_integral_v<T>;
// 场景一:概念约束 vs 无约束
// 两个同名的函数模板 f,一个有约束,一个没有约束
// 受约束的版本:只接受整型参数
template<Integral T>
void f(T x) {
std::cout << "有 concepts 约束" << std::endl;
}
// 无约束的版本:接受任何类型参数
template<typename T>
void f(T x) {
std::cout << "无 concepts 约束(通用版本)" << std::endl;
}
// 场景二:多个概念之间的偏序关系
// A 概念:可移动构造
template<typename T>
concept A = std::is_move_constructible_v<T>;
// B 概念:可拷贝构造(注意这只是一个概念名,不代表拷贝是 A 的子集)
template<typename T>
concept B = std::is_copy_constructible_v<T>;
// C 概念:同时满足 A 和 B,即 A 和 B 的合取
// C 比 A 和 B 都"更受约束"
// 因为 C 蕴含 A(C ⇒ A),C 也蕴含 B(C ⇒ B)
template<typename T>
concept C = A<T> && B<T>;
// 受 A 约束的版本
template<typename T>
requires A<T>
void g(T x) {
std::cout << "g()->A" << std::endl;
}
// 受 C 约束的版本(C 比 A 更受约束)
template<typename T>
requires C<T>
void g(T x) {
std::cout << "g()->C" << std::endl;
}
int main() {
// 场景一测试
f(1); // 输出:有 concepts 约束(int 满足 Integral)
f("xxx"); // 输出:无 concepts 约束(const char* 不满足 Integral,但可以匹配通用版本)
// 场景二测试
double d = 3.14;
g(d); // 输出:g()->C(double 同时满足 A 和 B,C 更受约束,优先匹配)
// unique_ptr 只可移动不可拷贝,所以只满足 A 不满足 C
g(std::unique_ptr<int>()); // 输出:g()->A(unique_ptr 只满足 A)
return 0;
}
偏序规则的关键点:
-
约束蕴含(Subsumption):编译器通过约束的"范式"(normal form)来判断蕴含关系。约束的范式通过将合取展开为"合取子句"(conjunctive clauses)、将析取展开为"析取子句"(disjunctive clauses)来得到。
-
选择规则:当两个模板都匹配时:
- 如果 A 的约束蕴含 B 的约束,则选择 A
- 如果 B 的约束蕴含 A 的约束,则选择 B
- 如果互不蕴含,则产生二义性错误
- 注意事项:蕴含关系是基于语法结构而非语义的。例如,两个在逻辑上等价但语法结构不同的约束,不会被编译器视为蕴含关系。这一点非常重要,也是实践中容易踩坑的地方。
1.8 综合实践样例
理解了概念的基本原理后,我们来看几个实际的工程应用场景,看看概念在真实项目中是如何发挥作用。
场景一:使用概念约束哈希表的 Key 类型
在实现哈希表时,Key 类型必须支持哈希计算。在 C++20 之前,我们在模板中无法明确表达这个要求,只能通过文档说明或者 SFINAE 技巧来约束。现在,概念可以清晰地表达这个要求:
cpp
#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
#include <unordered_map>
#include <iostream>
// 概念:可哈希类型
// 对于类型 T,要求 std::hash<T>{}(a) 可以编译通过
// 并且其结果可以转换为 std::size_t
template<typename T>
concept HashType = requires(T a) {
// 复合要求:检查 std::hash<T>{}(a) 是否合法
// 并且结果必须可转换为 std::size_t
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
// 使用概念约束的哈希表类模板
// K 必须满足 HashType 概念
// HashFunc 是哈希函数对象,默认使用 std::hash<K>
template<HashType K, typename V, class HashFunc = std::hash<K>>
class hashmap {
public:
// 插入键值对
bool insert(const K& key, const V& value) {
// 计算哈希值
// HashFunc() 创建一个哈希函数对象
// (key) 调用该函数对象,计算 key 的哈希值
size_t hashi = HashFunc()(key);
// 实际插入逻辑省略...
std::cout << "插入键值对,哈希值: " << hashi << std::endl;
return true;
}
// 查找键对应的值
V* find(const K& key) {
size_t hashi = HashFunc()(key);
// 查找逻辑省略...
return nullptr;
}
};
int main() {
// 正确:int 和 std::string 都支持 std::hash
hashmap<int, std::string> hm1;
hashmap<std::string, int> hm2;
// 下面这行代码如果取消注释会编译失败
// 因为 std::pair 没有提供 std::hash 特化
// 所以 std::pair<std::string, int> 不满足 HashType 概念
// hashmap<std::pair<std::string, int>, std::string> hm3;
hm1.insert(42, "forty-two");
hm2.insert("hello", 1);
return 0;
}
参数解析:
template<HashType K, class HashFunc = std::hash<K>>:HashType K表示模板参数K必须满足HashType概念。HashFunc默认使用std::hash<K>,这是一个函数对象类型。{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>:这是一个复合要求,检查std::hash<T>{}(a)表达式的结果是否可以转换为std::size_t。
场景二:多种复杂条件组合和嵌套
在实际项目中,我们经常需要组合多个条件来约束类型。下面这个例子展示了如何定义"可打印的范围"概念:
cpp
#include <concepts>
#include <iostream>
#include <vector>
#include <mutex>
#include <list>
// 概念:可打印的范围(容器)
// 要求 T 必须满足以下条件:
// 1. t.begin() 返回 T::iterator 类型
// 2. t.end() 返回 T::iterator 类型
// 3. T::value_type 必须是整型
// 4. T::value_type 类型的值必须支持流输出
template<typename T>
concept PrintableRange = requires(T t) {
// 复合要求:begin() 必须返回 T::iterator 类型
{ t.begin() } -> std::same_as<typename T::iterator>;
// 复合要求:end() 必须返回 T::iterator 类型
{ t.end() } -> std::same_as<typename T::iterator>;
// 嵌套要求:确定 T 的元素类型是整型
requires std::integral<typename T::value_type>;
// 嵌套要求:元素类型必须支持流输出
// 这里使用 requires 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, 4, 5 };
print_range(v); // 正确:vector<int> 满足 PrintableRange
// std::vector<std::mutex> vm; // 错误:std::mutex 不是整型,不满足
// print_range(vm); // 编译错误:不满足 PrintableRange 概念
std::list<long> lst{ 10, 20, 30 };
print_range(lst); // 正确:list<long> 也满足条件
return 0;
}
参数解析:
requires(typename T::value_type e) { std::cout << e; }:这是一个内嵌的 requires 表达式,它声明了一个T::value_type类型的参数e,然后检查std::cout << e是否合法。requires std::integral<typename T::value_type>:这是一个嵌套要求,它检查T::value_type是否满足std::integral概念。
返回值说明:
print_range 函数本身没有返回值(void),但概念 PrintableRange 的"求值结果"是一个编译期 bool 值。当模板参数不满足概念时,编译器会给出清晰的错误信息,指出具体是哪个要求不满足。
场景三:使用概念解决模板匹配问题
在 C++ 中,当我们自定义容器时,可能会遇到构造函数模板与非模板函数之间的二义性问题。概念可以帮助我们精确控制模板的匹配条件:
cpp
#include <concepts>
#include <iostream>
#include <vector>
// 概念:迭代器类型
// 要求类型 T 支持前置自增和解引用
template<typename T>
concept IteratorType = requires(T it) {
++it; // 前置自增
*it; // 解引用
};
// 自定义的命名空间
namespace bit {
// 自定义的 vector 容器
template<typename T>
class vector {
public:
typedef T* iterator; // 迭代器类型
typedef T value_type; // 元素类型
// 强制编译器生成默认构造函数
vector() = default;
// 构造函数模板:接受任意类型的迭代器范围
// 这段代码在没有概念约束时,会与下面的 vector(size_t, const T&) 构造函数
// 产生二义性,因为调用 vector(10, 1) 时,10 既可以作为迭代器(int 类型)
// 也可以作为 size_t 类型
// 使用 IteratorType 概念约束
// 只有真正的迭代器类型才能匹配这个构造函数模板
template<IteratorType InputIterator>
vector(InputIterator first, InputIterator last) {
std::cout << "使用迭代器范围构造" << std::endl;
while (first != last) {
push_back(*first);
++first;
}
}
// 构造函数:n 个值为 val 的元素
vector(size_t n, const T& val = T()) {
std::cout << "使用 n 个相同值构造" << std::endl;
}
// 辅助函数:向容器中添加元素
void push_back(const T& x) {}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
}
int main() {
// 在 C++20 之前,下面这行代码会产生二义性错误
// 因为 10 是 int 类型,既可以作为迭代器(匹配模板构造函数)
// 也可以作为 size_t(匹配非模板构造函数)
// 但在 C++20 中,int 不满足 IteratorType 概念
// 所以模板构造函数被排除,正确地匹配了非模板版本
bit::vector<int> v7(10, 1); // 正确:匹配非模板版本
bit::vector<size_t> v9(10, 1); // 正确:匹配非模板版本
std::cout << "构造成功!" << std::endl;
return 0;
}
关键点解析:
template<IteratorType InputIterator>:IteratorType概念确保只有真正的迭代器类型才能匹配这个构造函数模板。int类型不满足IteratorType(因为int不支持++和*操作),所以vector(10, 1)不会匹配模板版本,而是找到非模板版本。- 在 C++20 之前,解决这个问题需要复杂的 SFINAE 技巧,而现在只需要一个简单的概念约束。
1.9 SFINAE 与 Concepts 的对比
SFINAE(Substitution Failure Is Not An Error)是 C++98/03 时代就存在的机制,而 Concepts 是 C++20 引入的新特性]。两者都能实现模板参数的约束,但在设计理念和易用性上有着天壤之别。
什么是 SFINAE:
SFINAE 的核心思想是:当模板参数替换导致某些表达式无效时,编译器不会报错,而是静默地排除这个候选函数,继续寻找其他匹配的版本。这个机制原本是模板实例化的一个"副作用",但被开发者巧妙地利用来实现编译期条件分支。
SFINAE 的典型写法:
cpp
// C++17:使用 enable_if 约束整型参数
// 这种写法冗长且晦涩,难以理解和维护
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {
std::cout << "整型参数: " << x << std::endl;
}
// 更复杂的 SFINAE:同时检查多个条件
// 当条件复杂时,代码几乎不可读
template<typename T,
typename = std::enable_if_t<
std::is_integral_v<T> &&
!std::is_same_v<T, bool> &&
sizeof(T) <= 4
>>
void bar(T x) {
std::cout << "小型非布尔整型: " << x << std::endl;
}
Concepts 的等效写法:
cpp
// C++20:使用概念约束
// 语义清晰,一目了然
template<std::integral T>
void foo(T x) {
std::cout << "整型参数: " << x << std::endl;
}
// 复杂条件同样清晰
template<typename T>
concept SmallNonBoolIntegral = std::integral<T> &&
!std::same_as<T, bool> &&
sizeof(T) <= 4;
template<SmallNonBoolIntegral T>
void bar(T x) {
std::cout << "小型非布尔整型: " << x << std::endl;
}
对比总结:
| 对比维度 | SFINAE | Concepts |
|---|---|---|
| 语法复杂度 | 复杂,需要 enable_if、decltype 等技巧 | 简洁直观,使用 concept 关键字 |
| 可读性 | 差,代码晦涩难懂 | 好,语义清晰,像自然语言 |
| 错误信息 | 几百行模板内部错误信息 | 清晰的约束违反提示 |
| 可维护性 | 低,修改约束容易出错 | 高,约束分离为独立概念 |
| 表达能力 | 通过类型萃取间接表达 | 通过 requires 表达式直接表达 |
| 组合能力 | 使用模板元编程技巧组合 | 使用 && 和 |
Concepts 不仅是 SFINAE 的语法糖,它提供了更强大的表达能力和更清晰的语义。在 C++20 及以后的版本中,应该优先使用 Concepts 而不是 SFINAE。
第二章 协程:异步编程的新范式
2.1 什么是协程
协程(Coroutine)是一个可以暂停执行并在之后恢复执行的函数。这个概念听起来简单,但它彻底改变了我们编写异步代码的方式
普通函数 vs 协程:
普通函数的执行路径是线性的:调用 -> 执行 -> 返回。一个函数一旦开始执行,就必须一直运行到结束,中间不能暂停。函数返回后,其所有的局部变量和状态都会被销毁。
协程则不同:调用 -> 执行到挂起点 -> 暂停(保存当前状态)-> 之后某个时刻恢复 -> 从暂停点继续执行 -> 最终结束。协程可以在执行过程中多次暂停和恢复,暂停时保存所有局部变量和执行位置,恢复时从暂停点继续执行,是不是感觉有点像langchain中的checkpoint呢~~,大家要融会贯通起来理解哦,每门编程语言都有它们的共通之处。
cpp
// 普通函数:从头到尾一次性执行
int normal_function() {
int result = 0;
for (int i = 0; i < 5; ++i) {
result += i; // 必须一次性执行完整个循环
}
return result; // 返回后,所有局部变量被销毁
}
// 协程函数:可以暂停和恢复
// 只要函数体包含 co_await、co_yield、co_return 中的任意一个
// 它就是协程
std::generator<int> coroutine_function() {
int result = 0;
for (int i = 0; i < 5; ++i) {
result += i;
co_yield result; // 暂停执行,返回当前结果
// 下次恢复时,从这里继续执行
}
// 协程结束
}
三个协程关键字:
C++20 定义了三个协程关键字,每个关键字扮演不同的角色:
co_await expression:用于暂停当前协程的执行,直到某个异步操作完成。co_await是协程中最核心的关键字,它让协程可以"等待"某个操作而不阻塞线程。co_yield expression:用于暂停执行并返回一个值给调用者。co_yield常用于生成器(Generator)模式,可以按需生成一系列值。co_return expression:用于完成协程的执行并返回一个值。co_return之于协程,就像return之于普通函数。
编译器如何识别协程:
当编译器遇到一个包含 co_await、co_yield 或 co_return 的函数时,它会自动将这个函数转换为一个协程状态机。这个转换过程是编译器自动完成的,但编译器需要开发者提供一些"基础设施"来指导协程的行为。
2.2 协程和线程的关系
协程和线程是不同维度的概念,它们之间是协作与互补的关系,而不是替代关系
线程:
线程是操作系统调度的基本单位。每个线程有自己的栈、寄存器和执行上下文。线程的创建、切换和销毁都由操作系统内核管理,这些操作的开销相对较大。一个线程在等待 I/O 操作时,会被操作系统挂起,让出 CPU 给其他线程执行。
协程:
协程是程序层面的概念,它由编译器管理,而不是操作系统。协程的切换完全在用户态进行,不需要进入内核态,因此开销极小。一个线程可以运行多个协程,协程之间的切换由程序自身控制。
用一个比喻来理解:
想象一家工厂,线程是工厂里的工人,协程是工人手中的工作手册。
- 工人(线程)是执行任务的基本单位,由厂长(操作系统)分配任务。
- 工作手册(协程)详细记录了某个任务的执行步骤。工人可以按照手册的步骤执行,遇到需要等待的步骤(比如等待材料到货),就把手册放在一边,去做其他工作。
- 当材料到货后,工人拿起手册,从上次停下的地方继续执行。
实践中的协作模式:
在实际项目中,协程通常与线程池结合使用:
- 线程池提供并行计算能力,由操作系统管理线程的调度。
- 协程管理大量的并发 I/O 任务,协程的切换比线程切换高效得多。
cpp
// 协程与线程池的协作模式
// 协程负责管理异步 I/O 操作
// 线程池负责执行计算密集型任务
// 两者结合,发挥各自的优势
// 协程发起异步 I/O 操作
// 在 I/O 等待期间,协程被挂起,线程可以处理其他任务
// I/O 完成后,协程被恢复,继续处理
Task<int> read_file_async(const std::string& path) {
// 协程被挂起,当前线程可以处理其他任务
std::string content = co_await async_read_file(path);
// I/O 完成后,协程恢复执行
int result = process_content(content);
co_return result;
}
性能对比:
| 对比维度 | 线程切换 | 协程切换 |
|---|---|---|
| 切换开销 | 高(微秒级) | 极低(纳秒级) |
| 是否进入内核态 | 是 | 否 |
| 需要保存的内容 | 完整寄存器、页表等 | 少量寄存器 |
| 可创建的并发数量 | 数千 | 数十万 |
| 编程复杂度 | 需要考虑同步和竞态 | 自然的同步代码风格 |
2.3 C++20 协程的三个核心组件
C++20 协程的设计非常灵活,但这也意味着它不是一个"开箱即用"的特性。要使用协程,开发者需要理解三个核心组件,并实现相应的类型。
这三个核心组件是:
- Promise 对象(Promise Object):协程的大脑,控制协程的创建、挂起、返回、异常处理等行为。
- 协程句柄(Coroutine Handle):协程的遥控器,允许外部代码控制协程的执行(恢复、销毁等)。
- Awaitable 对象(Awaitable Object) :协程的等待机制,定义
co_await表达式的行为。
2.3.1 Promise 对象
Promise 对象是协程的核心控制器。它定义在协程的返回类型内部,命名为 promise_type。编译器在创建协程时会自动构造 promise_type 的实例,并调用它的一系列成员函数来控制协程的行为。
Promise 对象的完整接口:
cpp
#include <coroutine>
#include <iostream>
#include <exception>
// 一个完整的协程返回类型定义
struct Task {
// promise_type 是协程框架的核心
// 编译器会查找这个类型并使用它来控制协程
struct promise_type {
int value = 0; // 存储协程产出的值
std::exception_ptr exception; // 存储协程中抛出的异常
// 1. get_return_object()
// 作用:创建并返回给调用者协程的返回值对象
// 调用时机:在协程函数体执行之前调用
// 返回值:协程的返回类型(这里是 Task)
// 关键操作:使用 from_promise 创建协程句柄
// 并将句柄传递给 Task 的构造函数
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 2. initial_suspend()
// 作用:决定协程在开始执行函数体之前是否应该先挂起
// 返回值:一个 Awaitable 对象
// std::suspend_always{} : 总是挂起(惰性执行)
// std::suspend_never{} : 从不挂起(立即执行)
// 返回 std::suspend_always{} 意味着协程创建后不会立即执行
// 需要手动调用 resume() 来启动
std::suspend_always initial_suspend() {
std::cout << "协程已创建,初始挂起" << std::endl;
return {};
}
// 3. final_suspend()
// 作用:决定协程执行结束后是否挂起
// 返回值:一个 Awaitable 对象
// std::suspend_always{} : 挂起,需要手动销毁协程帧
// std::suspend_never{} : 不挂起,协程自动销毁
// 注意:如果返回 std::suspend_always,必须手动调用 destroy()
// 否则会导致内存泄漏
std::suspend_always final_suspend() noexcept {
std::cout << "协程执行结束,最终挂起" << std::endl;
return {};
}
// 4. return_void() 和 return_value()
// 作用:处理 co_return 语句
// 注意:两者只能实现其中一个,不能同时实现
// return_void() : 处理 co_return;(无返回值)
// return_value() : 处理 co_return value;(有返回值)
void return_void() {
std::cout << "协程无值返回" << std::endl;
}
// 5. yield_value()
// 作用:处理 co_yield 语句
// co_yield expr 被编译器转换为 co_await promise.yield_value(expr)
// 返回值:一个 Awaitable 对象,决定产出值后是否挂起
// 通常返回 std::suspend_always{} 来挂起协程
// 让调用者有机会处理产出的值
std::suspend_always yield_value(int val) {
std::cout << "协程产出值: " << val << std::endl;
value = val; // 存储产出的值
return {}; // 产出后挂起
}
// 6. unhandled_exception()
// 作用:处理协程体内未捕获的异常
// 当协程内抛出异常且没有被 try-catch 捕获时
// 这个函数会被调用
// 通常使用 std::current_exception() 捕获异常
// 存储后,在外部重新抛出
void unhandled_exception() {
std::cout << "捕获到未处理异常" << std::endl;
exception = std::current_exception(); // 存储异常
}
};
// 协程句柄,用于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数:接收协程句柄
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
// 析构函数:销毁协程帧
// 注意:因为 final_suspend() 返回 std::suspend_always
// 所以必须手动销毁协程帧
~Task() {
if (handle) {
std::cout << "析构函数中销毁协程帧" << std::endl;
handle.destroy(); // 手动销毁协程帧
}
}
// 获取协程执行的结果
int get_result() {
// 如果协程还没有执行完成,先恢复它
if (!handle.done()) {
handle.resume();
}
// 检查是否有异常存储
if (handle.promise().exception) {
std::rethrow_exception(handle.promise().exception);
}
return handle.promise().value;
}
};
各成员函数详细解析:
get_return_object():
- 参数 :无参数。通过
std::coroutine_handle<promise_type>::from_promise(*this)获取当前 promise 对应的句柄。 - 返回值 :协程的返回类型(这里是
Task)。这个返回值会在协程创建时返回给调用者。 - 调用时机 :在协程函数体开始执行之前调用。也就是说,在
co_await、co_yield等操作之前,get_return_object()就已经被调用了。 - 典型实现:创建一个句柄,并用这个句柄构造协程的返回对象。调用者通过这个返回对象来控制协程的执行。
initial_suspend():
- 参数:无参数。
- 返回值 :一个 Awaitable 对象(如
std::suspend_always或std::suspend_never)。 - 行为 :
std::suspend_always{}表示协程创建后立即挂起,不执行函数体。std::suspend_never{}表示协程创建后立即开始执行函数体。 - 用途 :
std::suspend_always用于"惰性"协程,协程需要手动恢复。std::suspend_never用于"饥饿"协程,协程创建后自动开始执行。
final_suspend():
- 参数 :无参数。必须声明为
noexcept。 - 返回值:一个 Awaitable 对象。
- 行为 :
std::suspend_always{}表示协程结束后挂起,允许外部代码在销毁前读取协程的状态。std::suspend_never{}表示协程结束后自动销毁。 - 重要提示 :如果返回
std::suspend_always,必须在外部显式调用destroy()来销毁协程帧,否则会导致内存泄漏。如果返回std::suspend_never,协程会自动销毁,此时再调用destroy()是未定义行为。
return_void() 和 return_value():
- 参数 :
return_void()无参数。return_value(type value)接收一个值,类型由协程的co_return语句决定。 - 返回值 :
void。 - 注意 :两者只能实现其中一个,不能同时实现。如果协程中既有
co_return value;又有co_return;,编译器会报错。 - 调用时机 :当协程执行到
co_return语句时调用。
yield_value():
- 参数 :
co_yield表达式的值,类型由协程决定。 - 返回值:一个 Awaitable 对象,决定产出值后是否挂起协程。
- 行为 :
co_yield expr被编译器转换为co_await promise.yield_value(expr)。yield_value()将产出的值存储到 promise 的成员变量中,然后返回一个挂起指示器。 - 用途:用于生成器场景,每次产生一个值后挂起,让调用者处理这个值。
unhandled_exception():
- 参数:无参数。
- 返回值 :
void。 - 行为 :当协程体内抛出异常且未被捕获时,这个函数会被调用。通常使用
std::current_exception()捕获异常并存储到std::exception_ptr中,以便在外部重新抛出。 - 注意:即使协程抛出异常,RAII 机制依然有效,栈上的对象(在协程帧内)会正确析构。
2.3.2 协程句柄
协程句柄(std::coroutine_handle<Promise>)是一个非拥有的、轻量级的句柄,它代表并允许你从外部直接操作一个特定的协程帧 。你可以把它理解成一个"遥控器",通过它来控制协程的执行。
协程句柄的核心 API:
cpp
#include <coroutine>
// 协程句柄的核心接口
// P 是 promise_type 的类型
template<typename Promise = void>
struct coroutine_handle {
// 构造函数:默认构造一个空句柄
coroutine_handle() noexcept;
// from_promise():从 Promise 对象创建句柄
// 参数:P& p - 一个 Promise 对象的引用
// 返回值:指向该 Promise 的协程句柄
// 调用时机:通常在 promise_type::get_return_object() 中使用
static coroutine_handle from_promise(Promise& p) noexcept;
// promise():获取句柄管理的 Promise 对象的引用
// 返回值:Promise& - 指向 Promise 对象的引用
// 用途:通过句柄访问 Promise 中存储的数据
Promise& promise() const;
// resume():恢复挂起的协程
// 行为:让协程从上次挂起的位置继续执行
// 直到遇到下一个挂起点或协程结束
// 注意:不能在非挂起状态的协程上调用
void resume() const;
// operator():与 resume() 功能相同
void operator()() const;
// done():检查协程是否已执行完成
// 返回值:bool
// true - 协程已执行完成(到达函数末尾或执行了 co_return)
// false - 协程仍在运行或处于挂起状态
bool done() const;
// operator bool():检查句柄是否有效
// 返回值:bool
// true - 句柄指向一个有效的协程
// false - 句柄为空(nullptr)
explicit operator bool() const;
// destroy():显式销毁协程帧
// 调用时机:当 final_suspend() 返回 std::suspend_always 时
// 必须在外部显式调用 destroy() 来释放协程帧的内存
// 否则会导致内存泄漏
void destroy() const;
// address():获取协程帧的地址
// 返回值:void* - 协程帧的底层地址
// 用途:用于调试或与底层系统交互
void* address() const;
// from_address():从地址创建协程句柄
// 参数:void* addr - 协程帧的地址
// 返回值:对应的协程句柄
static coroutine_handle from_address(void* addr);
};
协程句柄的各种 API 使用详解:
from_promise():
- 参数 :
Promise& p,一个 Promise 对象的引用。 - 返回值 :
std::coroutine_handle<Promise>,指向该 Promise 的协程句柄。 - 它是静态成员函数 ,通过 Promise 对象来创建句柄。这是创建协程句柄的主要方式,通常在
promise_type::get_return_object()中使用。 - 使用场景 :
auto handle = std::coroutine_handle<promise_type>::from_promise(*this);
promise():
- 参数:无。
- 返回值 :
Promise&,指向 Promise 对象的引用。 - 用途 :通过句柄访问 Promise 中存储的数据,如
handle.promise().current_value。 - 注意 :在空句柄上调用
promise()是未定义行为,调用前应检查句柄是否有效。
resume() 和 operator():
- 参数:无。
- 返回值 :
void。 - 行为:让一个处于挂起状态的协程继续执行。协程从上次挂起的地方恢复,直到遇到下一个挂起点、协程结束、或抛出异常。
- 注意 :在非挂起状态的协程上调用
resume()是未定义行为。调用resume()前应使用done()检查协程状态。
done():
- 参数:无。
- 返回值 :
bool。true表示协程已执行完成,false表示协程仍在运行或处于挂起状态。 - 用途 :在调用
resume()前检查协程是否还有工作要做。
destroy():
- 参数:无。
- 返回值 :
void。 - 行为:显式销毁协程帧,释放其内存。
- 何时调用 :当
final_suspend()返回std::suspend_always时,必须在外部显式调用destroy()。否则会导致内存泄漏。 - 注意 :如果
final_suspend()返回std::suspend_never,协程会自动销毁,此时再调用destroy()是未定义行为。
2.3.3 Awaitable 对象
在 C++20 协程中,任何可以出现在 co_await 表达式右边的对象,都是一个 Awaitable 对象。Awaitable 对象定义了协程遇到 co_await 时的行为。
Awaitable 的三个核心函数:
一个类型要成为 Awaitable,必须实现三个特定的成员函数(或者通过 operator co_await 返回一个实现了这些函数的对象)。编译器会将 co_await expression 转换为一系列对这些函数的调用。
cpp
#include <coroutine>
#include <iostream>
// 编译器对 co_await 的转换逻辑
// 当编译器遇到 co_await expr 时,会生成类似下面的代码:
{
// 步骤 1:获取 awaitable 对象
auto&& awaitable = <expression>; // 获取 co_await 右边的表达式
// 步骤 2:检查是否已就绪
// 调用 await_ready() 检查操作是否已经完成
// 如果已就绪,跳过挂起步骤,直接获取结果
if (!awaitable.await_ready()) {
// 步骤 3:准备暂停
// 调用 await_suspend(),传入当前协程的句柄
// 这里安排异步操作,并将协程句柄传递给操作
// 当操作完成时,通过句柄恢复协程
awaitable.await_suspend(current_coroutine_handle);
// 步骤 4:协程在此处暂停
// 编译器插入暂停点
<suspend-coroutine>
// 步骤 5:当协程恢复时,从这里继续执行
// 异步操作完成后,通过句柄恢复协程
}
// 步骤 6:获取结果
// 调用 await_resume() 获取 co_await 表达式的结果
return awaitable.await_resume();
}
三个核心函数的详细解析:
bool await_ready() const:
- 参数:无。
- 返回值 :
bool。true表示异步操作已完成,无需暂停协程。false表示操作未完成,需要暂停协程。 - 用途 :这是一个性能优化机制。如果操作已经完成(比如缓存命中),直接返回
true可以避免不必要的暂停和恢复开销。 - 最佳实践 :通常声明为
noexcept。对于立即完成的操作,返回true。
type await_suspend(std::coroutine_handle<>) const:
- 参数 :
std::coroutine_handle<>,代表当前正在执行的协程的句柄。可以保存这个句柄,在异步操作完成后通过它恢复协程。 - 返回值 :可以是
void、bool或std::coroutine_handle<>,不同的返回值类型决定了不同的控制流行为。
返回值类型详解:
void:默认行为。协程暂停,控制权返回到当前协程的调用者。这是最常见的返回类型。bool:true表示协程已挂起,控制权返回到调用者。false表示不挂起协程,立即恢复当前协程(相当于await_ready()返回true)。std::coroutine_handle<>:协程暂停,然后立即恢复返回的句柄所代表的协程。这实现了"对称转移"(symmetric transfer),是无栈协程链式调用的关键机制。
type await_resume() const:
- 参数:无。
- 返回值 :可以是
void或任何其他类型。这个返回值就是整个co_await表达式的结果。 - 用途 :在协程恢复后(或
await_ready()返回true后)被调用,用于获取异步操作的结果。 - 最佳实践 :如果操作可能失败,可以在这里抛出异常。如果操作不可能失败,声明为
noexcept。
标准库提供的 Awaitable 类型:
C++20 标准库提供了两个最简单的 Awaitable 类型:
cpp
#include <coroutine>
// std::suspend_always:总是暂停
struct suspend_always {
// await_ready() 总是返回 false
// 意味着协程总是会暂停
constexpr bool await_ready() const noexcept { return false; }
// await_suspend() 什么都不做
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
// await_resume() 什么都不做,返回 void
constexpr void await_resume() const noexcept {}
};
// std::suspend_never:从不暂停
struct suspend_never {
// await_ready() 总是返回 true
// 意味着协程永远不会暂停
constexpr bool await_ready() const noexcept { return true; }
// await_suspend() 永远不会被调用
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
// await_resume() 什么都不做,返回 void
constexpr void await_resume() const noexcept {}
};
operator co_await() 重载:
operator co_await 是一个用户定义的转换函数,它允许将自定义类型转换为可以被 co_await 使用的 Awaitable 类型:
cpp
#include <coroutine>
#include <iostream>
// 自定义类型,通过 operator co_await 提供 Awaitable 支持
class AdvancedTask {
int value_;
public:
AdvancedTask(int value) : value_(value) {}
// 对于 const 左值的 operator co_await
// 当 co_await 一个 const 左值对象时使用
auto operator co_await() const & {
// 返回一个匿名的 Awaitable 结构体
struct ConstLvalueAwaiter {
const AdvancedTask& task_; // 持有对象的 const 引用
// 总是挂起(演示目的)
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
// 当 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
// 当 co_await 一个可修改的左值对象时使用
auto operator co_await() & {
struct LvalueAwaiter {
AdvancedTask& task_; // 持有对象的非 const 引用
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
task_.value_ += 10; // 可以修改对象
return task_.value_;
}
};
return LvalueAwaiter{*this};
}
};
operator co_await 三种重载形式的区别:
| 重载形式 | 语法 | 适用场景 | 能否修改对象 |
|---|---|---|---|
const & |
operator co_await() const & |
co_await const_obj |
不能修改 |
& |
operator co_await() & |
co_await mutable_obj |
可以修改 |
&& |
operator co_await() && |
co_await std::move(obj) |
可以修改并移动 |
2.4 协程的执行流程
理解了协程的三个核心组件后,我们来看一个完整的协程执行流程。协程的生命周期可以分为几个清晰的阶段。
阶段一:协程创建
当调用一个协程函数时,编译器执行以下步骤:
- 分配协程帧(coroutine frame)的内存。协程帧包含了所有局部变量、参数副本、Promise 对象等。
- 在协程帧内构造 Promise 对象(
promise_type的实例)。 - 调用
promise.get_return_object()获取协程的返回对象,并返回给调用者。 - 调用
promise.initial_suspend(),根据返回值决定是否挂起协程。
阶段二:初始挂起或立即执行
- 如果
initial_suspend()返回std::suspend_always{},协程挂起,不执行函数体。调用者获得返回对象,但协程处于"未启动"状态。 - 如果
initial_suspend()返回std::suspend_never{},协程立即开始执行函数体。
阶段三:执行到挂起点
协程执行函数体,直到遇到以下情况之一:
co_await expr:检查await_ready(),如果返回false,协程挂起。await_suspend()被调用,传入当前协程句柄。异步操作完成后,通过句柄恢复协程。co_yield expr:调用promise.yield_value(expr),将值存储到 Promise 中,然后根据返回值决定是否挂起。co_return expr或函数体结束:调用promise.return_value(expr)或promise.return_void()。
阶段四:最终挂起
- 调用
promise.final_suspend(),根据返回值决定是否挂起。 - 如果返回
std::suspend_always{},协程挂起,外部代码可以在销毁前读取协程状态。 - 如果返回
std::suspend_never{},协程自动销毁。
阶段五:协程销毁
- 如果
final_suspend()返回std::suspend_always{},必须在外部显式调用handle.destroy()来销毁协程帧。 - 如果
final_suspend()返回std::suspend_never{},协程自动销毁。
2.5 协程学习样例之生成器
生成器(Generator)是协程最直观的应用场景之一 。它允许我们编写一个函数,按需生成一系列值,而不是一次性生成所有值。
简单的 Generator 实现:
cpp
#include <coroutine>
#include <exception>
#include <iostream>
// 1. 协程的返回类型 - Generator
struct Generator {
// 2. 核心:promise_type 必须定义为嵌套类型
struct promise_type {
int current_value; // 用于存储产出的值
// 2a. 创建返回值对象
// 通过 from_promise 创建句柄,并用句柄构造 Generator
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 2b. 初始挂起:返回 std::suspend_always{}
// 协程创建后立即挂起,不执行函数体
// 实现"惰性生成"------按需启动
std::suspend_always initial_suspend() { return {}; }
// 2c. 最终挂起:返回 std::suspend_always{}
// 协程结束后挂起,需要手动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2d. 处理 co_yield
// co_yield value 被转换为 promise.yield_value(value)
// 将值存储到 current_value 中,然后返回挂起指示器
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) {}
// 析构函数:负责销毁协程帧
// 因为 final_suspend() 返回 std::suspend_always
// 所以必须手动销毁协程帧
// 如果忘记调用 destroy(),会导致内存泄漏
~Generator() {
if (handle) handle.destroy();
}
// 4. 提供给外部的 API
// 获取当前产出的值
// 从 Promise 对象的 current_value 成员中读取
int value() const {
return handle.promise().current_value;
}
// 恢复协程执行,直到下一个 co_yield 或协程结束
// 返回值:true 表示还有值(协程尚未结束)
// false 表示没有更多值了(协程已结束)
bool next() {
// 检查协程是否已结束
if (!handle.done()) {
handle.resume(); // 恢复协程执行
}
// 返回协程是否还有值
// 注意:这里返回 !handle.done() 的取反
// 如果协程已结束,返回 false;否则返回 true
return !handle.done();
}
};
// 使用生成器的协程函数
// 生成从 start 到 end-1 的整数序列
Generator range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // 产出当前值并挂起
// 外部调用 next() 后,从下一行继续执行
}
// 循环结束,协程执行完毕
// 自动调用 promise.return_void()
}
int main() {
// 创建协程,initial_suspend() 返回 std::suspend_always
// 此时协程处于挂起状态,for 循环尚未开始执行
auto gen = range(1, 5);
// 循环恢复协程,每次恢复后读取产出的值
while (gen.next()) { // 恢复协程,执行到 co_yield 处挂起
// 从 Promise 中读取产出的值
std::cout << gen.value() << " ";
}
// 输出: 1 2 3 4
// main 函数结束,gen 析构
// 析构函数中调用 handle.destroy() 销毁协程帧
return 0;
}
执行流程详细拆解:
协程创建阶段(auto gen = range(1, 5)):
- 编译器创建协程帧,在帧内存储局部变量
i、参数start=1、end=5和promise_type实例。 - 编译器构造
promise_type实例,初始化current_value。 - 调用
promise.get_return_object():通过std::coroutine_handle<promise_type>::from_promise(*this)创建句柄,并用句柄构造Generator对象。 - 调用
promise.initial_suspend():返回std::suspend_always{},协程挂起。此时range的for循环尚未开始执行。 main中的gen已就绪,但协程处于"未启动"状态。
初始挂起状态(initial_suspend() 返回 std::suspend_always{}):
协程创建后不立即执行,而是先挂起。这实现了"惰性执行"------协程按需启动,而不是在创建时立即执行。此时 main 中的 gen 已经就绪,但 range 的 for 循环尚未开始执行。
恢复执行阶段(gen.next() 调用):
gen.next()判断!handle.done()(当前协程未结束,done()返回false),调用handle.resume()。- 协程从挂起点恢复,执行
range的逻辑:
- 第一次恢复:进入
for循环,i = 1,执行co_yield 1。 - 后续恢复:从
co_yield的挂起点继续,i++,再次执行co_yield,直到i >= end。
产出值挂起状态(co_yield 触发):
co_yield i被编译器转换为co_await promise.yield_value(i)。promise.yield_value(i)将i存入promise.current_value。yield_value()返回std::suspend_always{},协程再次挂起。gen.next()返回!handle.done()(此时协程未结束,返回true)。- 外部通过
gen.value()读取promise.current_value的值。
协程结束阶段(for 循环退出):
range 的 for 循环执行完毕(i = 5 >= end = 5),协程执行到函数末尾。编译器自动调用 promise.return_void()。此时协程标记为"已结束"(handle.done() 变为 true)。
最终挂起状态(final_suspend() 触发):
promise.final_suspend() 返回 std::suspend_always{},协程保持挂起状态。此时 gen.next() 再次调用时,handle.done() 为 true,next() 返回 false,main 的 while 循环退出。
协程销毁阶段(gen 析构):
main 函数结束,Generator 对象 gen 出作用域。析构函数判断 handle 有效,调用 handle.destroy()。销毁协程帧(释放 i、start、end、promise_type 实例的内存),协程生命周期完全结束。
支持迭代器的 Generator 实现:
上面的例子中,我们使用 while 循环和 next()、value() 方法来遍历生成器。更优雅的方式是提供一个迭代器,使生成器支持范围 for 循环:
cpp
#include <coroutine>
#include <iostream>
// 模板化的 Generator 支持迭代器
template<typename T>
struct Generator {
// promise_type 定义
struct promise_type {
T current_value; // 存储产出的值
// 创建 Generator 返回对象
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(); }
// 处理 co_yield
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
// 处理 co_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;
}
// 不等于比较:检查协程是否还有值
// 用于 for 循环的结束条件判断
bool operator!=(const iterator& other) const {
return !handle.done(); // 协程未结束就继续
}
};
// 构造函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
// 析构函数
~Generator() { if (handle) handle.destroy(); }
// begin():返回起始迭代器,首次恢复协程
iterator begin() {
if (handle)
handle.resume(); // 首次恢复
return iterator{ handle };
}
// end():返回结束迭代器(空句柄)
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)) { // 使用范围 for 循环
std::cout << i << " ";
}
std::cout << std::endl;
std::cout << "Fibonacci below 50: ";
for (auto num : fibonacci(50)) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
std::generator(C++23 标准库支持):
C++23 标准库正式引入了 std::generator,它提供了标准化的生成器支持。在后来的 C++26 版本中,还在进一步支持 std::task 等协程类型。
cpp
// C++23 标准库的 std::generator 使用示例
// 需要 C++23 编译器支持
#include <generator>
#include <iostream>
#include <ranges>
// 使用 std::generator 实现斐波那契数列生成器
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 循环遍历生成器
for (auto num : fibonacci(10)) {
std::cout << num << " ";
}
// 输出: 0 1 1 2 3 5 8 13 21 34
std::cout << std::endl;
// 结合 ranges 视图
auto fib = fibonacci(20);
// 只取前 10 个
for (auto num : fib | std::views::take(10)) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
2.6 协程学习样例之异步线程恢复
这个样例展示了 co_await 的完整使用方法,以及协程如何在另一个线程中恢复执行 。这是理解协程异步能力的关键例子。
cpp
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 增强的可等待对象,添加更多日志以展示执行流程
struct AsyncOperation {
int value; // 操作结果值
bool ready; // 是否已就绪
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;
}
// await_ready():检查异步操作是否已完成
// 如果 ready 为 true,表示操作已完成,不需要暂停协程
// 这是性能优化:如果结果已经就绪,直接返回,避免暂停开销
bool await_ready() const noexcept {
std::cout << "[" << name << "] await_ready() 调用, 返回: "
<< ready << " (线程: " << std::this_thread::get_id() << ")"
<< std::endl;
return ready;
}
// await_suspend():协程即将暂停时调用
// 参数:handle - 当前协程的句柄,用于在异步操作完成后恢复协程
// 这里我们启动一个新线程来模拟异步操作
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()
// 这意味着协程的后续逻辑将在当前线程(异步线程)中执行
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;
}
// await_resume():协程恢复后调用
// 返回值就是 co_await 表达式的结果
int await_resume() noexcept {
std::cout << "[" << name << "] await_resume() 调用, 返回结果: "
<< value << " (线程: " << std::this_thread::get_id() << ")"
<< std::endl;
return value; // 返回异步操作的结果
}
};
// 协程返回类型 Task
struct Task {
struct promise_type {
int current_value;
// 创建 Task 返回对象
Task get_return_object() {
std::cout << "promise_type::get_return_object() 调用" << std::endl;
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// initial_suspend() 返回 std::suspend_never
// 意味着协程创建后立即开始执行,不需要手动恢复
std::suspend_never initial_suspend() noexcept {
std::cout << "promise_type::initial_suspend() 调用 - 立即开始执行"
<< std::endl;
return {};
}
// final_suspend() 返回 std::suspend_always
// 需要手动销毁协程帧
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;
// 第一次 co_await:执行异步操作
// 协程将被挂起,在另一个线程中执行异步操作
// 操作完成后,协程在异步线程中恢复执行
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;
// 第二次 co_await:再次执行异步操作
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;
// 第三次 co_await:尝试一个已经就绪的操作
// 因为 ready 为 true,await_ready() 返回 true
// 协程不会挂起,直接返回结果
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;
// 创建协程
// 因为 initial_suspend() 返回 std::suspend_never
// 协程创建后立即开始执行
auto task = detailed_async_example();
std::cout << "协程任务已创建,控制权返回主函数"
<< " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 主线程等待异步操作完成
// 注意:协程在 co_await 处挂起后,控制权返回主线程
// 主线程可以继续做其他事情
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;
}
执行流程分析:
main函数开始,输出当前线程 ID。- 调用
detailed_async_example()创建协程。因为initial_suspend()返回std::suspend_never{},协程立即开始执行。 - 协程进入
main函数,输出"协程函数开始执行"。 - 执行到第一次
co_await:创建AsyncOperation{100, false, "操作1"}。await_ready()返回false(因为ready = false),协程需要挂起。 await_suspend()被调用,传入当前协程的句柄。函数启动一个新线程,在分离的线程中执行异步操作(模拟耗时 1 秒的操作)。await_suspend()返回,协程挂起。控制权返回到main函数。main函数继续执行,输出"协程任务已创建",然后主线程睡眠 3 秒。- 1 秒后,异步线程完成操作,调用
handle.resume()恢复协程。 - 协程在异步线程中恢复执行,从
co_await处继续。await_resume()被调用,返回值作为co_await的结果。 - 协程继续执行,遇到第二次
co_await,重复步骤 4-9。 - 第三次
co_await时,ready = true,await_ready()返回true,协程不挂起,直接返回结果。 - 协程执行完成,
final_suspend()被调用,协程挂起等待销毁。 main函数中的task对象析构,析构函数调用handle.destroy()销毁协程帧。
2.7 协程中的异常处理
协程中的异常处理有一套特殊的机制。当协程体内抛出异常时,异常的处理方式取决于 Promise 类型中 unhandled_exception() 方法的实现。
cpp
#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 {}; }
// 最终挂起(必须标记为 noexcept)
std::suspend_always final_suspend() noexcept { return {}; }
// 关键:异常处理函数
// 当协程体内抛出异常且未被捕获时,此函数被调用
// 使用 std::current_exception() 捕获当前异常
// 并将异常存储到 exception 成员变量中
void unhandled_exception() {
std::cout << "捕获到异常,存储起来" << std::endl;
exception = std::current_exception(); // 存储异常
}
// 处理 co_return value;
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 << "重新抛出存储的异常" << std::endl;
std::rethrow_exception(handle.promise().exception);
}
// 返回正常结果
return handle.promise().value;
}
// 析构:销毁协程帧
~SimpleTask() {
if (handle) handle.destroy();
}
};
// 会抛出异常的协程函数
SimpleTask throwing_coroutine() {
std::cout << "异常协程开始执行" << std::endl;
// 第一次挂起
co_await std::suspend_always{};
std::cout << "准备抛出异常..." << std::endl;
// 抛出异常
// 这个异常不会被协程体内的 try-catch 捕获
// 所以会被传递到 unhandled_exception() 处理
throw std::runtime_error("协程内部发生错误!");
co_return -1; // 这行不会执行,因为异常已经抛出
}
// 立即抛出异常的协程
SimpleTask immediate_throw_coroutine() {
std::cout << "立即抛出异常的协程" << std::endl;
// 在协程开始时就抛出异常
// 注意:这里没有 co_await 之前就抛出了异常
// 异常仍然会被 unhandled_exception() 捕获
throw std::logic_error("立即发生的错误!");
co_return 100; // 不会执行
}
int main() {
std::cout << "=== 测试异常协程 ===" << std::endl;
try {
// 创建协程
auto task2 = throwing_coroutine();
// 获取结果:如果协程内部有异常,这里会重新抛出
int result = task2.get_result();
std::cout << "结果: " << result << std::endl;
}
catch (const std::runtime_error& e) {
// 捕获从协程重新抛出的异常
std::cout << "捕获到 runtime_error: " << e.what() << std::endl;
}
std::cout << "\n=== 测试立即异常 ===" << std::endl;
try {
auto task3 = immediate_throw_coroutine();
int result = task3.get_result();
std::cout << "结果: " << result << std::endl;
}
catch (const std::logic_error& e) {
std::cout << "捕获到 logic_error: " << e.what() << std::endl;
}
return 0;
}
异常处理的关键点:
-
unhandled_exception():这是 Promise 类型的核心异常处理函数。当协程体内抛出异常且未被捕获时,此函数被调用。通常使用std::current_exception()捕获当前异常并存储到std::exception_ptr中。 -
std::exception_ptr:这是一个可以存储任意类型异常的智能指针。通过std::current_exception()捕获当前异常,通过std::rethrow_exception()重新抛出存储的异常。 -
RAII 在协程中依然有效 :即使协程抛出异常,栈上的对象也会正确析构。协程帧本身的内存由
coroutine_handle管理,在destroy()调用时释放。 -
异常可以在协程外部捕获 :通过
rethrow_exception()可以在协程外部重新抛出异常,然后在外部用try-catch捕获。
2.8 await_transform() 函数
await_transform() 是 Promise 类型的一个可选成员函数,它允许在 co_await 表达式上应用自定义转换。当编译器遇到 co_await expression 时,会查找 Promise 类型的 await_transform() 方法。
cpp
#include <coroutine>
#include <iostream>
#include <string>
// 文件读取等待器
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)来异步读取文件
// 当读取完成时,恢复协程 h
std::cout << "异步读取文件描述符: " << fd_ << std::endl;
// async_read(fd_, h); // 实际项目中这里调用异步 I/O API
}
// 协程恢复后,返回读取到的数据
std::string await_resume() {
// 返回读取到的数据
return "模拟读取到的数据";
}
};
// 协程的 Promise 类型
struct MyPromise {
// ... 其他 Promise 成员函数
// await_transform 允许在 co_await 表达式上应用自定义转换
// 当协程内 co_await 一个 int 时,我们将其视为文件描述符
// 并返回一个自定义的 FileReadAwaiter
// 参数:int fd - 文件描述符
// 返回值:FileReadAwaiter - 一个 Awaitable 对象
// 这个函数的作用是:将 co_await int 转换为 co_await FileReadAwaiter
auto await_transform(int fd) {
std::cout << "将 int 解释为文件描述符,启动异步读取" << std::endl;
return FileReadAwaiter{fd};
}
// 可以重载多个 await_transform 版本
// 处理不同类型的 co_await 参数
// auto await_transform(std::string url) {
// return HttpRequestAwaiter{url};
// }
};
await_transform() 的详细解析:
- 参数 :
co_await表达式的操作数类型。可以是任意类型,Promise 类型可以为不同的参数类型提供不同的重载版本。 - 返回值 :一个 Awaitable 对象(实现了
await_ready、await_suspend、await_resume三个函数的对象)。 - 用途 :允许对
co_await的操作数进行自定义转换。例如,可以将co_await 42(一个整数)转换为co_await FileReadAwaiter{42}(一个文件读取操作)。 - 作用机制 :当编译器遇到
co_await expr时,会先检查 Promise 类型是否有await_transform(expr)方法。如果有,则使用await_transform的返回值作为 Awaitable 对象;如果没有,则直接使用expr本身作为 Awaitable 对象。
第三章 协程的综合实战
3.1 协程学习样例之 echo 服务器
以下代码实现了一个简单的 epoll + 协程的回声服务器,帮助理解协程在网络编程中的使用场景。这个示例需要在 GCC 10 及以后版本上运行,因为这个版本开始才支持协程。
cpp
// 协程 + epoll 实现回声服务器
// 需要 GCC 10+ 编译,链接 -lpthread
// 编译命令:g++ -std=c++20 echo_server.cpp -o echo_server -lpthread
#include <coroutine>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <system_error>
// 以下为简化示例,展示协程在网络编程中的使用模式
// 实际项目应使用成熟的协程库(如 Boost.Asio、libunifex、cppcoro 等)
// 异步读取操作
struct AsyncRead {
int fd; // 文件描述符
char* buffer; // 读取缓冲区
size_t size; // 读取大小
size_t bytes_read; // 已读取的字节数
// 检查是否已就绪
bool await_ready() { return false; }
// 协程挂起时,注册异步读取操作
void await_suspend(std::coroutine_handle<> h) {
// 这里应该通过 epoll 注册读取事件
// 当事件触发时,调用 h.resume() 恢复协程
// 实际实现中,这里会使用 epoll_ctl 注册 EPOLLIN 事件
// 并将协程句柄保存到事件上下文中
std::cout << "注册异步读取,fd: " << fd << std::endl;
}
// 协程恢复时,返回读取的字节数
size_t await_resume() { return bytes_read; }
};
// 异步写入操作
struct AsyncWrite {
int fd;
const char* buffer;
size_t size;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 通过 epoll 注册写入事件
std::cout << "注册异步写入,fd: " << fd << std::endl;
}
size_t await_resume() { return size; }
};
// 协程任务:处理单个客户端连接
// 读取客户端发送的数据,然后原样返回(回声)
struct EchoTask {
struct promise_type {
EchoTask get_return_object() {
return EchoTask{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle<promise_type> handle;
EchoTask(std::coroutine_handle<promise_type> h) : handle(h) {}
~EchoTask() { if (handle) handle.destroy(); }
};
// 回声处理协程
EchoTask echo_handler(int client_fd) {
char buffer[1024];
try {
while (true) {
// 异步读取客户端数据
// 协程在这里挂起,等待数据到达
size_t n = co_await AsyncRead{client_fd, buffer, sizeof(buffer)};
if (n == 0) {
// 客户端关闭连接
std::cout << "客户端关闭连接,fd: " << client_fd << std::endl;
break;
}
// 将数据原样返回给客户端
co_await AsyncWrite{client_fd, buffer, n};
}
}
catch (const std::exception& e) {
std::cout << "处理客户端异常: " << e.what() << std::endl;
}
// 关闭客户端连接
close(client_fd);
}
int main() {
std::cout << "Echo 服务器启动(协程版)" << std::endl;
std::cout << "实际项目中,这里会初始化 epoll 事件循环" << std::endl;
std::cout << "并使用协程处理每个客户端连接" << std::endl;
// 在实际项目中,主循环应该是:
// 1. 创建 socket,绑定端口,监听
// 2. 进入 epoll_wait 循环
// 3. 当有新连接时,创建 EchoTask 协程处理
// 4. 当有数据到达时,恢复对应的协程
std::cout << "\n协程回声服务器的基本模式:" << std::endl;
std::cout << "1. 每个客户端连接创建一个 EchoTask 协程" << std::endl;
std::cout << "2. 协程使用 co_await 等待异步 I/O 操作" << std::endl;
std::cout << "3. 在 I/O 等待期间,协程被挂起,不占用线程" << std::endl;
std::cout << "4. I/O 完成后,协程被恢复,继续处理" << std::endl;
std::cout << "5. 这种方式可以处理大量并发连接,而不需要大量线程" << std::endl;
return 0;
}
3.2 协程库的选择与使用
C++20 协程是一个底层语言设施,而不是一个开箱即用的并发工具。在实际项目中,通常会使用上层封装好的协程库。
常用的协程库:
| 库名称 | 特点 | 适用场景 |
|---|---|---|
| cppcoro | 轻量级,提供了丰富的协程类型 | 学习和中小型项目 |
| libunifex | Facebook 出品,高性能 | 大规模异步系统 |
| Boost.Asio | 成熟稳定,生态完善 | 网络编程 |
| Folly | Facebook 出品,与 C++20 协程集成 | 大规模服务端开发 |
编译器版本支持:
| 编译器 | 最低版本 | 完整支持版本 |
|---|---|---|
| GCC | 10 | 11+ |
| Clang | 10 | 13+ |
| MSVC | VS 2019 16.8 | VS 2022 17.0+ |
检查编译器版本:
cpp
#include <iostream>
int main() {
// MSVC 编译器版本检查
#ifdef _MSC_VER
std::cout << "_MSC_VER = " << _MSC_VER << std::endl; // 主版本号
std::cout << "_MSC_FULL_VER = " << _MSC_FULL_VER << std::endl; // 完整版本号
#endif
// GCC 编译器版本检查
#ifdef __GNUC__
std::cout << "__GNUC__ = " << __GNUC__ << std::endl; // 主版本号
std::cout << "__GNUC_MINOR__ = " << __GNUC_MINOR__ << std::endl; // 次版本号
#endif
// Clang 编译器版本检查
#ifdef __clang__
std::cout << "__clang_major__ = " << __clang_major__ << std::endl;
std::cout << "__clang_minor__ = " << __clang_minor__ << std::endl;
#endif
// 检查 C++ 标准版本
#if __cplusplus >= 202002L
std::cout << "支持 C++20 或更高版本" << std::endl;
#else
std::cout << "不支持 C++20" << std::endl;
#endif
// 检查协程支持
#if __has_include(<coroutine>)
std::cout << "编译器支持 <coroutine> 头文件" << std::endl;
#endif
return 0;
}
结语
C++20 引入的 Concept 和 Coroutine 两个特性,分别从模板编程和异步编程两个维度,给 C++ 语言带来了革命性的变化。
Concept 让模板编程从"黑暗时代"进入了"文明时代"。它让模板参数的约束变得清晰、可读、可维护。编译器给出的错误信息也不再是几百行难以理解的模板实例化堆栈,而是直接指出哪个约束没有被满足。对于库的编写者和使用者来说,这都是一个巨大的进步。
Coroutine 则提供了一种全新的异步编程范式。它让异步代码可以像同步代码一样顺序编写,不再需要回调函数和复杂的状态机。协程的切换开销极低,可以创建大量的并发任务,特别适合 I/O 密集型的应用场景。
当然,C++20 协程目前还处于发展初期。标准库对协程的支持正在逐步完善,C++23 引入了 std::generator,C++26 在进一步支持 std::task 等协程类型。在实际项目中,通常需要结合第三方协程库(如 cppcoro、libunifex、Boost.Asio 等)来获得更好的开发体验。
随着编译器的不断完善和标准库的逐步丰富,Concept 和 Coroutine 必将成为 C++ 开发者日常工作中不可或缺的工具。