咱们这个教程主线采用的 C++20 标准,是继 C++11 之后改动规模最大的一次重要升级。
它在模板类型检查、容器算法、字符串格式化和比较运算等高频场景中提供了一批现代工具,解决了很多过去写起来繁琐或报错难懂的老问题。
C++20 新特性概览
C++20 在语言基础语法与标准库两个层面都做了大幅扩充,日常开发最常接触的 6 项代表性特性如下表所示。
| 代表特性 | 所属类别 / 头文件 | 主要变化与作用 |
|---|---|---|
| Concepts(概念) | 语言特性 / <concepts> | 在编译期对模板参数提出明确的类型要求,让报错信息直白易读 |
| Ranges(范围库) | <ranges> / <algorithm> | 支持算法直接接收整个容器,并提供管道符(|)串联过滤与转换 |
| std::span | <span> | 提供不拥有内存的连续序列视图,可统一接收原生数组、array 与 vector |
| std::format() | <format> | 使用 {} 占位符按格式拼接字符串,兼顾可读性与类型安全 |
| 三路比较运算符(<=>) | 语言特性 / <compare> | 引入太空船运算符 <=> 与 = default,自动生成全套 6 个比较运算符 |
| 协程(Coroutines) | 语言特性 / <coroutine> | 引入 co_await、co_yield、co_return 关键字,支持函数暂停与恢复执行 |
Concepts 约束与 Ranges 范围库
在编写模板函数和处理容器数据时,C++20 带来了 Concepts 和 Ranges 两项非常直观的升级。
1. 模板概念约束(Concepts)
以前我们编写普通函数模板时,如果调用者传错了数据类型,编译器往往会输出一大段晦涩难懂的深层模板报错,排查起来很费劲。
在 C++20 中,<concepts> 头文件引入了 Concepts 概念约束,支持我们直接把类型要求写在模板声明上。一旦传入的实参不符合要求,编译器就会在调用位置给出直白的错误提示。下面我们来看一个例子。
示例 1:约束模板参数为整数
#include <concepts>
#include <iostream>
// 约束模板参数 T 必须是整数类型
template <std::integral T>
T addInt(T a, T b) {
return a + b;
}
int main() {
std::cout << addInt(10, 20) << '\n';
std::cout << addInt(100L, 200L) << '\n';
return 0;
}运行结果如下。
30
300分析:
在这个例子中,template <std::integral T> 规定函数模板 addInt() 只接收 int、long 等整数类型。代码里 100L 末尾的 L 表示这是一个 long 长整型字面量,同样满足整数约束。
如果我们在调用时误写成 addInt(1.5, 2.3) 传入小数,编译器会直接指出 double 不满足 std::integral 约束,一眼就能定位问题所在。
2. 范围算法与视图(Ranges)
以前调用 STL 算法排序时,我们每次都必须手动传入头尾两个迭代器,比如写成 std::sort(nums.begin(), nums.end())。
到了 C++20,标准库新增的范围库不仅支持写成 std::ranges::sort(nums) 直接传入整个容器,还支持使用竖线管道符(|)将多个筛选与转换操作串联起来。我们来看一个简单例子就懂了。
示例 2:排序容器并筛选偶数
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 6, 3};
// 直接对整个容器升序排列
std::ranges::sort(nums);
// 使用管道符筛选偶数
auto evens = nums | std::views::filter([](int n) {
return n % 2 == 0;
});
std::cout << "排序后的偶数:";
for (int n : evens) {
std::cout << n << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
排序后的偶数:2 6 8分析:
std::ranges::sort(nums); 这句代码表示直接对整个 nums 容器进行升序排列,排完后元素变为 1, 2, 3, 5, 6, 8,省去了手动书写 .begin() 和 .end() 的步骤。
下方的 nums | std::views::filter(...) 中,竖线 | 就像一根连接水管,把左边的元素依次传给右边的筛选器。它不会额外分配内存去复制新容器,而是在遍历 evens 时按需挑出偶数。
连续序列视图:std::span
当我们编写只读遍历函数,既想接收原生数组 int arr[],又想接收 std::vector<int> 或 std::array<int, N> 时,过去往往得重复编写多个重载函数。
在 C++20 中,<span> 头文件新增了 std::span 连续序列视图。它就像一个通用的 “观察窗口”,自己不复制底层数据,只记录连续内存的起始位置和元素个数。

借助 std::span,我们只需要把函数形参写成 std::span<const int>,就能用同一份函数统一接收各种连续存储的序列。请看下面例子。
示例 3:统一接收数组与 vector
#include <iostream>
#include <span>
#include <vector>
// 统一接收连续的 int 序列
void printSpan(std::span<const int> data) {
for (int n : data) {
std::cout << n << ' ';
}
std::cout << "(共 " << data.size() << " 个元素)\n";
}
int main() {
int rawArr[3] = {10, 20, 30};
std::vector<int> vec = {100, 200, 300, 400};
printSpan(rawArr); // 传入原生数组
printSpan(vec); // 传入 std::vector
return 0;
}运行结果如下。
10 20 30 (共 3 个元素)
100 200 300 400 (共 4 个元素)分析:
在上面例子中,printSpan() 只写了一份代码,就能同时接收原生数组 rawArr 和动态数组 vec。并且 data 自带 size() 方法获取元素个数,不需要再像以前传递原生数组那样额外多传一个长度参数了。
这里之所以把形参写成 std::span<const int>,是因为函数内部只读取元素而不做修改,加上 const 既安全又能兼容只读容器。
字符串格式化:std::format
在拼接包含多个变量的字符串时,传统的 printf() 一旦写错格式符很容易导致运行出错,而连续写一长串 << 又会把一句话切得很碎。
在 C++20 中,<format> 头文件提供了 std::format() 函数,采用花括号({})作为占位符,兼顾了可读性与类型安全。
示例 4:按占位符格式化字符串
#include <format>
#include <iostream>
#include <string>
int main() {
std::string name = "Jack";
int score = 95;
auto text = std::format("姓名:{},得分:{}", name, score);
std::cout << text << '\n';
return 0;
}运行结果如下。
姓名:Jack,得分:95分析:
在这个例子中,std::format() 会按顺序把 name 和 score 的值填入格式字符串的 {} 占位符中,并返回拼接好的 std::string 字符串 text,比使用 + 号逐个转换再拼接清晰得多。
三路比较运算符(<=>)
三路比较运算符(<=>)定义在 <compare> 头文件中,因为外形很像飞船,常被称为 “太空船运算符”。a <=> b 只需要比较一次,就能同时区分出 “小于”、“等于” 和 “大于” 这 3 种大小关系。
在实际开发中,它最实用的地方是简化自定义类型的运算符重载。以前我们想让结构体支持全部比较操作,得手动把 ==、!=、<、<=、>、>= 全写一遍,现在只需声明一行默认的 operator<=> 即可。
示例 5:自动生成全套比较运算
#include <compare>
#include <iostream>
struct Point {
int x;
int y;
// 自动生成全套 6 个比较运算符
auto operator<=>(const Point& other) const = default;
};
int main() {
Point p1 = {1, 2};
Point p2 = {1, 5};
if (p1 < p2) {
std::cout << "p1 小于 p2\n";
}
if (p1 != p2) {
std::cout << "p1 与 p2 不相等\n";
}
return 0;
}运行结果如下。
p1 小于 p2
p1 与 p2 不相等分析:
这行 operator<=> 声明拆开看其实很好理解:形参 const Point& other 用来接收另一个参与比较的对象,括号后面的 const 保证比较时不修改自身数据。末尾的 = default; 则表示交由编译器按成员声明的先后顺序依次比较,也就是先比较 x,x 相等时再比较 y,同时自动配齐 ==、< 等全套 6 个比较运算符。
在这个例子中,p1.x 与 p2.x 都是 1,而 p1.y 小于 p2.y,因此 p1 < p2 和 p1 != p2 都成立。
小伙伴们要注意:p1 <=> p2 本身返回的是 std::strong_ordering 等专用比较结果对象,不是普通的 int 整数,不能直接放进 std::cout << 打印。平时像上面这样声明 = default; 后,直接使用 <、==、!= 判断即可。
协程简介
普通函数一旦调用,就会一口气从头执行到 return 结束。而协程(Coroutines)则像夹着书签看长篇小说:它可以在执行中途暂停并交回控制权,等稍后条件就绪时再从暂停位置恢复执行,常用于高并发网络编程与异步任务调度。
在 C++20 中,标准为协程新增了以下 3 个专用关键字,以后小伙伴们在阅读项目代码时看到它们,就能认出这是一个协程函数:
- co_await:暂停当前协程,等待某个耗时或异步操作完成后再恢复执行。
- co_yield:暂停当前协程并向外产生一个值,常用于按需生成序列数据的生成器。
- co_return:结束当前协程并返回结果,协程函数内部不能使用普通的
return语句返回值。
由于 C++20 只在 <coroutine> 头文件中提供了底层框架接口,从零手写一个可运行的协程返回类型需要编写不少支撑代码。实际开发中我们通常会搭配现成的异步网络库来使用,现阶段大家先认清这 3 个关键字的作用即可。
常见问题
1. 本地编译 C++20 代码时提示找不到 <format> 或 <concepts>,应该怎么解决?
部分编译器默认仍停留在 C++17 标准,我们在命令行编译时记得加上 -std=c++20 参数。如果使用的是 Visual Studio,可以在项目属性中将 C++ 语言标准切换为 C++20。
此外,<format> 标准库在编译器中的落地时间稍晚,建议搭配 GCC 13、Clang 17 或 MSVC 2022 及以上版本编译。
2. 常听说的 C++20 模块(Modules,即 import 语法),为什么入门教程里很少使用?
模块(Modules)是 C++20 引入的另一项大特性,目标是逐步减少传统 #include 头文件的重复解析开销,加快大型项目的编译速度。
不过它对构建工具(如 CMake)和源文件编译顺序的配置要求较高,目前主流开源库与日常开发仍以 #include 头文件为主,初学阶段我们先了解有这个概念即可。
