第26章 C++20特性

第26章 C++20特性

如果说C++11是C++的文艺复兴,C++17是它的改革开放,那么C++20就是它的工业革命!这一代标准引入了概念(Concepts)、协程(Coroutines)、模块(Modules)等革命性特性,让C++从"拿着锤子找钉子"的莽撞大汉,变成了一个会思考、善表达的优雅绅士。

C++20的口号是:“让模板元编程不再是黑魔法,让并发编程不再是杂技表演。”

本章我们就来逐一揭开C++20的神秘面纱,看看这位"成熟稳重"的新版本C++到底带来了哪些让人眼前一亮(或者说眼前一黑?)的新特性。


26.1 概念(Concepts)与约束

26.1.1 概念的定义与使用

概念(Concepts) 是C++20引入的一种约束模板参数类型要求的机制。你可以把它想象成模板参数的"准入考试"——只有通过了考试(满足特定条件)的类型才能使用这个模板。

打个比方:你开了一家餐厅,招聘厨师时要求"会颠勺"和"能吃辣"。概念就是这两条招聘要求,而具体某个厨师(类型)能不能来上班,就看他满不满足这些要求。

标准概念库

C++20在<concepts>头文件中为我们准备了一系列"预制试卷",也就是标准概念库。常见的包括:

  • std::integral<T>:检查T是否是整数类型
  • std::floating_point<T>:检查T是否是浮点类型(C++23新增,C++20需用std::is_floating_point_v<T>
  • std::movable<T>:检查T是否可移动
  • std::copyable<T>:检查T是否可拷贝
  • std::default_initializable<T>:检查T是否可默认构造
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <concepts>

// C++20: 概念(Concepts)- 约束模板参数的类型要求

// 定义概念:就像写招聘要求
// Numeric概念:必须是整数类型或者浮点数类型,二者满足其一即可
// 注意:std::floating_point 是 C++23 才有的概念!
// C++20 中可以用 std::is_floating_point_v<T> 或仅用 std::integral 来约束
// 以下示例使用 C++20 兼容写法(仅约束整数),实际项目请根据编译器标准版本选择
template<typename T>
concept Numeric = std::integral<T>;  // C++20 中浮点数概念需用 type_traits 或升级到 C++23

// Addable概念:必须是两个T可以相加的类型
// requires表达式是C++20的"复杂问询台",能检查更细致的条件
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;  // T必须支持+运算符,否则这个概念就不满足
};

// 使用概念约束模板参数:相当于在模板函数上加了"入场券检查"
template<Numeric T>  // 只有满足Numeric概念的类型才能调用这个函数
T add(T a, T b) {
    return a + b;
}

template<Addable T>  // 只有满足Addable概念的类型才能调用这个函数
T twice(T x) {
    return x + x;
}

int main() {
    // 调用add函数,传入整数
    std::cout << "add(1, 2) = " << add(1, 2) << std::endl;  // 输出: 3

    // 如果需要同时支持浮点数,C++20中需要另外定义一个概念或使用 requires 表达式
    // C++23 起可直接使用 std::floating_point 概念

    // 如果尝试传入字符串?编译器会直接报错:"你的类型不满足要求,禁止入内!"

    return 0;
}

requires表达式

requires表达式是C++20的"高级侦探",能够检查类型是否具有某些能力。它的语法看起来有点像魔法咒语,但其实很有规律:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <concepts>

// requires表达式:定义更复杂的要求
// 这个Hashable概念检查类型T是否能被哈希(用于unordered_map等容器)
template<typename T>
concept Hashable = requires(T a) {
    std::hash<T>{}(a);  // T必须能被哈希,就像必须有"身份证号"才能办会员卡
};

// 组合要求:用大括号{}包围每个表达式检查,用->指定返回值类型要求
template<typename T>
concept Comparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;  // a==b必须返回bool(或可转换为bool)
    { a != b } -> std::convertible_to<bool>;  // a!=b同理
    { a < b } -> std::convertible_to<bool>;    // a<b也必须返回bool
};

int main() {
    std::cout << "requires expressions allow complex type constraints" << std::endl;
    std::cout << "(requires表达式允许复杂类型约束)" << std::endl;
    
    // 小提示:如果类型不满足概念,编译器会报错,并告诉你具体哪里不满足
    // 这比以前的模板错误信息(动辄几百行)友好多了!
    
    return 0;
}

冷知识:在C++20之前,如果你写了一个模板,传入错误类型,编译器会吐出一大堆天书般的错误信息,从第1行到第999行,看得人头皮发麻。有了Concepts之后,错误信息会精简很多,告诉你"类型T不满足概念X的要求",而不是让你在茫茫错误海洋中自己捞针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <concepts>
#include <vector>

int main() {
    // C++20标准概念库 - 官方"验证工具箱"
    std::cout << std::boolalpha;  // 开启布尔显示模式:true/false而不是1/0
    
    // integral概念:检查是否是整数类型
    std::cout << "std::integral<int>: " << std::integral<int> << std::endl;  // 输出: true(int是整数)
    std::cout << "std::integral<double>: " << std::integral<double> << std::endl;  // 输出: false(double不是整数)
    
    // movable概念:检查是否可移动(能不能用std::move)
    std::cout << "std::movable<std::vector<int>>: " 
              << std::movable<std::vector<int>> << std::endl;  // 输出: true(vector可移动)
    
    // copyable概念:检查是否可拷贝
    std::cout << "std::copyable<std::vector<int>>: " 
              << std::copyable<std::vector<int>> << std::endl;  // 输出: true(vector可拷贝)
    
    // default_initializable概念:检查是否可默认构造
    std::cout << "std::default_initializable<int>: " 
              << std::default_initializable<int> << std::endl;  // 输出: true(int可默认构造)
    
    return 0;
}

26.2 范围库(Ranges)

26.2.1 范围管道操作

范围库(Ranges) 是C++20最令人兴奋的特性之一!它让算法的组合变得像搭积木一样简单有趣。

想象一下:你有一堆积木(数据),想筛选出红色的、堆成塔形的、取前5个。如果用传统方式,你需要写三层循环;如果用STL算法,你需要嵌套调用;而用Ranges,你可以像搭管道一样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    // C++20: Ranges库 - 惰性求值的算法组合
    // 想象一下:这个库让你能像水管工一样连接数据流
    
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 管道操作:数据从左到右流过每个"过滤器"
    // 就像 Unix 的管道符 | 一样,数据被一步步"加工"
    auto result = v 
        | std::views::filter([](int x) { return x % 2 == 0; })  // 第1步:过滤,只保留偶数
        | std::views::transform([](int x) { return x * x; })     // 第2步:转换,每个数平方
        | std::views::take(3);                                   // 第3步:取前3个
    
    std::cout << "偶数的平方(取前3个): ";
    for (int x : result) {
        std::cout << x << " ";  // 输出: 4 16 36
    }
    std::cout << std::endl;
    
    // 对比传统方式:
    // std::vector<int> filtered;
    // std::copy_if(v.begin(), v.end(), std::back_inserter(filtered), 
    //     [](int x){ return x % 2 == 0; });
    // std::vector<int> transformed;
    // std::transform(filtered.begin(), filtered.end(), std::back_inserter(transformed),
    //     [](int x){ return x * x; });
    // ... 看到这里你是不是已经想放弃了?
    
    return 0;
}

26.2.2 惰性求值

Ranges最妙的地方在于惰性求值(Lazy Evaluation)——不在创建视图时计算,只在真正需要结果时才计算。

这就像点外卖:你不饿的时候不会真的去厨房做饭,只有当你开始吃了,系统才开始炒菜。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    // C++20 Ranges的视图是惰性求值的
    // 不在创建时计算,在迭代时才计算
    // 这就像"预览"功能:先看看要处理什么,实际处理要等真正迭代
    
    std::vector<int> v{1, 2, 3, 4, 5};
    
    // 创建视图:这时候只是在"规划路线",并没有真正执行transform
    // 就像你制定旅游计划,但人还在家里坐着
    auto view = v | std::views::transform([](int x) {
        std::cout << "正在转换数字: " << x << std::endl;  // 这个打印暂时不会发生
        return x * 2;
    });
    
    std::cout << "视图已创建,尚未执行转换(你还在沙发上)" << std::endl;
    
    // 现在开始迭代,才真正执行转换操作
    // 这就像你终于出门了,旅游计划开始执行
    std::cout << "===== 第一次迭代 =====" << std::endl;
    for (int x : view) {
        std::cout << "收到结果: " << x << std::endl;
    }
    
    std::cout << "===== 迭代完毕 =====" << std::endl;
    
    return 0;
}

运行结果会显示"正在转换数字"的打印只在迭代时出现,而不是在创建视图时。

生活类比:惰性求值就像自动售卖机——你不投币,商品不会掉出来。Ranges视图就是那台等待你操作的机器,迭代就是投币动作。


26.3 协程(Coroutines)

26.3.1 co_await

协程(Coroutines) 是C++20最"未来感"的特性,简单来说就是"可以暂停和恢复的函数"。想象一下你去餐厅点餐,服务员说"您稍等,我去后厨看看",然后他跑去后厨忙别的了,等菜好了再回来给你上菜——这就是协程的工作模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <coroutine>

// C++20协程是实验性功能,需要编译器支持
// 此示例展示协程语法结构,但实际运行需要特殊编译器配置

// 定义一个协程类型,需要包含promise_type
struct HelloCoroutine {
    // promise_type 是协程的"大管家",负责管理协程的生命周期
    struct promise_type {
        // get_return_object:返回协程的最终结果对象
        HelloCoroutine get_return_object() { return HelloCoroutine{}; }
        
        // initial_suspend:协程开始前的行为
        // suspend_never = 不暂停,立即执行
        // suspend_always = 暂停,等待恢复
        std::suspend_never initial_suspend() { return {}; }
        
        // final_suspend:协程结束后的行为
        std::suspend_never final_suspend() noexcept { return {}; }
        
        // return_void:协程正常结束时返回什么
        void return_void() {}
        
        // unhandled_exception:处理未被捕获的异常
        void unhandled_exception() {}
    };
};

// 定义一个简单的协程函数
HelloCoroutine hello() {
    std::cout << "Hello" << std::endl;
    
    // co_await:挂起协程(是否实际暂停由awaitable对象决定)
    // suspend_never{} 创建一个"永不暂停"的对象,所以这行不会真的暂停
    // (实际使用时这里通常是某个异步操作)
    co_await std::suspend_never{};
    
    std::cout << "World" << std::endl;
}

int main() {
    std::cout << "C++20 coroutines(协程是实验性功能)" << std::endl;
    std::cout << "注意:协程需要编译器flags支持(如 -fcoroutines)" << std::endl;
    // hello();  // 如果编译器支持,实际调用会打印 Hello World
    
    return 0;
}

26.3.2 co_yield

co_yield 用于生成器模式,想象成一个"会自动补货的自动售货机"——每次你拿走一个商品,机器就会自动生产下一个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <coroutine>

// co_yield示例:生成器模式
// 这就像一个"数数机器人",你问它一次,它就往后数一个

struct Generator {
    struct promise_type {
        int current_value = 0;
        
        Generator get_return_object() { 
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; 
        }
        
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        
        // co_yield会调用这个yield_value
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};  // 暂停,等待恢复
        }
        
        void return_void() {}
        void unhandled_exception() {}
    };
    
    std::coroutine_handle<promise_type> handle;
    
    explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    
    ~Generator() { if (handle) handle.destroy(); }
    
    // 获取下一个值并恢复协程
    int next() {
        handle.resume();  // 恢复协程执行
        return handle.promise().current_value;  // 返回yield的值
    }
};

// 一个生成斐波那契数列的协程
Generator fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;  // 吐出当前值,然后暂停
        int temp = a;
        a = b;
        b = temp + b;
    }
}

int main() {
    std::cout << "co_yield 用于生成器模式(懒加载序列)" << std::endl;
    std::cout << "斐波那契数列前10个:" << std::endl;
    
    // 实际上需要完整编译器支持才能运行
    // auto gen = fibonacci();
    // for (int i = 0; i < 10; i++) {
    //     std::cout << gen.next() << " ";
    // }
    
    std::cout << "[示例代码,协程需要完整编译器支持]" << std::endl;
    
    return 0;
}

26.3.3 co_return

co_return 用于协程返回值,就像普通函数的return,但专门给协程用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <coroutine>

// co_return示例:协程返回值

struct AsyncOperation {
    struct promise_type {
        int result = 0;
        
        AsyncOperation get_return_object() { 
            return AsyncOperation{std::coroutine_handle<promise_type>::from_promise(*this)}; 
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        
        // co_return会触发这个return_value
        void return_value(int v) { result = v; }
        void unhandled_exception() {}
    };
    
    std::coroutine_handle<promise_type> handle;
    
    explicit AsyncOperation(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~AsyncOperation() { if (handle) handle.destroy(); }
    
    int getResult() { return handle.promise().result; }
};

// 模拟异步任务
AsyncOperation asyncTask() {
    std::cout << "正在执行异步任务..." << std::endl;
    // 模拟一些操作...
    co_return 42;  // 返回结果
}

int main() {
    std::cout << "co_return 用于协程返回值" << std::endl;
    std::cout << "[协程示例,需要完整编译器支持]" << std::endl;
    // auto op = asyncTask();
    // std::cout << "结果: " << op.getResult() << std::endl;
    
    return 0;
}

26.3.4 协程状态机

协程背后其实是一个状态机(State Machine),由编译器自动生成。协程的每次暂停和恢复,本质上就是在状态机中切换状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

/*
 * 协程状态机解析
 * 
 * 当你写一个协程函数时,编译器会生成一个隐藏的状态机结构:
 * 
 *                        ┌─────────────────────────┐
 *                        │     Created (创建)      │
 *                        │   协程被调用,帧分配     │
 *                        └───────────┬─────────────┘
 *                                    │
 *                        ┌───────────▼─────────────┐
 *                        │  Initial Suspend        │
 *                        │  (初始化暂停点)          │
 *                        │  执行协程体直到首个暂停点 │
 *                        └───────────┬─────────────┘
 *                                    │
 *              ┌────────────────────┼────────────────────┐
 *              │                    │                    │
 *              ▼                    ▼                    ▼
 *    ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
 *    │   Suspended 1   │  │   Suspended 2   │  │   Suspended N   │
 *    │   (暂停状态1)    │  │   (暂停状态2)    │  │   (暂停状态N)    │
 *    │   co_await等待  │  │   co_yield输出  │  │   其他暂停点     │
 *    └────────┬────────┘  └────────┬────────┘  └────────┬────────┘
 *             │                   │                    │
 *             └───────────────────┼────────────────────┘
 *                                 │ resume()
 *                                 ▼
 *                        ┌─────────────────────────┐
 *                        │      Completed          │
 *                        │     (执行完成)           │
 *                        │   销毁协程帧,释放资源    │
 *                        └─────────────────────────┘
 */

int main() {
    std::cout << "协程状态机是编译器自动生成的" << std::endl;
    std::cout << "每个co_await/co_yield/co_return都是一个暂停点" << std::endl;
    std::cout << "协程帧(Coroutine Frame)存储所有局部变量和状态" << std::endl;
    
    return 0;
}

警告:C++20协程目前仍被视为"实验性"功能,只有部分编译器(如GCC 10+、Clang 10+)支持,且需要添加编译标志-fcoroutines。在生产环境中使用前,请确保你的项目确实需要协程,并且已经充分测试。


26.4 模块(Modules)

26.4.1 模块基础概念与动机

模块(Modules) 是C++20最具颠覆性的特性之一,它试图从根本上改变C++代码的组织方式。想象一下:以前的头文件就像餐厅的开放式厨房——食材、调料、半成品都摆在外面;而模块就像密封的外卖盒——你只需要知道里面是什么,不需要看到它是怎么做的。

为什么需要模块?

传统的 #include 预处理方式有几个让人头疼的问题:

  1. 重复编译:同一个头文件可能被包含多次,导致相同的代码被编译多次
  2. 文本替换#include本质上是把头文件内容复制粘贴到源文件,优雅二字与它无关
  3. 无封装性:只要包含了头文件,就能访问其中的所有内容,包括你不希望被看到的

模块的出现就是为了解决这些问题!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// C++20模块:替代头文件的新的代码组织方式
// 优点:
// - 更好的封装:内部实现对使用者完全隐藏
// - 避免重复编译:模块只编译一次,生成 .ifc 接口文件(Interface File Container)
// - 更快的编译速度:模块接口是二进制的,导入更快

// ============ 文件: hello.cpp(模块实现)============
// export module hello;  // 声明这是一个模块,名为"hello"

export module hello;  // 模块声明:告诉编译器"这是一个可以给别人用的模块"

// export关键字:只有export的内容才对模块外可见
export void greet() {
    // std::cout在这只是占位,实际使用时需要导入std模块
    printf("Hello, Modules!\n");  // 模块内部的实现细节用户看不到
}

// ============ 文件: main.cpp(使用模块)============
// import hello;  // 导入名为"hello"的模块

/*
int main() {
    import hello;  // 导入模块
    greet();       // 调用模块导出的函数
    
    // 注意:模块的好处是导入速度比#include快很多
    // 并且用户看不到greet()函数背后的实现细节
}
*/

26.4.2 export关键字

export关键字在模块系统中有两个作用:

  1. export module 模块名; — 声明一个模块
  2. export { 要导出的内容 }; — 导出那些希望对模块外部可见的声明
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// export module:声明模块
export module my_module;  // 声明一个叫 my_module 的模块

// export:导出内容
export int add(int a, int b) {  // 这个函数可以被模块外调用
    return a + b;
}

export class MyClass {  // 整个类都可以导出
public:
    void doSomething();
};

// 不带export的就是模块内部实现,外部看不到
int helperFunction() {  // 这是一个"幕后英雄",模块外无法访问
    return 42;
}

26.4.3 import关键字

import关键字用于导入模块,就像 #include 但更优雅:

1
2
3
4
5
6
7
8
9
// import 模块名; 导入模块
import hello;  // 导入hello模块

// import std.core; 导入标准库模块(C++20支持部分标准库模块化)
// 注意:std::cout等需要导入std.core或std.io

int main() {
    greet();  // 调用hello模块中的函数
}

26.4.4 模块接口单元

模块接口单元(Module Interface Unit) 是模块的"门面",定义了模块对外提供的所有内容。通常放在 .cppm 文件中(C++ Module source file)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ============ 文件: math.cppm(模块接口单元)============
// .cppm 是C++20建议的模块接口文件扩展名

export module math;  // 声明模块

// 导出函数声明
export int add(int a, int b);
export int multiply(int a, int b);

// 导出模板
export template<typename T>
T maxValue(T a, T b) {
    return a > b ? a : b;
}

// 导出类
export class Calculator {
public:
    int add(int a, int b);
    int subtract(int a, int b);
};

// 注意:这里只是声明,具体实现在模块实现单元中

26.4.5 模块实现单元

模块实现单元(Module Implementation Unit) 包含模块的内部实现代码,对外部不可见:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ============ 文件: math.cpp(模块实现单元)============
// 这是math模块的实现,没有export module声明

module math;  // 属于math模块的实现部分

// 实现接口单元中声明的函数
int Calculator::add(int a, int b) {
    return a + b;
}

int Calculator::subtract(int a, int b) {
    return a - b;
}

int add(int a, int b) {
    return a + b;
}

26.4.6 模块分区

模块分区(Module Partitions) 允许把一个模块拆分成多个部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 模块分区语法
export module my_module:part1;  // 声明my_module的part1分区
export module my_module:part2;  // 声明my_module的part2分区

// 主模块声明
export module my_module;

// 主模块可以导入自己的分区
import :part1;  // 导入part1分区
import :part2;  // 导入part2分区

// 使用分区中的内容
export void usePart1() {
    part1Function();  // 调用part1中的函数
}

26.4.7 全局模块片段

全局模块片段(Global Module Fragment) 是一种兼容性机制,允许在模块声明前包含传统头文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module;  // 全局模块片段开始

#include <cstdio>  // 传统头文件,放在这里不影响模块的纯净性
#include <vector>

export module my_module;

// 后续代码都是模块的一部分
export void print() {
    std::printf("Hello from module!\n");
}

26.4.8 私有模块片段

私有模块片段(Private Module Fragment) 允许把一部分实现放在模块接口文件中,但标记为"私有"——外部看不到,但参与模块的编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export module my_module;

// 公有接口
export int publicAdd(int a, int b);

// 私有实现(私有模块片段)
module;  // 私有片段开始
int privateMultiply(int a, int b) {
    return a * b;
}

int helper(int x) {  // 这些都是私有的
    return x + 1;
}

26.4.9 模块与头文件对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <vector>

/*
 * ============ 模块 vs 头文件:世纪对决 ============
 * 
 * 传统头文件方式:
 * ┌─────────────────────────────────────────────────────────────┐
 * │  #include <iostream>                                         │
 * │  #include <vector>                                           │
 * │  #include <string>                                           │
 * │  #include <unordered_map>                                    │
 * │  ... 头文件越多,编译越慢                                      │
 * └─────────────────────────────────────────────────────────────┘
 * 
 * 头文件的"罪行":
 * 1. 文本替换:#include本质上就是复制粘贴,想想你项目中那些被包含
 *    上千次的<iostream>,编译时间就是这样被吃掉的
 * 2. 重复编译:#ifndef/#pragma once只是避免重复编译,但不能避免
 *    重复处理这些头文件的文本内容
 * 3. 无封装:包含头文件后,private/private的成员函数都能被看到,
 *    想保密?门都没有!
 * 
 * 模块的优势:
 * ┌─────────────────────────────────────────────────────────────┐
 * │  import std.core;  // 二进制模块,速度快                       │
 * │  import my_module;  // 模块只编译一次,到处可用                 │
 * └─────────────────────────────────────────────────────────────┘
 * 
 * 模块的"美德":
 * 1. 编译一次,多次使用:模块编译后会生成 IFC(模块接口文件),
 *    下次导入时直接读取,不需要重新解析文本
 * 2. 真·封装:模块外根本看不到模块内的东西,除非你明确export
 * 3. 更快的增量编译:修改实现文件不会导致依赖它的所有文件重新编译
 */

int main() {
    std::cout << "模块是C++20的杀手级特性" << std::endl;
    std::cout << "虽然目前支持度还不完美,但它代表了C++的未来方向" << std::endl;
    
    std::vector<int> v{1, 2, 3, 4, 5};  // 依然是传统头文件方式
    for (int x : v) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

26.5 三向比较运算符<=>

三向比较运算符 <=> 又称"宇宙飞船运算符"(Spaceship Operator),是C++20引入的一个强大特性。它能同时生成六种比较关系==, !=, <, <=, >, >=),大大简化了自定义类型的比较操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <compare>

// 三向比较运算符:<=> 返回一个比较结果对象

struct Version {
    int major;  // 主版本号
    int minor;  // 次版本号
    int patch;  // 补丁版本号
    
    // = default 让编译器自动生成默认的三向比较实现
    // 编译器会自动按照成员声明的顺序进行比较
    auto operator<=>(const Version&) const = default;
};

int main() {
    Version v1{1, 2, 3};
    Version v2{1, 2, 3};
    Version v3{1, 3, 0};
    
    std::cout << std::boolalpha;  // 显示 true/false 而不是 1/0
    
    // 自动获得 == 运算符
    std::cout << "v1 == v2: " << (v1 == v2) << std::endl;  // 输出: true
    
    // 自动获得 < 运算符
    std::cout << "v1 < v3: " << (v1 < v3) << std::endl;    // 输出: true
    // 因为 v1.minor(2) < v3.minor(3),所以 v1 < v3
    
    // 三向比较的结果是一个比较类别(comparison category)类型
    // std::partial_ordering、std::strong_ordering 或 std::weak_ordering
    auto result = v1 <=> v3;
    std::cout << "v1 <=> v3 的结果类型: ";
    if (result < 0) {
        std::cout << "less(小于)" << std::endl;  // 输出: less
    } else if (result > 0) {
        std::cout << "greater(大于)" << std::endl;
    } else {
        std::cout << "equivalent(等价)" << std::endl;
    }
    
    // 默认情况下,== 和 != 是反转的,< 和 > 也是反转的
    std::cout << "v1 != v3: " << (v1 != v3) << std::endl;  // 输出: true
    
    return 0;
}

趣味比喻:三向比较运算符就像一个超级比较器,你问它"这两个版本谁更大?“它不仅回答"v1更小”,还顺便告诉你"如果我给v1加1就刚好等于v3"、“如果v1和v3各减1就是相等的”——好吧我夸张了,但意思就是这个意思。

讲完了"比较",让我们来看看另一个"赋值"相关的重磅特性——指定初始化,让你的初始化代码从此告别"猜位置"的尴尬游戏!


26.6 指定初始化

指定初始化(Designated Initializers) 允许你在创建对象时,通过成员名字而不是位置来初始化成员。这在C语言和Python中早已有之,C++20终于把它引入了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

// 带默认值的结构体
struct Point {
    double x = 0;  // 默认x=0
    double y = 0;  // 默认y=0
};

int main() {
    // C++20: 指定初始化 - 按名字初始化成员
    // 以前你只能这样写:Point p1{1.0, 2.0};
    // 但这样谁能记得住x是第一个还是y是第一个?
    
    // 使用指定初始化,明确告诉编译器每个值给哪个成员
    Point p1{.x = 1.0, .y = 2.0};  // x=1, y=2,清晰明了!
    
    // 只初始化y,x使用默认值
    Point p2{.y = 5.0};            // x=0 (默认), y=5
    
    // 乱序也可以!C语言风格也支持
    Point p3{.y = 3.0, .x = 7.0};  // x=7, y=3
    
    std::cout << "p1: (" << p1.x << ", " << p1.y << ")" << std::endl;  // 输出: (1, 2)
    std::cout << "p2: (" << p2.x << ", " << p2.y << ")" << std::endl;  // 输出: (0, 5)
    std::cout << "p3: (" << p3.x << ", " << p3.y << ")" << std::endl;  // 输出: (7, 3)
    
    return 0;
}

生活场景:指定初始化就像填表格时用"姓名:XXX"、“地址:YYY"而不是"第1栏填XXX,第2栏填YYY”——再也不用担心填错位置了!


26.7 consteval与constinit

C++20引入了两个新的关键字来控制编译时和初始化行为

  • consteval:强制要求表达式必须在编译期求值,否则报错
  • constinit:确保变量在静态初始化阶段被初始化,避免动态初始化顺序问题
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

// consteval: 强制编译期求值
// 就像一个"强迫症老师",必须当场算出答案,不允许抄作业
consteval int compileTimeSquare(int x) {
    return x * x;  // 这个函数必须在编译时就算出结果
}

// constinit: 保证静态初始化
// 就像一个"先到先得"的牌子,确保这个变量第一个被初始化
constinit int globalValue = 42;  // 静态存储期的变量

// 错误示例(编译会失败):
// consteval int badSquare(int x) {
//     return x * x;
// }
// int main() {
//     int n = 5;
//     int result = badSquare(n);  // 错误!n不是常量表达式
// }

int main() {
    // compileTimeSquare(5) 必须在编译时就算出结果
    constexpr int val = compileTimeSquare(5);  // 编译期:5*5=25
    std::cout << "compileTimeSquare(5) = " << val << std::endl;  // 输出: 25
    
    // constinit 变量可以在运行时使用,但保证先于其他静态变量初始化
    std::cout << "constinit globalValue = " << globalValue << std::endl;  // 输出: 42
    
    // 如果尝试这样写(编译失败):
    // int runtimeValue = 10;
    // int bad = compileTimeSquare(runtimeValue);  // 错误!
    
    return 0;
}

记忆口诀consteval = “const” + “eval”(evaluate)= 常量求值,必须编译期完成;constinit = “const” + “init” = 常量初始化,保证静态初始化顺序。


26.8 模板Lambda

模板Lambda 是C++20给Lambda表达式的一项超能力——以前Lambda的参数类型必须是明确的,现在可以用template<typename T>来声明类型参数了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

int main() {
    // C++20: 模板Lambda - Lambda也支持模板参数了!
    
    // 以前(C++17及之前):Lambda的参数类型必须写死
    // auto oldStyle = [](int x) { return x * 2; };  // 只能接受int
    
    // C++20新写法:Lambda可以使用模板参数
    auto generic = []<typename T>(T x) {  // 注意:template<> 要放在 lambda 前面
        return x * 2;
    };
    
    // 现在这个Lambda可以接受任何支持乘法运算的类型!
    std::cout << "generic(5) = " << generic(5) << std::endl;  // 输出: 10
    std::cout << "generic(3.14) = " << generic(3.14) << std::endl;  // 输出: 6.28
    
    // 甚至可以这样玩:
    auto add = []<typename T>(T a, T b) -> T {
        return a + b;
    };
    
    std::cout << "add(100, 200) = " << add(100, 200) << std::endl;  // 输出: 300
    std::cout << "add(1.5, 2.5) = " << add(1.5, 2.5) << std::endl;  // 输出: 4
    
    return 0;
}

想象一下:模板Lambda就像是Lambda的"变身卡",用了它之后,Lambda就可以在调用时根据传入参数的类型自动变身対応,再也不用担心类型不匹配的问题了!


26.9 std::format格式化库

std::format 是C++20引入的格式化库,灵感来自Python的f-string,终于让C++有了像样的字符串格式化!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <format>  // C++20格式化库

int main() {
    // C++20: std::format - Python风格的格式化
    // 告别 sprintf 的不安全,告别 stringstream 的繁琐
    
    // 基础用法:{} 是占位符,按顺序填入参数
    std::cout << std::format("Hello, {}!", "World") << std::endl;  // 输出: Hello, World!
    
    // 数字进制转换
    // :d = 十进制, :x = 十六进制, :b = 二进制
    std::cout << std::format("十进制: {:d}, 十六进制: {:x}, 二进制: {:b}", 42, 42, 42) << std::endl;
    // 输出: 十进制: 42, 十六进制: 2a, 二进制: 101010
    
    // 浮点数格式化
    // :.2f = 保留2位小数
    std::cout << std::format("{:.2f}", 3.14159) << std::endl;  // 输出: 3.14
    
    // 对齐和填充
    std::cout << std::format("{:>10}", "右对齐") << std::endl;  // 右对齐,总宽度10
    std::cout << std::format("{:<10}", "左对齐") << std::endl;  // 左对齐,总宽度10
    std::cout << std::format("{:^10}", "居中对齐") << std::endl;  // 居中,总宽度10
    
    // 带填充字符
    std::cout << std::format("{:0>10}", 42) << std::endl;  // 输出: 0000000042
    
    return 0;
}

感言:用过Python的f"Hello, {name}!"的程序员,在第一次看到std::format时往往会热泪盈眶——终于不用再和sprintf的安全漏洞和stringstream的繁琐语法打交道了!


26.10 日历与时区库

C++20扩展了<chrono>库,添加了完整的日历时区支持,让日期时间处理终于变得现代化!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <chrono>
#include <format>

int main() {
    // C++20: 日历和时区扩展
    // chrono库之前只能处理时间点,现在还能处理日期和时区了
    
    using namespace std::chrono;
    
    // ========== 日历支持 ==========
    
    // 年-月-日
    year_month_day today = floor<days>(system_clock::now());
    std::cout << "今天是: " << std::format("{:%Y-%m-%d}", today) << std::endl;
    
    // 获取星期几
    weekday wd = floor<days>(system_clock::now());
    std::cout << "今天是: " << wd << std::endl;  // 输出类似: Sun/Mon/Tue...
    
    // 日期运算
    auto tomorrow = today + days{1};
    auto lastWeek = today - weeks{1};
    
    std::cout << "明天是: " << std::format("{:%Y-%m-%d}", tomorrow) << std::endl;
    std::cout << "上周今天是: " << std::format("{:%Y-%m-%d}", lastWeek) << std::endl;
    
    // ========== 时区支持 ==========
    
    // 导入IANA时区数据库(需要平台支持)
    // auto tz = current_zone();  // 获取当前系统时区
    // auto now = zoned_time{tz, system_clock::now()};
    // std::cout << std::format("{:%Y-%m-%d %H:%M:%S %Z}", now) << std::endl;
    
    std::cout << "[时区功能示例,需要平台支持]" << std::endl;
    
    return 0;
}

吐槽:在C++20之前,你想在C++里计算"2024年春节是星期几",往往需要借助第三方库或者自己造轮子。现在有了标准日历库,终于可以跟那些"手动查日历"的黑暗岁月说拜拜了!


26.11 std::jthread与同步原语增强

std::jthread 是C++20新增的"自动 join 的 thread"——是的,你没看错,以前的std::thread不会自动等待(join),如果你忘了调用join()detach(),程序可能会崩溃或者产生未定义行为。std::jthread就是来解决这个问题的!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <thread>
#include <chrono>

int main() {
    // C++20: std::jthread - 自动join的线程
    // j = joinable(会自动join的)
    
    // 创建一个jthread
    // std::jthread jt([](int x) {
    //     std::cout << "jthread 开始工作,参数: " << x << std::endl;
    //     std::this_thread::sleep_for(std::chrono::milliseconds(100));
    //     std::cout << "jthread 完成!" << std::endl;
    // }, 42);
    
    // 注意:不需要手动调用jt.join()!
    // 当jt被销毁时(比如离开作用域),析构函数会自动调用join()
    // 就像一个贴心的服务员,你吃完饭不用自己收拾,他会帮你打理一切
    
    std::cout << "std::jthread 在析构时自动join" << std::endl;
    std::cout << "再也不用担心忘记调用 join() 导致程序崩溃了!" << std::endl;
    
    // ========== C++20 同步原语增强 ==========
    // std::counting_semaphore - 信号量
    // std::latch - 一次性栅栏
    // std::barrier - 可重用栅栏
    
    // counting_semaphore 示例
    // std::counting_semaphore sem{2};  // 最多2个并发
    // sem.acquire();  // 获取信号量
    // sem.release();  // 释放信号量
    
    std::cout << "[完整示例需要编译器支持]" << std::endl;
    
    return 0;
}

血泪史:用过std::thread的老兵都知道,最常见的bug就是创建线程后忘记join()——轻则程序卡死,重则OS崩溃。std::jthread就像给线程装上了安全气囊,让多线程编程变得不那么惊心动魄了。


26.12 原子智能指针

原子智能指针 是C++20为std::shared_ptr添加的线程安全包装,让多个线程可以安全地同时访问同一个shared_ptr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <memory>
#include <atomic>
#include <thread>
#include <vector>

int main() {
    // C++20: 原子智能指针
    // std::atomic<std::shared_ptr<T>> 提供了线程安全的共享指针操作
    
    // 创建一个原子shared_ptr
    // std::atomic<std::shared_ptr<int>> atomicPtr = 
    //     std::make_shared<int>(42);
    
    // 线程安全的load(读取)
    // auto ptr = atomicPtr.load();
    
    // 线程安全的store(写入)
    // atomicPtr.store(std::make_shared<int>(100));
    
    // 线程安全的exchange(交换)
    // auto oldPtr = atomicPtr.exchange(newPtr);
    
    // compare_exchange - 线程安全的CAS操作
    // std::shared_ptr<int> expected = std::make_shared<int>(42);
    // atomicPtr.compare_exchange_strong(expected, std::make_shared<int>(100));
    // 如果atomicPtr == expected,则设置为newPtr,返回true
    // 否则,expected被更新为atomicPtr的实际值,返回false
    
    std::cout << "原子智能指针在C++20中提供线程安全的shared_ptr操作" << std::endl;
    
    // 以前多线程操作shared_ptr需要加锁,现在可以直接用原子操作
    // 这对于高性能并发场景(如lock-free数据结构)非常有用
    
    return 0;
}

技术背景:在C++20之前,std::shared_ptr本身是线程安全的(引用计数),但多个线程同时"读写"同一个shared_ptr本身(即一个线程读、一个线程写)需要外部加锁。原子智能指针就是来解决这个问题的!


26.13 std::span

std::span 是C++20引入的"视图类型",它是对一段连续内存的轻量级视图,类似于指针+长度,但没有所有权——就像一个"看房团",只负责看,不负责买。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <span>
#include <vector>
#include <array>

// 使用span作为函数参数,可以接受任何连续内存的"视图"
void printSpan(std::span<int> s) {
    std::cout << "span大小: " << s.size() << ", 内容: ";
    for (int x : s) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    
    // 从vector创建span
    printSpan(v);  // 整个vector:1 2 3 4 5
    
    // 从数组创建span
    int arr[] = {10, 20, 30, 40, 50};
    printSpan(arr);  // 整个数组:10 20 30 40 50
    
    // 从子范围创建span
    // span支持first(n)、last(n)、subspan(offset, count)等视图操作
    printSpan(v.data() + 1, 3);  // v[1]到v[3]:2 3 4
    
    // span本身不拥有数据,只是视图
    // 这对于函数参数特别有用——可以接受数组、vector、甚至string_view
    // 而不需要写一堆重载
    
    // 示例:span的子视图
    std::span<int> fullSpan = v;
    auto firstThree = fullSpan.first(3);  // 前3个元素
    auto lastTwo = fullSpan.last(2);      // 后2个元素
    
    std::cout << "前3个: ";
    for (int x : firstThree) std::cout << x << " ";
    std::cout << std::endl;
    
    std::cout << "后2个: ";
    for (int x : lastTwo) std::cout << x << " ";
    std::cout << std::endl;
    
    return 0;
}

生活比喻:把std::span想象成电影院的座位表——你拿着座位表可以告诉别人"第5排到第10排都是我们的",但座位表本身不是座位,你不能拆了它当柴烧。span就是那个座位表,而真正的座位是底层的数组或vector。


26.14 char8_t与UTF-8支持

char8_t 是C++20新增的字符类型,专门用于UTF-8编码。与char不同,char8_t明确表示"这个字符是UTF-8编码的",避免了歧义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main() {
    // C++20: char8_t 用于UTF-8字符
    // char8_t 是 unsigned char 的别名,但语义完全不同
    
    // char8_t c1 = 'A';  // ASCII字符
    char8_t c2 = u8'中';  // UTF-8中文字符(需要源文件保存为UTF-8编码)
    
    // char8_t 配合 u8"..." 字符串字面量
    const char8_t* u8str = u8"你好,C++20!";  // UTF-8字符串
    
    std::cout << "char8_t 用于明确的UTF-8支持" << std::endl;
    std::cout << "[完整UTF-8示例需要控制台编码支持]" << std::endl;
    
    // C++17及之前,UTF-8字符串用 const char* 表示,容易混淆
    // const char* oldStyle = "你好";  // 不明确是UTF-8还是GBK还是ANSI
    // const char8_t* newStyle = u8"你好";  // 明确是UTF-8
    
    return 0;
}

历史背景:在C++20之前,C++对UTF-8的支持一直很"暧昧"——char既可以表示ASCII,又可以表示UTF-8,还可以表示其他编码,全靠程序员自己心里有数。char8_t的出现终于让UTF-8字符串有了明确的类型标识。


26.15 新属性:[[no_unique_address]][[likely]][[unlikely]]

C++20引入了几个新的属性(Attributes),它们虽然看起来不起眼,但各有神通:

  • [[no_unique_address]]:优化空类的存储
  • [[likely]] / [[unlikely]]:给编译器提供分支预测提示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>

// 空类:不包含任何成员
struct Empty {};

struct WithAttr {
    // [[no_unique_address]] 告诉编译器:
    // "如果Empty类型的大小不影响逻辑,可以把它压缩到"缝隙"里"
    // 如果Empty类型是空类(如std::unique_ptr的特例),编译器可以
    // 把它放在结构体的任何位置,甚至不占用额外空间
    [[no_unique_address]] Empty e;  // 如果编译器选择优化,可能不占空间
    int value;
};

struct WithoutAttr {
    Empty e;  // 至少占用1字节(即使Empty是空类)
    int value;
};

int processPositive(int x) {
    // [[likely]] 告诉编译器:"这个分支很可能是true,优化它"
    // 编译器会把 x > 0 的分支优化得更快
    if (x > 0) [[likely]] {
        return x * 2;
    } else [[unlikely]] {
        return -x;
    }
}

void slowOperation() {
    // [[unlikely]] 告诉编译器:"这个函数很少被调用,可以不那么积极地优化"
    // 用于错误处理、边界情况等
    std::cout << "这是一个很少被调用的函数(慢路径)" << std::endl;
}

int main() {
    std::cout << "sizeof(Empty) = " << sizeof(Empty) << std::endl;  // 输出: 1(空类大小至少1字节)
    std::cout << "sizeof(WithAttr) = " << sizeof(WithAttr) << std::endl;  // 可能输出: 4(优化后)
    std::cout << "sizeof(WithoutAttr) = " << sizeof(WithoutAttr) << std::endl;  // 输出: 8(1+4对齐)
    
    // 演示[[likely]]和[[unlikely]]
    int result = processPositive(10);  // 大概率执行第一个分支
    std::cout << "processPositive(10) = " << result << std::endl;  // 输出: 20
    
    slowOperation();  // 这个函数不太可能经常被调用
    
    return 0;
}

实际应用[[no_unique_address]]对于节省内存很有用,尤其是在容器内部。比如std::optional<T>内部就用到了类似优化。[[likely]]/[[unlikely]]对于性能关键代码很有帮助,比如网络协议处理、游戏引擎等场景。


本章小结

本章我们探索了C++20引入的核心特性,可以分为以下几大类:

模板与类型系统增强

特性作用类比
概念(Concepts)约束模板参数类型,让错误信息更友好模板的"招聘要求"
requires表达式定义复杂的类型约束高级"入职考试"
三向比较运算符自动生成所有比较运算符比较的"一条龙服务"
指定初始化按名字初始化成员填表时的"姓名:XXX"写法
模板LambdaLambda支持模板参数Lambda的"变身卡"

Ranges库

特性作用亮点
范围管道操作|组合算法Unix管道风格
惰性求值按需计算,不立即执行外卖模式:点餐才做

编译期计算

特性作用
consteval强制编译期求值
constinit保证静态初始化顺序

字符串与格式化

特性作用
std::formatPython风格的格式化
char8_t明确的UTF-8类型

其他重要特性

特性作用
协程(Coroutines)可暂停恢复的函数(实验性)
模块(Modules)替代头文件的代码组织方式
std::jthread自动join的线程
原子智能指针线程安全的shared_ptr
std::span连续内存的轻量视图
日历与时区库标准化的日期时间处理
新属性no_unique_address、likely、unlikely

C++20是C++历史上最重要的更新之一。它不仅让代码更安全、更易写,还为C++的未来发展奠定了基础。虽然部分特性(如协程、模块)目前支持度还不完美,但学习和理解它们将帮助你在未来的C++项目中占得先机!

graph TB
    subgraph "C++20 核心特性"
        A[概念 Concepts] --> B[模板增强]
        C[requires表达式] --> B
        D[三向比较运算符] --> B
        E[模板Lambda] --> B
        
        F[范围库 Ranges] --> G[惰性求值]
        
        H[模块 Modules] --> I[编译速度]
        
        J[协程 Coroutines] --> K[异步编程]
        
        L[格式化库] --> M[std::format]
        
        N[编译期计算] --> O[consteval]
        N --> P[constinit]
    end
    
    Q[C++20] --> R[更安全的代码]
    Q --> S[更快的编译]
    Q --> T[更好的性能]
    
    style A fill:#e1f5fe
    style F fill:#e1f5fe
    style H fill:#e1f5fe
    style J fill:#fff3e0
    style L fill:#e8f5e9
    style N fill:#f3e5f5

下一章我们将深入探讨C++23的更多特性,敬请期待!

最后修改 March 30, 2026: 更新 C++ 教程 (da65b52)