C++20 新特性
C++20 是一个 major 版本,有四个最重要的特性——"The Big Four":模块(Modules)、概念(Concepts)、协程(Coroutines)、范围(Ranges)。此外格式化库、日历库、<=>、constexpr 的大幅扩展也都在这一版落地。本文逐项展开,附可运行的示例与原理说明。
编译器支持情况见 Compiler support for C++20。系列总览见 现代 C++ 新特性汇总。
语言特性
模块
模块用真正的「接口」取代头文件的文本包含:编译一次生成二进制接口文件(BMI/CMI),后续导入不再重复解析:
// math.cppm —— 模块接口单元
export module math;
export int add(int a, int b) { return a + b; } // export 的才对外可见
int internal_helper() { return 0; } // 未导出:模块私有
// main.cpp
import math;
int main() { return add(1, 2); } // 无需任何头文件 它要解决的三个老问题:
| 头文件的问题 | 模块的答案 |
|---|---|
| 宏会泄漏污染使用方 | 模块不导出宏,天然隔离 |
| 同一头文件被重复解析成百上千次 | 接口编译一次,导入是读预编译产物 |
#include 顺序影响含义 | import 顺序无关 |
还有两个进阶机制:import <iostream>;(头文件单元,渐进迁移用)和 module :private 分区(接口与实现分离)。
概念
Concepts 是对模板参数的命名约束,错误信息从几页难以定位的模板展开变成一行「不满足约束 X」:
#include <concepts>
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T> // 用法一:直接约束模板参数
T average(const std::vector<T>& v);
template<typename T> requires Number<T> // 用法二:requires 子句
T sum(const std::vector<T>& v);
template<typename T>
concept Iterable = requires(const T& t) { // 用法三:requires 表达式,定义式约束
t.begin();
t.end();
typename T::value_type; // 要求有成员类型
{ t.size() } -> std::convertible_to<std::size_t>; // 要求表达式及返回类型
}; 比 SFINAE/enable_if 强的地方还在于可以按约束重载——更受限的候选胜出:
template<std::integral T> T calc(T x) { /* 整数算法 */ }
template<std::floating_point T> T calc(T x) { /* 浮点算法 */ } 标准库预置了 <concepts> 里的一大批:same_as、convertible_to、integral、equality_comparable、invocable、ranges::range 等。
协程
三个新关键字:co_await(挂起等待)、co_yield(产出值并挂起)、co_return(返回)。包含任一关键字的函数就是协程——可以被挂起并稍后恢复的函数。
C++20 提供的是底层机制(协程帧、promise_type 协议),不提供开箱即用的 task 类型;标准库的易用封装 std::generator 要等 C++23:
#include <coroutine>
template<typename T>
struct Generator {
struct promise_type {
T value_;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; } // 惰性启动
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) { value_ = v; return {}; } // 交出值并挂起
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> h_;
explicit Generator(std::coroutine_handle<promise_type> h) : h_(h) {}
~Generator() { if (h_) h_.destroy(); }
bool next() { h_.resume(); return !h_.done(); }
T value() const { return h_.promise().value_; }
};
Generator<int> counter(int n) {
for (int i = 0; i < n; ++i)
co_yield i; // 每次交出一个值就挂起
}
// 使用:像迭代器一样按需拉取
auto g = counter(5);
while (g.next()) std::cout << g.value() << ' '; // 0 1 2 3 4 执行模型(生成器这类惰性协程):
原理:协程被编译器改写为状态机,局部变量保存在堆上的协程帧里;挂起点之间就是普通代码。价值:把异步回调、惰性序列、状态机写成「看起来顺序」的代码。
可指定的初始值设定项
C99 指定初始化的受限版本:按成员名初始化,跳过的取默认值:
struct Options {
int threads = 1;
bool verbose = false;
int depth = 10;
};
Options o{.threads = 8, .depth = 20}; // verbose 取默认 false
// 限制(比 C99 严):
// 必须按声明顺序:{.depth = 20, .threads = 8} 编译错误
// 不能混合指定与非指定初始化
// 不能用于聚合之外的类型(含用户构造函数的不行) <=>(三向比较操作符)
一次定义生成全套比较运算符,三路比较返回「小于/等于/大于」三态结果:
struct Version {
int major, minor;
auto operator<=>(const Version&) const = default; // 生成 < <= > >=
bool operator==(const Version&) const = default; // == 需要单独定义
};
static_assert(Version{1, 2} < Version{1, 10}); // 字典序比较成员
int c = (a <=> b); // 三态:负 / 0 / 正
// 标准库比较工具直接消费三态:
auto ord = std::strong_order(a, b); 三态结果的类型分三档,表达不同的比较语义:
| 类别 | 语义 | 典型例子 |
|---|---|---|
std::strong_ordering | 全序且等价即可替换 | int:a == b 则 a、b 完全等同 |
std::weak_ordering | 全序,等价不代表可替换 | 忽略大小写的字符串:"AB" 等价 "ab" 但内容不同 |
std::partial_ordering | 存在"不可比较" | float:与 NaN 比较无序 |
注意:默认 == 不会从 <=> 自动合成(委员会认为 a == b 应该可单独高效实现),所以两者都要写,都 = default 即可。
按值捕获 *this 与 [=] 的弃用
[*this] 按值捕获本身是 C++17 引入的(P0018R3,见 C++17 新特性);C++20 的实际变化是弃用 [=] 隐式捕获 this(P0806R2):
struct Widget {
int value{};
auto tasks() {
// C++17:显式写法
auto by_ref = [this] { return value; }; // 捕获指针,对象销毁后悬垂
auto by_val = [*this] { return value; }; // 拷贝整个对象进闭包,安全
// C++20 起应避免:
// auto implicit_this = [=] { return value; }; // 弃用:= 隐式捕获了 this
auto fixed = [=, this] { return value; }; // 明确写出 this,不再告警
return std::pair{by_ref(), by_val()};
}
}; 标准属性 [[no_unique_address]]、[[likely]] 和 [[unlikely]]
template<typename Char>
struct Buffer {
[[no_unique_address]] std::allocator<Char> alloc; // 空类型不占成员空间
Char* data{};
std::size_t size{};
};
// sizeof(Buffer<char>) == sizeof(void*) + sizeof(size_t),allocator 是空类被优化掉
if (cache_hit) [[likely]] {
fast_path(); // 提示编译器此分支大概率执行(影响代码布局/预测)
} else [[unlikely]] {
slow_path();
} [[no_unique_address]] 是带状态分配器、空比较器等场景的关键优化;两个分支属性只是提示,不影响语义。
constexpr 函数中的更多功能
constexpr 里允许 new/delete、union、try-catch、dynamic_cast、typeid:
constexpr int sum_first(int n) {
auto* p = new int[n]{1, 2, 3, 4, 5, 6}; // 编译期允许 new
int s = 0;
try {
for (int i = 0; i < n; ++i) s += p[i];
} catch (...) { /* constexpr 求值中抛异常仍会失败,但语法上允许 */ }
delete[] p; // 瞬态分配:编译期求值内必须释放
return s;
}
static_assert(sum_first(3) == 6, ""); 「瞬态分配」(transient allocation)规则:编译期求值中分配的内存必须在同一求值内释放,不能漏到运行期——保证了 constexpr 结果可复现。
consteval 函数
consteval 声明立即函数:只允许编译期调用,比 constexpr 更严格:
consteval int square(int x) { return x * x; }
constexpr int a = square(5); // OK:编译期
int n;
std::cin >> n;
// int b = square(n); // 编译错误:运行期实参不许进立即函数 用途:编译期表格、哈希、配置校验——凡是「绝不允许落到运行期」的计算,用 consteval 把违反变成编译错误(constexpr 版本会静默退化为运行期调用)。
constinit 变量
constinit 保证变量静态初始化(编译期完成),杜绝静态初始化顺序问题(SIOF):
struct Table {
int data[256];
constexpr Table() : data{} {
for (int i = 0; i < 256; ++i) data[i] = i * i;
}
};
constinit Table squares; // 编译期就初始化完成,零运行期构造
// 若类型的初始化无法常量求值 -> 直接编译错误,而不是悄悄变成动态初始化
// constinit 不要求 const:之后仍可修改
// 与 constexpr 的区别:constexpr 必须 const,constinit 只约束初始化时机
constinit std::atomic<int> init_flag{0}; // 原子变量编译期初始化,启动即安全 using 用于带作用域的 enum
using enum 把枚举值引入当前作用域,写起来少前缀:
enum class Color { red, green, blue };
void draw(Color c);
void demo() {
using enum Color; // 作用域内生效
Color c = red; // 不用写 Color::red
draw(green);
} 典型用途:在 switch 处理枚举的函数里统一引入,一堆 case Color::red 变成 case red。
一些小的扩展
// char8_t:u8 字面量的新独立类型(与 char 彻底分开)
char8_t c = u8'a'; // C++17 时是 char,C++20 起是 char8_t
// std::u8string 等随之出现
// lambda 模板参数:lambda 也能写真正的模板形参
auto first_of = []<typename T>(const std::vector<T>& v) -> std::optional<T> {
if (v.empty()) return std::nullopt;
return v.front();
};
// 比泛型 lambda(auto 形参)强在能拿到类型 T 本身:
auto size_of_elem = []<typename T>(const T&) { return sizeof(T); }; 标准库特性
范围、视图和管道
<ranges> 把「容器+迭代器」升级为「范围(range)」体系:惰性视图用管道符 | 串联,按需拉取,不产生中间容器:
namespace rv = std::views;
auto evens_sq = rv::iota(1)
| rv::filter([](int x) { return x % 2 == 0; })
| rv::transform([](int x) { return x * x; })
| rv::take(5);
// 到这里什么都没算(惰性)
for (int x : evens_sq)
std::cout << x << ' '; // 4 16 36 64 100
// 算法也有 ranges 版:直接吃容器,还支持"投影"
std::ranges::sort(people, {}, &Person::age); // 按 age 排,第三个参数是投影
auto found = std::ranges::find(v, 42); // 不用手传 begin/end 关键概念:view 是不拥有数据的惰性范围;管道从左到右组合;投影(projection)让算法按成员字段操作而不需要写 lambda 解包。
printf() 风格的格式化:format() 和 vformat()
<format> 提供类型安全的 {} 占位格式化(Python 风格),替代 printf 的不可检查与 iostream 的冗长:
#include <format>
std::string s = std::format("{} scored {:.1f}", "tom", 92.45); // "tom scored 92.5"
std::string t = std::format("{1} then {0}", a, b); // 位置参数
std::string u = std::format("{:*>10}", 42); // "********42"(填充对齐宽度)
std::string v = std::format("{:+.3e}", 1234.5678); // "+1.235e+03"
// vformat:格式串运行期才确定时用(format 的 v 前缀系列)
// 用户输入的模板:std::vformat(user_template, std::make_format_args(x, y)); C++23 起在其上继续建了 std::print/std::println,见 C++23 新特性。
日历和时区
<chrono> 加入人类可读的日历类型与时区支持:
using namespace std::chrono;
using namespace std::chrono_literals;
// 日历类型:年/月/日可拆可组
auto today = 2026y / September / 4d; // year_month_day
auto leap = 2024y / February / last; // 闰年 2 月最后一天:29
auto weeks_in_month = std::chrono::year{2026} / August / std::chrono::Sunday[2]; // 第 2 个周日
// 日期算术
sys_days deadline = year{2026} / 10 / 1;
auto days_left = duration_cast<days>(deadline - floor<days>(system_clock::now()));
// 时区
auto now_utc = std::chrono::system_clock::now();
auto now_sh = std::chrono::zoned_time{"Asia/Shanghai", now_utc};
auto now_nyc = std::chrono::zoned_time{"America/New_York", now_utc};
std::cout << now_sh << '\n' << now_nyc; // 同一时刻的本地表示 span
<span> 是对连续内存的读写视图(string_view 的泛化版),函数签名的「我要一段连续数据」从此有明确表达:
void process(std::span<const int> data) { // 只读:长度随传
for (int x : data) use(x);
}
std::vector<int> v{1, 2, 3};
int arr[]{4, 5, 6};
process(v); // vector:OK
process(arr); // 原生数组:OK
process({v.data(), 2}); // 指针+长度:OK
// std::list 不能转 span:不连续
std::span<int, 3> fixed{v.data(), 3}; // 定长 span,大小进类型
// 传 span 而不是 const vector& 的好处:不强制调用方拥有容器 source_location
<source_location> 取调用位置信息,替代 __FILE__/__LINE__ 宏:
#include <source_location>
void log(std::string_view msg,
const std::source_location& loc = std::source_location::current()) {
// 注意:current() 作为默认实参,取到的是"调用方"的位置
std::clog << loc.file_name() << ':' << loc.line()
<< " [" << loc.function_name() << "] " << msg << '\n';
}
void demo() {
log("started"); // 输出 demo() 所在文件与行号,而不是 log() 的
} 宏的替代品:类型安全、可传递、可比较,还能拿到函数名(__func__ 也没有了)。
数学常数
<numbers> 提供编译期数学常数(pi、e、ln10、phi 等):
#include <numbers>
double area = std::numbers::pi * r * r; // 3.14159265358979323846...
double growth = std::numbers::e; // 2.71828...
double digits10 = std::numbers::ln10; // 2.30258...(10 为底的对数换算)
double golden = std::numbers::phi; // 1.61803... 黄金分割
// 还有 sqrt2、sqrt3、inv_pi、egamma 等,全部是 double 精度的 inline constexpr 变量模板 对 atomic 的许多扩展
// 1. 原子浮点(fetch_add / fetch_sub)
std::atomic<double> sum{0.0};
sum.fetch_add(1.5);
// 2. 等待/通知:比忙等省 CPU,比条件变量轻
std::atomic<int> ready{0};
// 等待方:值还是 0 就阻塞(醒来后需重新检查并循环)
while (ready.load(std::memory_order_acquire) == 0)
ready.wait(0, std::memory_order_acquire);
// 通知方
ready.store(1, std::memory_order_release);
ready.notify_one();
// 3. atomic_ref:让"已有的普通变量"临时获得原子能力
int plain = 0;
std::atomic_ref<int> ref{plain};
ref.fetch_add(1); // plain 被原子修改
// 4. std::atomic<T>::is_always_lock_free:类型是否永远免锁(编译期可判) barrier 和 latch
两种线程协调原语,替代手工实现的计数条件变量:
// latch:一次性倒计数,"等大家到齐"
std::latch ready{4};
for (int i = 0; i < 4; ++i)
pool.submit([&] { load_assets(); ready.count_down(); });
ready.wait(); // 等全部完成,开始下一阶段(latch 不可复用)
// barrier:可复用的阶段屏障,每轮全员到齐后自动进入下一轮
std::barrier sync{4}; // 还可带完成回调:barrier<...> sync{4, []{ swap_buffers(); }};
for (int step = 0; step < 10; ++step) {
compute_chunk();
sync.arrive_and_wait(); // 等所有线程到齐,一起进入下一轮
} 选择:一次性用 latch(更轻),多轮迭代用 barrier。
特性测试宏
按特性而不是按编译器版本写条件编译:
#if __has_cpp_attribute(nodiscard) // 属性存在性
# define NODISCARD [[nodiscard]]
#else
# define NODISCARD
#endif
#if __has_include(<version>)
# include <version> // 所有库特性宏集中于此
#endif
#ifdef __cpp_lib_format // 库特性:__cpp_lib_XXX
# include <format>
# define HAS_FORMAT 1
#endif
#ifdef __cpp_consteval // 语言特性:__cpp_XXX
constexpr
#endif
int fast_calc(); 比 #if _MSC_VER >= 1930 这类版本号判断稳健得多(同版本编译器不同 flag 支持不同)。
bit_cast<>
std::bit_cast<To>(from) 以位级重新解释对象,且可用于 constexpr:
#include <bit>
#include <cstdint>
constexpr std::uint32_t bits = std::bit_cast<std::uint32_t>(1.0f);
// bits == 0x3F800000:IEEE 754 的 1.0
// 对比三种"重解释":
// reinterpret_cast:不允许 constexpr,有严格别名违规风险
// memcpy:允许合法但不是 constexpr
// bit_cast:合法 + constexpr,要求两侧大小相同且可平凡拷贝 典型用途:浮点位操作、哈希混合、序列化测试。
位操作
<bit> 收录一批常用位运算(大多 constexpr,免锁数据结构与协议解析常用):
std::popcount(0b1011u); // 3:置位个数
std::countl_zero(0x0010u); // 前导零个数(x86 上对应 LZCNT 指令)
std::countr_zero(0b1010u); // 1:尾部零
std::rotl(0x89u, 4); // 循环左移 4 位
std::rotr(0x89u, 4); // 循环右移
std::has_single_bit(64u); // true:是否 2 的幂
std::bit_ceil(5u); // 8:不小于 5 的最小 2 的幂(对齐计算常用)
std::bit_floor(5u); // 4
std::bit_width(5u); // 3:表示 5 需要几位 更多的标准库函数成为 constexpr
容器、算法大面积 constexpr 化,编译期可以造表了:
constexpr int sum_1_to_10() {
std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // constexpr 容器
std::sort(v.begin(), v.end(), std::greater<>{}); // constexpr 算法
int s = 0;
for (int x : v) s += x;
return s; // 析构时释放(瞬态分配),不泄漏到运行期
}
static_assert(sum_1_to_10() == 55, ""); 标准库中更多地使用 <=> 操作符
标准库类型整体接通三向比较:
std::string a = "apple", b = "banana";
if ((a <=> b) < 0) { /* a < b,一次比较拿到全序 */ }
std::vector<int> v1{1, 2}, v2{1, 3};
auto ord = v1 <=> v2; // 容器按字典序三向比较
// 自定义类型默认 <=> 后,标准容器/算法对其自动可用全套比较
struct Key {
int id;
auto operator<=>(const Key&) const = default;
};
std::map<Key, std::string> m; // Key 可作 key(< 来自 <=>) 一些小的扩展
// jthread:析构自动 stop + join 的线程(配合 stop_token 协作式取消)
std::jthread worker([](std::stop_token st) {
while (!st.stop_requested())
poll_once();
}); // 作用域结束:request_stop() 然后等待退出
// semaphore:信号量(counting_semaphore),限流/资源池
std::counting_semaphore<4> io_slots{4}; // 最多 4 个并发 IO
io_slots.acquire();
do_io();
io_slots.release();
// std::binary_semaphore 是计数上限为 1 的别名
// 数值
std::midpoint(0, 10); // 5:防溢出的中点
std::lerp(0.0, 10.0, 0.25); // 2.5:线性插值
// 字符串便捷函数
std::string f = "config.yaml";
f.starts_with("config"); // true
f.ends_with(".yaml"); // true 小结
C++20 的 Big Four 里,概念与范围已经可以直接改善生产代码;模块受工具链(编译器、CMake、各仓库混合构建)成熟度限制,落地前需要验证;协程建议先用手写 Generator 或第三方库(cppcoro 等)过渡,C++23 的 std::generator 落地后再转正。<=>、格式化、日历、位操作这些中小特性没有引入成本问题,随用随取。
系列其他篇目:现代 C++ 新特性汇总 · C++11 新特性 · C++14 新特性 · C++17 新特性 · C++23 新特性