Files
张宗平 1053ebf41c 教案基准切换:新版 10 天安排 + 4 天版大纲(含中初级误区审计)
- docs/SCHEDULE_2026_10DAY.md:2026-07-05 新版 10 天安排正式文本,
  取代 TRAINING_PLAN_2026.md 旧执行计划作为课堂设计基准
- docs/teaching/OUTLINE_4DAY.md:4 天讲授 + Day5 综合案例的正式教案大纲,
  含逐天设计、旧主题去向决策(QSS→Day3、JSON→Day4、QThread→Day5 提升)、
  中初级学员误区盲点审计(10 张新坑卡:所有权/编码路径/连接语义三主线)、
  项目就绪度与高阶兴趣钩子评估
- OUTLINE.md 顶部加基准变更横幅,5 天版保留作历史参照与回退预案
- 顺带收编两篇此前未跟踪的自学文档 MODERN_CPP.md / MODERN_CPP_20PLUS.md
  (大纲与幻灯片多处链接它们)
2026-07-05 12:49:32 +08:00

25 KiB
Raw Permalink Blame History

C++20 与以后(C++20 / 23 / 26 概览)

本文是 MODERN_CPP.mdC++11/14/17)的续篇。

范围与基线说明:本仓库的编译基线仍是 C++17(见顶层 CMakeLists.txt),所以本文介绍的是 C++20 起 的「进阶/前瞻」特性——需要在 -std=c++20 / -std=c++23 下编译,是本仓库示例默认能用的版本。每节标注引入版本;附录给出本机 GCC 13.3.0 的实测可用性(不是猜的,每条都编过)。

为什么单列一份:C++20 是继 C++11 之后最大的一次语言更新,四大件(Concepts / Ranges / Coroutines / Modules)体量足够撑一份独立文档。


0. 版本脉络:C++20 是又一次「大爆炸」

版本 年份 定位 标志性内容
C++17 2017 实用增量 optional/variant/string_view/filesystem/结构化绑定
C++20 2020 四大件 Concepts · Ranges · Coroutines · Modules <=> · std::format
C++23 2023 完善与补库 expected · std::print · mdspan · flat_map · ranges 扩展 · 推导 this
C++26 2026(开发中) 暂定 std::execution(Senders/Receivers) · =delete("...") · inplace_vector · 契约/反射(未定)

Stroustrup 评价 C++20:「自 C++11 以来对 C++ 最大的冲击。」

四大件一句话

  1. Concepts(概念)——给模板加约束,错误信息从「天书」变「人话」。
  2. Ranges(范围)——可组合、惰性的区间管道,STL 算法的现代化。
  3. Coroutines(协程)——语言级协程,生成器/异步的统一底座。
  4. Modules(模块)——替代 #include,更快编译 + 更干净的封装。

关于工具链:本文附录 A 给出了本机 GCC 13.3.0 的实测矩阵。结论先行: C++20 几乎全可用C++23 的语言特性多数可用,但不少库(<print>/<mdspan>/<flat_map> 等)要 GCC 14+C++26 在 13.3 上基本用不了(需 GCC 14+ 且开 -std=c++2c)。课堂演示时请按此选用。


1. Concepts:给模板加约束 C++20

1.1 问题:模板错误信息「天书」

一个没有约束的模板,对所有类型都「假装能用」,直到实例化时在最深处失败:

template<typename T>
void print(const T& x) { std::cout << x; }   // 要求 x 可 <<

struct Foo {};
print(Foo{});   // ❌ 报错:operator<< 找不到,外加一长串模板展开

编译器把 operator<< 失败的整条实例化链路倒给你看,真正有用的「Foo 根本不可流式输出」 被淹没在几十行噪声里。Concepts 的目标:在实例化前就用一句人话告诉你类型不达标

1.2 三个能力合在一起:命名谓词 + requires 表达式 + 能力分派

下面是一个能体现 Concepts 全部威力的例子——一个「按类型能力分派」的通用 print int/std::string 直接打印,容器(如 vector)递归逐元素打印,二者靠类型能力自动选对。

#include <concepts>
#include <iostream>
#include <vector>
#include <string>

// 概念 1:可流式输出——探查一个 operator<<,并要求返回类型是 ostream&
template<typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t } -> std::same_as<std::ostream&>;
};

// 概念 2:可迭代——探查成员函数 begin()/end()
template<typename T>
concept Iterable = requires(const T& t) {
    t.begin();
    t.end();
};

// 重载 A:可流式的直接打印(int / std::string / const char* ……)
void print(const Printable auto& x) {
    std::cout << x;
}

// 重载 B:可迭代、但本身不能直接 << 的容器 → 递归逐元素打印
template<typename T>
requires Iterable<T> && (!Printable<T>)
void print(const T& c) {
    std::cout << "[ ";
    for (const auto& e : c) print(e);   // 递归:容器套容器也能打
    std::cout << "] ";
}

int main() {
    print(42);                                  // → A
    print(std::string("s"));                     // → Astring 能 <<,不进 B
    print(std::vector<int>{1, 2, 3});            // → B
    print(std::vector<std::vector<int>>{{1,2},{3,4}});  // → B 递归到 B 再到 A
    // print(Foo{});   // ❌ 编译错误:清晰,见下
}

运行输出:

42
s
[ 1 2 3 ]
[ [ 1 2 ] [ 3 4 ] ]

这个小例子同时展示了 Concepts 的四个关键能力(这正是 add(Number) 那种玩具例子看不到的):

能力 体现在
requires 表达式探查成员 Iterablet.begin(); t.end(); 检查成员函数是否存在
探查运算符 + 返回类型要求 Printable{ os << t } -> std::same_as<std::ostream&>;
能力分派(按类型能力重载) 两个 print 重载靠 Printable / Iterable 自动二选一
概念可参与逻辑运算 requires Iterable<T> && (!Printable<T>)! 排除已处理情况

那个 !Printable<T> 是关键细节std::string 同时满足 PrintableIterable (它既能 << 又有 begin/end)。若不加 !Printable<T>,两个重载都可行且互不约束 → 调用歧义报错。这里要求重载 B 只处理「可迭代但不可流式」的类型,让 std::string 干净地落进重载 A。这是实际工程里反复用到的模式。

1.3 报错终于说人话了

把上面 print(Foo{}) 的注释去掉,错误信息变成:

required for the satisfaction of 'Printable<auto:16>' [with auto:16 = Foo]
note: constraints not satisfied

一句话点出「Foo 不满足 Printable」,不再有模板展开噪声。

1.4 挂载概念的三种写法(用最小的例子演示语法)

上例用了简写模板语法 void print(const Printable auto& x),最清爽。完整三种写法:

template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;   // 组合标准库概念

// ① requires 子句(前置)
template<typename T> requires Number<T>
T add1(T a, T b) { return a + b; }

// ② requires 子句(尾随,适合约束长时)
template<typename T>
T add2(T a, T b) requires Number<T> { return a + b; }

// ③ 简写模板语法(推荐,省掉 typename)
auto add3(Number auto a, Number auto b) { return a + b; }

requires(...) { 表达式序列 } 检查这些表达式是否合法,还能用 -> 概念 附加返回类型要求, 概念本身可由 && || ! 组合(如上例),也可由标准库概念(<concepts> 里的 integralfloating_pointsame_asconvertible_toderived_from…)拼装。

常见的坑

  • 能力重叠 → 歧义:一个类型同时满足多个概念、且无一个更「强」(subsumption)时, 多个重载都可行 → 编译歧义。上面用 !Printable<T> 显式排除就是解法之一。
  • 约束太松仍会回到「天书」:概念只写 template<typename T> 不加 requires, 失败照样从深处爆出来——概念的价值在于把约束前置、命名
  • concept 是零开销:纯编译期谓词,不参与运行期、不产生代码。

2. Ranges:可组合的区间管道 C++20

#include <ranges>
#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    namespace rv = std::views;

    // 管道:惰性求值,不产生中间容器
    auto result = std::views::iota(1)              // 1, 2, 3, ...
                | rv::filter([](int x){ return x % 2 == 0; })  // 偶数
                | rv::transform([](int x){ return x * x; })    // 平方
                | rv::take(5);                     // 取前 5 个 → 4,16,36,64,100

    for (int x : result) std::cout << x << " ";
}

核心概念

  • 视图 (view):轻量、惰性的区间适配器(如 filter/transform/take/drop)。不拷贝数据,遍历时才计算,可 O(1) 拷贝。
  • 管道 |:像 Unix 管道一样把多个适配器串起来,a | f | g
  • ranges 算法std::ranges::sort(v) / std::ranges::find(v, x) 直接吃容器,不用 v.begin()/v.end();还支持投影 sort(people, {}, &Person::age)
  • C++23 扩展views::enumerate(带下标遍历)、views::zip(并行拉链)、views::slide/chunk/stride/adjacent 等(见附录 A 可用性)。

常见的坑

  • 视图是惰性的:filter 里的谓词在遍历时才执行,定义时不跑。
  • 视图不拥有底层容器:容器销毁后视图悬垂(类似 string_view)。
  • iota(1)(无上界)是无限区间,必须配合 take 之类才有穷,否则 for 死循环。

3. Coroutines:语言级协程 C++20

协程是能暂停、再恢复的函数,用三个关键字:co_await(等待异步操作)、co_yield(产出一个值)、co_return(结束)。

#include <coroutine>
#include <iostream>

// 最小生成器(C++20 没有标准 std::generator,需自己写 promise_type
//   C++23 才有 <generator> 直接可用,见附录 A
Generator<int> count_up(int from) {
    while (true)
        co_yield from++;        // 每次调用 next() 唤醒,产出一个数
}

int main() {
    auto g = count_up(10);
    for (int i = 0; i < 3; ++i)
        std::cout << g() << " ";   // 10 11 12
}

核心概念

  • 函数体里出现 co_await/co_yield/co_return 之一,即为协程。
  • 协程返回一个「协程句柄对象」(其类型必须含 promise_type),调用者通过它控制暂停/恢复。
  • 框架只规定机制(如何挂起/恢复、如何把值传出去),策略(调度、I/O)由库或你自己的 promise_type 实现。所以裸 C++20 协程偏底层,实际多用库(CppCoro/ASIO/C++23 的 std::generator)。

常见的坑

  • 协程帧在堆上分配,生命周期与返回的句柄绑定——句柄销毁前协程不能跑完 → 泄漏/悬垂。
  • 协程本身的「调度」要你自己接,标准库不提供线程池或事件循环(C++26 的 std::execution 在补这块,暂定)。

Qt 侧:Qt 也有协程支持(Qt Coroutine / 协程化的信号槽),但课程内异步更常用 QThread + 信号槽,协程属进阶内容。


4. Modules:替代 #include C++20

// ---- greet.cppm(模块接口单元)----
export module greet;
export int hello() { return 42; }

// ---- main.cpp----
import greet;
int main() { return hello(); }

核心概念:模块是语义导入——编译一次后缓存为二进制接口(.gcm 等),下游导入不重复解析,也不受宏污染。相比 #include 的「文本粘贴」,模块带来:

  • 更快编译(无重复解析、无宏展开风暴、头文件顺序无关)。
  • 更好封装export 才对外可见,内部实现不泄漏符号/宏)。

重要的现实情况(务必在课堂上讲清):

  • 模块的语言机制已在 C++20 定稿,但构建系统集成(CMake/Make/Ninja)与各编译器(GCC/Clang/MSVC)的支持在 C++20 周期是「能用但繁琐」GCC 要 -fmodules-ts 并安排模块的预编译顺序、生成 .gcm 缓存;CMake 到 3.28+ 才较好地支持。
  • import std;(直接导入整个标准库)要更晚的工具链(GCC 14+ / Clang 17+ / MSVC 19.36+)才好用。
  • 本机 GCC 13.3 可以编模块,但需要手动编排编译顺序(详见附录 A 的实测说明)。
  • 结论:课堂以「讲机制 + 看语法」为主;要做成可运行示例,需投入构建脚本,超出本仓库「最小示例」范畴。

5. 三向比较 <=> C++20

#include <compare>
struct Point {
    int x, y;
    auto operator<=>(const Point&) const = default;   // 一行生成 <,<=,==,!=,>=,>
};

核心概念<=>("宇宙飞船运算符")做三向比较,返回一个比较类别

  • std::strong_orderingless/equal/greater,全序,如 int
  • std::weak_ordering(相等但不等价,如大小写不敏感的字符串)
  • std::partial_ordering(可能无序,如浮点 NaN

= default 让编译器按成员逐一比较,自动派生全部 6 个关系运算符。也可以=default ==,编译器会反过来补 !=

struct V {
    int x;
    bool operator==(const V&) const = default;   // 自动获得 !=
};

常见的坑

  • double 之间是 partial_ordering(因为有 NaN),a <=> b 的结果可能是 unordered
  • operator<=> 默认生成 ==/!=;但若类含指针或不希望全序,需自己定义。

6. constevalconstinit C++20

consteval int sq(int x) { return x * x; }   // 立即函数:必须在编译期求值
constinit int g = sq(5);                     // 常量初始化(值可在运行期,但初始化在编译期)

int n;
std::cin >> n;
int a = sq(5);        // OK,编译期
// int b = sq(n);     // ❌ n 不是常量,consteval 强制编译期 → 报错

区别(三者别混):

constexpr consteval constinit
修饰对象 变量 / 函数 仅函数 仅变量(静态/线程局部)
编译期求值 可能(能在编译期就在,不能就运行期) 必须(一定是编译期,否则编译错误) 初始化必须是常量表达式
是否 const (函数) 不是(值之后可改,只是初始化在编译期)

constinit 解决什么:防止静态初始化顺序灾难(跨翻译单元的全局对象初始化顺序未定义)。要求初始化是常量表达式,就把对象放进「零开销」的常量初始化阶段,绕开顺序问题。


7. 更多 constexpr:编译期能做更多 C++20

C++20 把 constexpr 的边界大幅推开:

  • constexpr 动态内存分配constexpr std::vectorconstexpr std::string(在常量表达式求值期间分配的内存需在求值结束前释放)。
  • constexpr 函数里可用 try/catchstd::is_constant_evaluated()C++20C++23 升级为 if consteval)。
  • 大量标准库函数(<cmath><algorithm> 的部分)标记为 constexpr
constexpr int build_sum() {
    std::vector<int> v{1, 2, 3, 4};     // C++20constexpr vector
    int s = 0;
    for (int x : v) s += x;
    return s;                            // 析构时释放,编译期合规
}
static_assert(build_sum() == 10);

8. Lambda 与函数对象增强

8.1 模板 lambda C++20

auto f = []<typename T>(std::vector<T> const& v) {
    return v.size();
};

让 lambda 像模板一样把类型当参数,不需要再用 auto 模糊推导——能在参数列表精确写出 std::vector<T> const&

8.2 推导 this(显式对象参数)C++23

struct Builder {
    // self 是显式的「this 参数」,类型可推导 → lambda 能递归、能写 CRTP 式重载
    void log(this auto& self, std::string_view msg) { /* ... */ }
};

// 递归 lambda(以前只能用 std::function 或 Y 组合子):
auto fib = [](this auto& self, int n) -> int {
    return n < 2 ? n : self(n - 1) + self(n - 2);
};

核心概念:把 this 写成显式参数this auto& self),类型可推导。一举多得:递归 lambda、const/非 const 重载统一、消除 CRTP 样板。

本机 GCC 13.3 不支持推导 this(附录 A),演示需 GCC 14+。

8.3 多维 operator[] 与静态 operator() C++23

struct Matrix {
    double operator[](int i, int j) { return data[i * cols + j]; }   // 多维下标
    int data[100], cols;
};
struct Less { static bool operator()(int a, int b) { return a < b; } }; // 静态调用

9. 杂项但常用的语言特性 C++20/23

特性 版本 示例
指定初始化 C++20 Point p{.x = 1, .y = 2};(来自 C,受限形式:须按声明顺序、不嵌套)
范围 for 的初始化语句 C++20 for (auto& c = get(); auto& e : c) { ... }
char8_t C++20 u8"" 的类型从 char 变成独立的 char8_tABI 破坏,老代码要改)
using enum C++20 using enum Color; 把枚举值引入作用域
聚合体 CTAD C++17/20 std::array a{1,2,3}; 推导 array<int,3>
if consteval C++23 替代 std::is_constant_evaluated(),更清晰
=delete("原因") C++26 删函数时带诊断信息(本机 -std=c++2b 已可编译)
位域成员默认初始化 C++20 struct { unsigned a : 3 = 0; };

10. 标准库大件 C++20

10.1 std::formatPython 风格格式化 C++20

#include <format>
std::string s = std::format("x = {}, pi ≈ {:.3f}", 42, 3.14159);

替代 printf(不类型安全)和 iostream(冗长、慢)的中间路线:{} 占位 + 安全的类型检查。

C++23std::print/std::println 让你直接 std::println("x = {}", 42),连 std::format+<< 都省了(本机要 GCC 14+)。

10.2 <chrono> 日历与时区 C++20

using namespace std::chrono;
auto year = 2025y;
auto date = 2025y/January/1d;          // 年/月/日:日历字段(字面量 y / Month / d
auto next_week = sys_days{date} + weeks{1};   // 日期 + 时长
auto dur = days{7} + 12h + 30min;     // 时长:180 小时(注意 days 是复数 = duration

把日历(年月日、星期)、时长、时区都纳入类型系统,编译器帮你抓「天数+小时数」这类单位错误。

坑(极易踩)7d(单数字面量)是 chrono::day 日历字段(一个月里的「几号」), 不是「7 天的时长」。7 天的时长要写 days{7}(复数类型 = duration)或 7*24h。 所以 7d + 12h 不能编译——字段不能和时长相加;只有 sys_days{date} + weeks{1} 这种「日期点 + 时长」才行。

10.3 std::span:连续内存的非拥有视图 C++20

void process(std::span<int> data);    // 接收数组 / vector / 某段 —— 零拷贝、带长度
int arr[4];  std::vector<int> v;
process(arr);  process(v);  process(std::span{v}.subspan(2, 5));

C++ 版的「指针+长度」:栈数组、vectorarray 都能无开销绑定,函数签名终于不必在 T*vector<T>& 之间二选一。

10.4 std::jthread:可停止的线程 C++20

#include <thread>
std::jthread worker([](std::stop_token st){
    while (!st.stop_requested()) { /* ...干活... */ }
});
// worker 析构时自动 join,且可 worker.request_stop() 协作式取消

std::thread 多两点:析构自动 join(不会 std::terminate)、内置协作式取消stop_token)。

10.5 其它 C++20

头文件 用途
<bit> std::bit_cast(类型双关,替代 memcpy 把戏)、popcountcountl_zerorotrbit_ceil
<numbers> std::numbers::piesqrt2…(变量模板)
<source_location> std::source_location::current() 取当前文件/行/函数(替代 __FILE__/__LINE__ 宏)
<version> 标准库特性检测宏
<concepts> 标准概念库:integral/floating_point/same_as/convertible_to
<ranges> 范围与视图
<coroutine> 协程底层设施

11. C++23 速览

C++23 主要是补 C++20 的坑 + 加库

类别 特性 一句话
错误处理 std::expected<T,E> 类型化的「成功/失败」,替代异常或返回码
输出 std::print / std::println std::format 直接打印到流
多维 std::mdspan 多维数组的非拥有视图(GPU/HPC 常用)
容器 std::flat_map / flat_set 有序但连续存储的「扁平」关联容器(缓存友好)
调试 std::stacktrace 运行期取调用栈
optional 单子 and_then / or_else / transform 链式处理「可能无值」
工具 std::to_underlying / std::unreachable / std::byteswap 枚举转整数 / 标记不可达 / 字节反转
范围 views::enumerate / zip / slide / chunk / adjacent / stride 区间管道再扩
语言 推导 this / if consteval / 多维 [] / 静态 operator() 见 §8
// expected:替代异常的「带类型的错误」
std::expected<int, std::string> parse(std::string_view s) {
    if (s.empty()) return std::unexpected("empty");
    return 42;
}
if (auto r = parse("x")) use(*r); else handle(r.error());

// views::enumerate:带下标遍历,告别手动 i++
for (auto [i, v] : std::views::enumerate(items)) { /* ... */ }

12. C++26 展望(暂定,未完全定稿)

状态声明:C++26 标准还在制定中,以下为「候选/已合并」特性,本机 GCC 13.3 基本都还用不了(需 GCC 14+,多数需 -std=c++2c,本机连该标志都不识别)。列在这里是为了让学员建立「C++ 还在持续演进」的认知。

  • std::executionSenders/ReceiversP2300:标准化的异步/并行执行框架,C++ 终于有统一的「调度 + 组合异步操作」底座。
  • = delete("原因"):删函数带诊断信息(本机 -std=c++2b 已能编译,是少数现在能试的)。
  • std::inplace_vector:栈上、容量固定的 vector(不动态分配,适合嵌入式/实时)。
  • std::linalg:基于 mdspan 的线性代数(BLAS 风格)。
  • #embed:把二进制文件直接嵌入源码,替代 xxd/工具链把戏。
  • 打包下标 a[1, 2, 3]:一次取多个元素。
  • 契约(Contracts/ 静态反射(Reflection:呼声很高,但能否进 C++26 仍有变数(多次反复)。

附 A:本机 GCC 13.3.0 实测可用性矩阵

全部为 -std=c++20 / -std=c++23 / -std=c++2b 下的实测结果(-Wall -pedantic-errors)。 可用 / 缺失或需更高版本。

C++20(基本全可用)

特性 结果 备注
Conceptsrequires / 简写 / <concepts>
Rangesviews::ranges::sort、管道)
<=> / <compare>
consteval / constinit
std::format / <format>
std::span / <bit> / <numbers> / <source_location>
std::jthread + stop_token
指定初始化 / 范围 for 初始化语句 / char8_t / using enum
模板 lambda []<typename T>(...)
<chrono> 日历字面量
Modules ⚠️ 语言机制可用,但需手动编排编译顺序与 .gcm 缓存,构建系统集成在 CMake 3.28+ 才较好
Coroutines<coroutine> ⚠️ 机制可用,但 std::generator 要 C++23C++20 需自写 promise_type

C++23(语言特性多数可用,库分两半)

特性 结果 备注
if consteval
多维 operator[]a[i,j]
静态 operator()
std::to_underlying / std::unreachable / std::byteswap
<expected>
std::optional 单子(and_then/transform/or_else
views::enumerate / zip / slide
推导 thisthis auto& self 需 GCC 14+
<print> / std::println 需 GCC 14+
容器 std::formatformat("{}", vector) 需 GCC 14+
<mdspan> / <flat_map> / <inplace_vector> 需 GCC 14+
<stacktrace> ⚠️ 头文件存在,但需链接 -lstdc++_exp,本机无该库 → 实际不可用
std::generator 需 GCC 14+

C++26(本机基本不可用)

特性 结果 备注
-std=c++2c / -std=c++26 标志 本机 GCC 13.3 不识别该标志
= delete("原因") -std=c++2b 下可编译(少数能试的)
std::execution / inplace_vector / #embed / 契约 / 反射 全部需 GCC 14+,多数尚未实现

附 B:学习路径建议

  1. 先吃透 C++17(见 MODERN_CPP.md):移动、智能指针、lambda、optional/variant 是 C++20 的前提。
  2. C++20 入门顺序Concepts → Ranges → <=>std::format/span/jthreadModules 和 Coroutines 放最后(构建/调度复杂度高)。
  3. C++23 实用件(你的工具链够新时):expectedstd::printviews::enumerate——日常工程立刻能受益。
  4. C++26 持续关注,不必急着用——标准与实现都还在收敛。
  5. 权威参考cppreference.comC++ 标准委员会邮件列表、Sutter《Welcome to the Jungle / 历年 trip report》。

附 C:与本仓库的关联

  • 本仓库基线 C++17:本文件所有特性都超出基线,需显式开 -std=c++20+;仓库默认构建(cmake -DCMAKE_BUILD_TYPE=Debug)不会触及。
  • 如需在课堂上跑 C++20 演示:可在 tools/demo_versions/ 加一个 -std=c++20 的目标(需在生成器/CMake 里单独加 COMPILE_OPTIONS),与现有 std_probe.py 跨版本对比机制衔接。这属于新增工程改动,需要你明确指令我才执行。
  • 跨版本探针tools/std_probe.py 已支持把片段喂给任意 -std=,可用它复现本文附录 A 的矩阵。
  • Qt 侧Qt 6 对 C++17 是基线、对 C++20 部分采用;本仓库用的隔离 Qt 5.14.2 以 C++17 为准,C++20 特性暂不混入 p03/ 生成产物。