Skip to content

C++20 新特性 ​

C++20 是一个 major 版本,有四个最重要的特性——"The Big Four":模块(Modules)、概念(Concepts)、协程(Coroutines)、范围(Ranges)。此外格式化库、日历库、<=>、constexpr 的大幅扩展也都在这一版落地。本文逐项展开,附可运行的示例与原理说明。

编译器支持情况见 Compiler support for C++20。系列总览见 现代 C++ 新特性汇总。

语言特性 ​

模块 ​

模块用真正的「接口」取代头文件的文本包含:编译一次生成二进制接口文件(BMI/CMI),后续导入不再重复解析:

cpp
// 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」:

cpp
#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 强的地方还在于可以按约束重载——更受限的候选胜出:

cpp
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:

cpp
#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 指定初始化的受限版本:按成员名初始化,跳过的取默认值:

cpp
struct Options {
    int  threads = 1;
    bool verbose = false;
    int  depth   = 10;
};

Options o{.threads = 8, .depth = 20};    // verbose 取默认 false

// 限制(比 C99 严):
// 必须按声明顺序:{.depth = 20, .threads = 8} 编译错误
// 不能混合指定与非指定初始化
// 不能用于聚合之外的类型(含用户构造函数的不行)

<=>(三向比较操作符) ​

一次定义生成全套比较运算符,三路比较返回「小于/等于/大于」三态结果:

cpp
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):

cpp
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]] ​

cpp
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:

cpp
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 更严格:

cpp
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):

cpp
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 把枚举值引入当前作用域,写起来少前缀:

cpp
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。

一些小的扩展 ​

cpp
// 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)」体系:惰性视图用管道符 | 串联,按需拉取,不产生中间容器:

cpp
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 的冗长:

cpp
#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> 加入人类可读的日历类型与时区支持:

cpp
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 的泛化版),函数签名的「我要一段连续数据」从此有明确表达:

cpp
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__ 宏:

cpp
#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 等):

cpp
#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 的许多扩展 ​

cpp
// 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 ​

两种线程协调原语,替代手工实现的计数条件变量:

cpp
// 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。

特性测试宏 ​

按特性而不是按编译器版本写条件编译:

cpp
#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:

cpp
#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,免锁数据结构与协议解析常用):

cpp
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 化,编译期可以造表了:

cpp
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, "");

标准库中更多地使用 <=> 操作符 ​

标准库类型整体接通三向比较:

cpp
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(< 来自 <=>)

一些小的扩展 ​

cpp
// 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 新特性

最近更新

基于 VitePress 构建