docs: address community issue feedback (#5 #338 #142 #246 #136 #303)

- const README: rewrite the space-saving claim per issue #5 follow-ups
  (constant-expression const vs #define immediates; which const qualify
  as constant expressions)
- const README: fix the class-section diagnosis per issue #338 (overload
  resolution picks the const overload; the restriction only surfaces
  once that overload is removed)
- sizeof README: virtual-inheritance result is compiler-dependent
  (GCC merges vbptr+vfptr -> 16, MSVC keeps them separate -> 32) #246
- static README: new section on internal linkage / access restriction
  with anonymous-namespace comparison #142
- 10-day practice: full problem list table + fixed day links #136
- new design_pattern/singleton/singleton_default.hpp: reusable
  thread-safe CRTP singleton template, compile+concurrency tested #303
This commit is contained in:
light-city
2026-10-13 19:23:54 +08:00
parent cfc9af1fcb
commit dfe1c933b3
159 changed files with 20461 additions and 182 deletions

View File

@@ -0,0 +1,229 @@
> 来自 [C++那些事网站](https://light-city.github.io/stories_things/modern-cpp/cpp20/cpp-coroutines/) 的文章,由站点同步脚本导出。
# C++20 协程入门
## 1.什么是Coroutines
自C++20之后,C++终于支持协程了,C++ 23应该带来额外的支持,至少应该涵盖最常见的用例。
**协程**(英语:coroutine)是计算机程序的一类组件,推广了[协作式多任务](https://zh.wikipedia.org/wiki/协作式多任务)的[子例程](https://zh.wikipedia.org/wiki/子例程),允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程更适合于用来实现彼此熟悉的程序组件,如[协作式多任务](https://zh.wikipedia.org/wiki/协作式多任务)、[异常处理](https://zh.wikipedia.org/wiki/异常处理)、[事件循环](https://zh.wikipedia.org/wiki/事件循环)、[迭代器](https://zh.wikipedia.org/wiki/迭代器)、[无限列表](https://zh.wikipedia.org/wiki/惰性求值)和[管道](https://zh.wikipedia.org/wiki/管道_(软件))。
> 来自维基百科的一段话描述了协程的基本思想。
从C++角度,可以简单理解协程是**任何包含co_return, co_yield或co_await的函数**。
目前C++20协程是函数对象上的语法糖,编译器将围绕协程生成一个代码框架。此代码依赖于用户定义的**promise types**。可能会在C++23之后对此有所变动,不过在此之前,我们先按照C++20的规则来,需要自己编写这些类型。
前面提到co_return、co_yield、co_await,这些将在未来章节进行阐述。
## 2.Step By Step Coroutine
接下来,我们从一个简单的coroutine例子入手,下面这个协程不会做任何事情,我们称它为HelloTask courtine。
```cpp
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
HelloTask myCoroutine() {
co_return; // make it a coroutine
}
int main() {
HelloTask x = myCoroutine();
}
```
编译运行这段代码:
```cpp
g++ hello.cpp -fcoroutines -std=c++20
```
需要你的编译器支持C++20特性。
从上到下来浏览一遍这段HelloWorld程序跟平时的开发有何不同。
### 2.1 头文件
首先是头文件引入了`coroutine`,这里我直接拿gcc-10的源码,看看到底做了什么事情。
> https://github.com/gcc-mirror/gcc/blob/releases/gcc-10/libstdc++-v3/include/std/coroutine
内容包括如下:
```cpp
// 用于发现协程promise_type的特征类型
// 核心代码:using promise_type = typename _Result::promise_type;
1.coroutine_traits
// 协程句柄,用于管理暂停或执行的协程以及Promise
2.coroutine_handle
// coroutine_handle全特化版本 是其他特化的公开基类。它保有handle的底层 地址。
2.1 coroutine_handle<void>
// coroutine_handle泛化版本
2.2 template <typename _Promise>
struct coroutine_handle : coroutine_handle<>
// coroutine_handle全特化版本 处理无操作协程
2.3 template <>
struct coroutine_handle<noop_coroutine_promise> : public coroutine_handle<>
3.平凡多可等待对象 Trivial awaitables
// await表达式应该暂停
3.1 suspend_always
// await表达式不应该暂停
3.2 suspend_never
```
可以看到里面包含了很多专有的术语:
- coroutine_handle
- Promise
- awaitable
等等。
后面所讲的协程内容都是围绕这些来展开的。
## 2.2 Promise
我们继续上面的HelloTask,会发现里面有这么一段:
```cpp
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
```
那么问题来了,为何这里要写promise_type?
之所以写promise_type是因为我们在上述2.1节提到了handle,当编译器创建协程的handle时,需要指定该协程对象的promise_type,所以所有的协程对象基本都有一个HelloTask::promise_type结构。仔细的朋友可能就非常懂了,这里传递promise_type过去,在Handle里面会解析出来,从而去判断当前用户自定义的协程对象的行为。
进一步的看看promise_type里面有什么,这里面的内容都是固定的,随着后续的内容越来越多,所涉及的函数也便是越来越多,例如这里使用的是`return_void`,那么当后面使用co_return返回对象时,便会使用另外一个函数了。
**get_return_object**是可以返回当前用户自定义对象的,这里是不做任何操作,承接上文2.1,我们可以使用coroutine_handle来做。
```cpp
struct promise_type {
using Handle = std::coroutine_handle<promise_type>;
HelloTask get_return_object() {
return HelloTask{Handle::from_promise(*this)};
}
// other member function
}
```
可以看到这里使用了coroutine_handle的from_promise成员去构造了一个对象出来。
**initial_suspend**:告诉编译器协程是否应该在第一次执行时保持挂起,或者表示协程实例化时是否保持挂起。
**final_suspend**:与initial_suspend相反,在协程函数体完成后,在协程的末尾有一个final_suspend。这使你有机会进行额外的清理。
这两个都返回awaitable,这里采用了头文件中的suspend_always,当然也可以用户自定义awaitable,这个会在后面文章进行阐述。
suspend_always与suspend_never的源码如下:
```cpp
struct suspend_always
{
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never
{
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
```
这里面包含了几个成员,
- await_ready
- 如果知道结果已经准备就绪是否同步完成?
- await_suspend
- **返回值可以是bool 、void、coroutine handle。**
- 这里返回的是void,则立即将控制返回到当前Coroutine的 caller/resumer (此Coroutine仍然被暂停),否则如果等待_suspend返回bool,true表示控制权交给caller/resumer,表示false恢复当前的coroutine。关于返回coroutine handle在后面文章进行阐述。
- await_resume
- 它的结果是整个co_await表达式的结果。所以返回值不一定是void,还可以是自定义对象之类的。
**return_void**: 当使用`co_return;`时,将调用return_void。
**unhandled_exception**:用于捕获异常。
最后,还差一个创建coroutine一步,根据一开始的定义协程是**任何包含co_return, co_yield或co_await的函数**。因此便有了:
```cpp
HelloTask myCoroutine() {
co_return; // make it a coroutine
}
```
### 2.3 整体流程
整个调用流程是什么呢?本节来详细阐述一下上述代码的执行逻辑,开始之前我们改一下代码,加上一些print操作:
```cpp
#include <iostream>
#include <coroutine>
struct HelloTask {
struct promise_type {
HelloTask get_return_object() { std::cout << "get_return_object" << std::endl; return { }; }
std::suspend_never initial_suspend() { std::cout << "initial_suspend" << std::endl; return {}; }
std::suspend_never final_suspend() noexcept { std::cout << "final_suspend" << std::endl;return {}; }
void return_void() { std::cout << "return_void" << std::endl; }
void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; }
};
};
HelloTask myCoroutine() {
std::cout << "create coroutine" << std::endl;
co_return; // make it a coroutine
}
int main() {
std::cout << "start main" << std::endl;
HelloTask x = myCoroutine();
std::cout << "end main" << std::endl;
}
```
此时输出:
```cpp
start main
get_return_object
initial_suspend
create coroutine
return_void
final_suspend
end main
```
流程如下:
- start main
- 在堆上分配coutine对象,构造promise,调用promise.get_return_object返回当前对象(此时输出了get_return_object),这里这么做的目的是把结果保存到了局部变量里面。
- promise.initial_suspend,此时输出initial_suspend,这里会触发co_await,输出create coroutine,执行协程体后,调用co_return。
- co_return调用promise.return_void 输出return_void。
- 最终,promise.final_suspend输出final_suspend。
- end main
关于协程的相关内容还有特别多的东西,本节为开篇,后续将持续更新~