C++20常用新特性
模块(Module)
从 C 语言中,C++ 继承了 #include 机制,依赖从头文件使用文本形式包含 C++ 源代码,这些头文件中包含了接口的文本定义。一个流行的头文件可以在大型程序的各个单独编译的部分中被 #include 数百次。基本问题是:
- 不够卫生:一个头文件中的代码可能会影响同一翻译单元中包含的另一个 #include 中的代码的含义,因此 #include 并非顺序无关。宏是这里的一个主要问题,尽管不是唯一的问题。
- 分离编译的不一致性:两个翻译单元中同一实体的声明可能不一致,但并非所有此类错误都被编译器或链接器捕获。
- 编译次数过多:从源代码文本编译接口比较慢。从源代码文本反复地编译同一份接口非常慢。
例如:
这段标准代码有 70 个左右的字符,但是在 #include 之后,它会产生 419909 个字符需要编译器来消化。尽管现代 C++ 编译器已有傲人的处理速度,但模块化问题已经迫在眉睫。
1 |
|
所以,在 C++ 程序中改进模块化是一个迫切的需求
C++20 模块化是对传统#include头文件机制的根本性替代,核心作用是「将代码按功能划分为独立模块,实现编译隔离、类型安全导入、编译速度大幅提升」—— 它解决了头文件重复包含、宏污染、编译冗余等几十年的历史问题,是 C++ 构建大型项目的核心基础设施升级
模块化通用定义
1. 核心概念
模块化是软件工程的核心思想,本质是 “分而治之”:
模块:一个独立的功能单元(如 “数学计算模块”“网络请求模块”),包含「内部实现代码」和「对外暴露的接口」;是一个用于在翻译单元间分享声明和定义的语言特性。它们可以在某些地方替代使用头文件。
封装:模块内部的细节对外部不可见,仅通过接口交互;
复用:模块可被多个程序 / 其他模块导入使用,无需重复编写;
解耦:模块间仅通过接口依赖,修改一个模块的内部实现不影响其他模块。
顺序独立性:
import X; import Y;应该与import Y; import X;相同。换句话说,任何东西都不能隐式地从一个模块“泄漏”到另一个模块。
这是
#include文件的一个关键问题。#include中的任何内容都会影响所有后续的#include。顺序独立性是“代码卫生”和性能的关键。
2. 模块化的核心价值(对比 “无模块的面条式代码”)
| 特性 | 无模块化(传统 C++ 头文件) | 模块化(C++20) |
|---|---|---|
| 代码封装 | 无(头文件所有符号全局可见) | 有(仅export的接口对外暴露) |
| 编译效率 | 低(头文件重复包含、重复编译) | 高(模块仅编译一次,复用编译结果) |
| 命名冲突 | 易发生(宏 / 全局符号污染) | 无(模块内符号私有,仅接口可见) |
| 依赖管理 | 文本替换(#include),易循环依赖 |
语义导入(import),天然隔离依赖 |
| 错误排查 | 链接期暴露错误,定位难 | 编译期校验接口,错误提前暴露 |
其主要优点如下:
没有头文件。
声明实现仍然可分离, 但非必要。
可以显式指定导出哪些类或函数。
不需要头文件重复引入宏(include guards)。
模块之间名称可以相同,并且不会冲突。
模块只处理一次,编译更快(头文件每次引入都需要处理,需要通过 pragma once 约束)。
预处理宏只在模块内有效。
模块的引入与引入顺序无关。
创建模块
源文件->添加->新建项->Module
创建***.ixx文件
1 | //创建模块 |
文件分工
| 文件类型 | 后缀 | 核心语法 | 内容职责 |
|---|---|---|---|
| 主接口单元 | .ixx |
export module 模块名; |
聚合所有分区接口(export import :分区名;),可补充少量全局接口声明 |
| 模块接口分区 | .ixx |
export module 模块名:分区名; |
拆分后的子接口声明(如代数 / 几何分区) |
| 模块实现单元 | .cpp |
module 模块名; |
实现主接口 + 所有分区接口的逻辑,可拆分为多个 .cpp(如 math_algebra_impl.cpp、math_geometry_impl.cpp) |
关键语法
| 语法元素 | 作用 |
|---|---|
export module 模块名 |
声明一个可导出的模块,模块文件的第一行必须是此声明 |
import 模块名 |
导入整个模块,可使用模块内所有export符号 |
import <标准库模块> |
导入模块化的标准库(如import <iostream>,替代#include <iostream>) |
export |
修饰符号(函数 / 类 / 变量),表示对外暴露;无export则为模块私有 |
module 模块名:分区名 |
声明模块分区,用于拆分大型模块 |
import :分区名 |
在模块内导入分区,export import :分区名表示对外暴露该分区的符号 |
接口分区
1.解决的问题
当模块的接口非常复杂(比如包含几十个函数 / 类),把所有export声明写在一个接口文件里会导致文件臃肿、维护困难。分区允许将接口拆分为:
- 多个分区接口单元(负责暴露某一部分接口);
- 一个主接口单元(聚合所有分区,对外提供统一的模块入口)。
2. 核心概念
| 单元类型 | 声明语法 | 作用 |
|---|---|---|
| 分区接口单元 | export module 模块名:分区名; |
暴露模块的一部分接口(如 “数学模块的代数部分”) |
| 主接口单元 | export module 模块名; |
聚合所有分区接口,对外提供统一入口。通过 export import :分区名; 语法,将分区接口暴露给模块使用者。 |
| 模块实现单元(可选) | module 模块名; |
为分区接口提供实现(和普通模块实现一致) |
关键语法细节
- 分区接口单元:必须以
export module 模块名:分区名;开头,定义该分区的导出接口。 - 主接口单元:通过
export import :分区名;导入并暴露分区接口;若省略export,则分区为私有,仅模块内可见。 - 外部使用:用户只能导入主模块(
import 模块名;),无法直接导入分区。
匿名命名空间
匿名命名空间是 C++ 中实现「文件级私有」的核心语法,用namespace {}定义的无名称命名空间,其内部的所有符号(函数、变量、类等)仅在当前编译单元(.cpp 文件)可见,等价于给符号加上static(但功能更全面)。
1 | // 匿名命名空间(无名称) |
核心特性
| 特性 | 具体说明 |
|---|---|
| 无名称 | 无法通过命名空间名::符号的方式访问,只能在当前文件内直接使用 |
| 文件级私有 | 符号仅在当前编译单元(.cpp 文件)可见,其他文件(包括同项目的其他.cpp)无法访问 |
| 内部链接 | 匿名命名空间的符号是「内部链接」,编译器会为每个编译单元生成独立的符号实例 |
| 等价于 static(但更优) | 对函数 / 变量而言,namespace { void f(); } 等价于 static void f();,但匿名命名空间可修饰类 / 结构体,static不行 |
对比static:匿名命名空间的优势
static也能实现文件级私有,但仅支持函数和变量,无法修饰类 / 结构体;匿名命名空间是C++ 标准推荐的文件级封装方式,功能更全面:
| 场景 | static |
匿名命名空间 |
|---|---|---|
| 修饰函数 | 支持(static void f()) |
支持(namespace { void f(); }) |
| 修饰变量 | 支持(static int a;) |
支持(namespace { int a; }) |
| 修饰类 / 结构体 | 不支持 | 支持(namespace { class C {}; }) |
| 标准推荐 | C++17 后不推荐(仅兼容) | 标准推荐的文件级封装方式 |
协程(Coroutine)
概念
协程可以理解为用户态的轻量级线程,也常被称作 “协作式子程序”。
- 通俗比喻:如果把操作系统管理的线程比作 “霸道的员工”(操作系统强行切换),那协程就是 “自觉的员工”(自己主动让出执行权)。线程的切换由操作系统内核控制,开销大;而协程的切换完全由程序员 / 代码控制,发生在用户态,几乎没有开销。
- 核心特点:协程可以在执行过程中暂停(挂起),保存当前的执行状态,等需要时再恢复执行,而且切换时不需要陷入内核态,效率远高于线程。
协程与线程区别
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度者 | 操作系统内核 | 程序员 / 编程语言 |
| 切换开销 | 大(内核态切换) | 极小(用户态切换) |
| 资源占用 | 高(MB 级栈空间) | 极低(KB 级栈空间) |
| 并行 / 并发 | 可真正并行(多核) | 单线程内并发 |
| 同步方式 | 需加锁(如 mutex) | 无需锁(协作式 |
C++ 提供了三个方法挂起协程:co_await, co_yield 和 co_return。
C++20协程只是提供协程机制,而不是提供协程库。C++20的协程是无栈协程,无栈协程是一个可以挂起/恢复的特殊函数,是函数调用的泛化,且只能被线程调用,本身并不抢占内核调度。
3 个核心关键字
C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。
| 关键字 | 作用 | 场景 |
|---|---|---|
co_await |
暂停协程,等待表达式完成后恢复 | 等待 IO、延迟、其他协程完成 |
co_yield |
暂停协程并返回值,下次恢复继续执行 | 生成器(如无限序列) |
co_return |
结束协程并返回最终值 | 协程完成后返回结果 |
co_yield some_value: 保存当前协程的执行状态并挂起,返回some_value给调用者co_await some_awaitable: 如果some_awaitable没有ready,就保存当前协程的执行状态并挂起co_return some_value: 彻底结束当前协程,返回some_value给协程调用者
<=> 三向比较运算符
也叫: 三路比较运算符,太空船运算符
<=> 是 C++20 新增的通用比较运算符,核心作用是一次定义,自动生成所有 6 种比较运算符(==、!=、<、>、<=、>=),解决传统比较运算符需要手动重载多个的冗余问题;它返回一个 “比较结果类型”,能直接表示 “小于 / 等于 / 大于” 三种状态。
基本概念
- 符号:
<=>(因形状像太空船得名); - 作用:替代传统的
operator</operator==等重载,一次性处理所有比较逻辑; - 返回值:
- 对于内置类型(int/long 等):返回
std::strong_ordering(强序,如0= 相等、<0= 小于、>0= 大于); - 对于浮点数:返回
std::partial_ordering(偏序,支持 NaN); - 自定义类型:可返回
std::strong_ordering/std::weak_ordering/std::partial_ordering。
- 对于内置类型(int/long 等):返回
核心优势
传统 C++ 中,要让自定义类型支持所有比较,需重载至少 2 个运算符(如==和<),再手动推导其他;而<=>只需重载一次,编译器自动生成所有比较运算符,告别冗余的比较重载:
| 实现方式 | 需要重载的运算符 | 代码量 | 维护成本 |
|---|---|---|---|
| 传统方式 | ==、<(或更多) |
多 | 高 |
| 三向比较运算符 | operator<=> |
少 | 低 |
关键语法规则
基础用法:内置类型
对于 int、double 等内置类型,<=>可直接使用,返回值可直接判断大小:
1 |
|
核心用法:自定义类型重载
对自定义类型重载<=>后,编译器会自动生成==/!=/</>/<=/>=,无需手动实现:
示例:自定义 Person 类,按年龄比较
1 |
|
| 语法细节 | 说明 |
|---|---|
= default |
编译器自动按成员变量 “逐一向比较”(成员声明顺序决定比较优先级),推荐使用; |
| 手动实现逻辑 | 可自定义比较规则(如仅按 age 比较,忽略 name):return age <=> other.age; |
const 修饰 |
重载时必须加const(因为比较不修改对象状态),否则编译报错; |
| 返回值类型 | 无需显式指定,用auto推导即可(编译器自动匹配合适的顺序类型); |
== 的特殊处理 |
若仅重载<=>并= default,编译器会自动生成operator==;若手动实现<=>,需手动重载==。 |
范围ranges
范围库始于 Eric Niebler 对 STL 序列观念的推广和现代化的工作。它提供了更易于使用、更通用及性能更好的标准库算法。
Ranges 是 C++20 对传统 STL 算法的革命性升级,核心是将 “数据容器” 和 “算法” 解耦,通过 “视图(View)” 实现惰性求值、无拷贝的元素变换 / 筛选,让代码更简洁、高效、易读,彻底解决传统 STL 算法需要手写迭代器、冗余临时容器的问题。
核心概念
核心组成:范围(Range)+ 视图(View)+ 适配器(Adapter)
| 概念 | 核心定义 | 特点 |
|---|---|---|
| Range | 可被遍历的对象(容器、数组、生成器等),满足begin()/end()接口 |
所有 STL 容器(vector/string 等)、原生数组都天然是 Range |
| View | 对 Range 的 “轻量级包装”,实现元素的变换 / 筛选,惰性求值(仅遍历时有计算) | 无拷贝、O (1) 构造、不可变 |
| Adapter | 用于修改 Range 的操作(如 filter/transform),通过 ` | `(管道符)链式调用 |
核心特性:惰性求值(Lazy Evaluation)
这是 Ranges 最高效的特性:
- 视图(View)只是 “计算规则” 的描述,定义时不执行任何计算;
- 只有当遍历视图(如 for 循环、
std::ranges::for_each)时,才会逐元素执行变换 / 筛选; - 无临时容器开销:传统 STL 的
copy_if/transform会生成临时容器,Ranges 直接在原数据上按需计算。
常用视图适配器(Views)
C++20 提供了丰富的内置视图,覆盖绝大多数数据处理场景:
| 视图适配器 | 作用 | 示例 |
|---|---|---|
views::filter |
筛选满足条件的元素 | views::filter([](int x){return x%2==0;}) |
views::transform |
变换元素(映射) | views::transform([](int x){return x*x;}) |
views::take |
取前 N 个元素 | views::take (3)(取前 3 个) |
views::drop |
跳过前 N 个元素 | views::drop (2)(跳过前 2 个) |
views::reverse |
反转元素顺序 | views::reverse |
views::slice |
切片(取 [start, end) 区间) | views::slice (2,5)(索引 2-4) |
views::iota |
生成连续整数序列(生成器) | views::iota(1, 6)(生成 1,2,3,4,5) |
案例:
1 |
|
日期和时区
日期库是多年工作和实际使用的结果,它基于 chrono 标准库的时间支持。在 2018 年,它进入了 C++20,并和旧的时间工具一起放在 <chrono> 中。
<chrono>是 C++11 引入的,核心是 “时间点” 和 “时间段”,C++20 新增了日期类型(year/month/day),更易用。
核心概念
不管是旧版本还是 C++20,<chrono>的核心都是 3 个概念,先吃透:
| 概念 | 大白话解释 | 代码示例(C++11+) |
|---|---|---|
| duration(时间段) | 表示 “一段时间长度”(比如 1 小时、3 天) | chrono::hours(1)、chrono::days(3)(C++20)、chrono::seconds(30) |
| time_point(时间点) | 表示 “某个具体时刻”(比如 2026-02-28 10:00) | chrono::system_clock::now()(获取当前时间点) |
| clock(时钟) | 获取时间点的 “工具” | system_clock(系统时间,可转换为日历)、steady_clock(稳定时钟,用于计时) |
C++20新增
新增 1:日历类型(year/month/day)—— 直观操作年月日
这是最核心的升级:直接用year/month/day定义日期,不用再手动转换time_t/tm。
1 |
|
新增 2:时钟增强 —— 获取当前日期更简单
C++20 可以直接把 “当前时间点” 转成year_month_day,不用再绕time_t:
1 |
|
新增 3:时区支持(std::chrono::time_zone)
C++20 新增了标准时区库,不用再依赖第三方库就能处理不同时区(比如 UTC 转北京时间)。
示例:UTC 时间转北京时间(东 8 区)
1 |
|
新增 4:格式化与解析(C++20 std::format)
C++20 的<format>库支持直接格式化<chrono>的时间 / 日期,不用手动拼字符串。
示例 :格式化日期时间
1 |
|
示例 2:解析字符串为日期
把 “2026-02-28” 这样的字符串直接转成year_month_day,不用手动解析:
1 |
|
C++20 vs C++11/17 <chrono> 核心对比
| 功能 | C++11/17 | C++20 |
|---|---|---|
| 日期定义 | 需手动拼 tm 结构体,易出错 | 直接用 year/month/day,直观 |
| 日期加减 | 需手动处理闰年 / 月末,麻烦 | 自动处理边界,直接 + days/months |
| 时区支持 | 无标准支持,需第三方库 | 内置 time_zone,支持时区转换 |
| 格式化 / 解析 | 需手动拼字符串,无标准接口 | 用 std::format/parse,一行搞定 |
| 日历算法(星期 / 闰年) | 需手动计算 | 内置 is_leap ()、weekday () 等工具 |
案例:
1 |
|
格式化
iostream 库提供了类型安全的 I/O 的扩展,但是它的格式化工具比较弱。
另外,还有的人不喜欢使用 << 分隔输出值的方式。
格式化库提供了一种类 printf 的方式去组装字符串和格式化输出值,同时这种方法类型安全、快捷,并能和 iostream 协同工作。
类型中带有 << 运算符的可以在一个格式化的字符串中输出。
1 | string s1 ="C++"; |
std::format是 C++20 替代传统printf/cout拼接的新一代格式化工具,语法类似 Python 的 f-string,更安全、更易读、功能更强,还原生支持 C++20 新类型(如chrono日期时间)。需包含头文件<format>
std::format核心语法
1. 基础用法:占位符{}
- 最简化:
{}按顺序匹配参数; - 指定位置:
{n}匹配第 n 个参数(从 0 开始); - 指定格式:
{:格式符}控制输出样式。
2.常用格式控制(重点)
| 格式符 | 作用 | 示例 | 输出 |
|---|---|---|---|
:.2f |
浮点数保留 2 位小数 | format("{:.2f}", 3.1415) |
3.14 |
:d |
十进制整数 | format("{:d}", 10) |
10 |
:x/:X |
十六进制(小 / 大写) | format("{:X}", 255) |
FF |
:8s |
字符串占 8 个字符(左对齐) | format("{:8s}", "abc") |
“abc “ |
:08d |
整数补 0 到 8 位 | format("{:08d}", 123) |
00000123 |
跨度
越界访问,有时也称为缓冲区溢出,从 C 的时代以来就一直是一个严重的问题。考虑下面的例子:
1 | void func1(int* p, int n) { // n 是什么? |
span<T> 类模板就这样被放到 C++ 核心指南的支持库中。
1 | void func(span<int> a) { // span 包含一个指针和一条大小信息 |
范围 for 从跨度中提取范围,并准确地遍历正确数量的元素(无需代价高昂的范围检查)。这个例子说明了一个适当的抽象可以同时简化写法并提升性能。对于算法来说,相较于挨个检查每一个访问的元素,明确地使用一个范围(比如 span)要容易得多,开销也更低。
std::span
std::span是 C++20 新增的 “轻量级容器视图”,本质是 “指针 + 长度” 的组合,用来安全、高效地访问连续内存区域(数组、vector、C 数组等),无需拷贝数据,还能避免数组越界,彻底替代裸指针 + 长度的不安全写法。
span 核心概念
| 术语 | 大白话解释 | 核心特点 |
|---|---|---|
| std::span | 连续内存的 “视图”(不拥有数据) | 1. 大小:仅占 2 个指针空间(指针 + 长度);2. 无拷贝:只是指向原数据,不复制;3. 安全:编译期 / 运行期可检查越界;4. 灵活:适配所有连续容器(vector、数组、C 数组)。 |
1 |
|
span 核心优势
- 零拷贝:只是视图,不复制原数据,内存开销极小(仅 2 个指针);
- 类型安全:编译期检查容器类型,避免裸指针的类型错误;
- 边界安全:支持越界检查(
at()抛异常、contains()判断),避免数组越界崩溃; - 简化代码:无需手动传递 “指针 + 长度”,函数参数更简洁;
- 兼容所有连续容器:vector、array、原生数组、动态数组都能适配,无需修改函数。
并发
C++ 并发编程的核心是 “多线程同时执行代码”,C++11 开始提供标准化的线程库(<thread>),后续版本持续增强,让开发者无需依赖平台 API(如 Windows 的 CreateThread、Linux 的 pthread),就能写出跨平台、安全的多线程代码。
C++ 中的并发主要靠线程(Thread) 实现:每个线程是独立的执行流,多个线程共享进程的内存空间(全局变量、堆内存),但有自己的栈空间。
C++ 并发的核心演进
| 版本 | 核心特性 | 作用 |
|---|---|---|
| C++11 | <thread>/<mutex> |
基础线程创建、互斥锁(解决数据竞争) |
| C++17 | std::scoped_lock |
更安全的锁(避免死锁) |
| C++20 | std::jthread/ 协程 |
自动析构的线程、轻量级并发(协程) |