C++20 新特性

C++20 是继 C++11 以来最大的版本更新,四大天王:Concepts、Ranges、Coroutines、Modules。


给模板参数加约束,让模板报错不再是天书:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 定义一个概念:T 必须是可加的
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

// 使用概念约束模板参数
template<Addable T>
T add(T a, T b) {
    return a + b;
}

四种约束形式:

  • 简单约束:sizeof(T) == 4
  • 类型约束:std::integral<T>
  • 复合约束:{ expr } -> 类型概念
  • 嵌套约束:requires { ... }
1
2
3
4
5
6
7
template<typename T>
concept Container = requires(T c) {
    c.begin();       // 必须有 begin()
    c.end();         // 必须有 end()
    typename T::value_type;  // 必须有 value_type 类型
    { c.size() } -> std::convertible_to<size_t>;
};

<concepts> 头文件里提供了一堆:

  • std::same_as<T, U> — 类型相同
  • std::derived_from<Derived, Base> — 派生关系
  • std::convertible_to<From, To> — 可转换
  • std::integral<T> — 整型
  • std::floating_point<T> — 浮点型
  • std::ranges::range<T> — 范围

彻底改变 STL 算法的写法,支持链式调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <ranges>

std::vector<int> nums = {1, 2, 3, 4, 5, 6};

// C++17:嵌套调用,可读性差
// C++20:管道式,从左到右读
auto result = nums
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 2; })
    | std::views::take(3);

for (int x : result) {
    std::cout << x << " ";  // 4 8 12
}
适配器作用
views::filter过滤
views::transform转换映射
views::take(n)取前 n 个
views::drop(n)跳过前 n 个
views::reverse反转
views::keys / values取 pair 的 key/value
views::join扁平化
views::split分割

views 不会立即计算,只有在迭代时才生成元素,性能更好。


函数可以暂停和恢复,异步编程神器:

1
2
3
co_await   // 暂停,等待异步操作
co_return  // 返回值并结束
co_yield   // 返回一个值并暂停,下次从这里继续
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield b;   // 返回 b,暂停在这里
        int next = a + b;
        a = b;
        b = next;
    }
}

// 使用
for (int x : fibonacci(10)) {
    std::cout << x << " ";
}
1
2
3
4
task<int> fetchData() {
    auto data = co_await asyncLoad();  // 暂停,不阻塞线程
    co_return data.size();
}

注意:标准库只提供了协程机制,没有提供具体的协程类型(task/generator),需要自己实现或用第三方库(如 cppcoro)。C++23 才补了 std::generator。


告别 #include,解决头文件地狱:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// math.ixx (模块接口文件)
export module math;

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

export namespace math_utils {
    int mul(int a, int b) { return a * b; }
}
1
2
3
4
5
6
import math;

int main() {
    add(1, 2);
    math_utils::mul(3, 4);
}

优点:

  • 编译速度大幅提升(只编译一次)
  • 没有宏污染
  • 模块内部的东西默认不导出,封装更好

俗称「飞船运算符」,一次比较得出大小关系:

1
2
3
4
5
6
7
8
9
struct Point {
    int x, y;
    auto operator<=>(const Point&) const = default;  // 自动生成所有比较运算符
};

Point a{1, 2}, b{3, 4};
a < b;   // OK,自动生成
a >= b;  // OK
a == b;  // OK

返回类型有三种:

  • std::strong_ordering — 强序(完全可比)
  • std::weak_ordering — 弱序
  • std::partial_ordering — 偏序(可能不可比,比如 NaN)

像 C 语言那样按名字初始化结构体成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Point {
    int x = 0;
    int y = 0;
    int z = 0;
};

Point p {
    .x = 10,
    .z = 30
    // y 用默认值 0
};

注意:顺序必须和声明顺序一致,不能跳着来。


必须在编译期执行,否则报错:

1
2
3
4
5
6
7
consteval int sqr(int n) {
    return n * n;
}

constexpr int a = sqr(10);  // OK,编译期
int b = sqr(20);            // OK,编译期常量
// int x = 5; sqr(x);       // ERROR,x 不是编译期常量

保证变量在编译期初始化,避免静态初始化顺序问题:

1
constinit int global = compute_at_compile_time();

Python 风格的格式化字符串,终于不用 printf 或 stringstream 了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <format>

std::string s = std::format("Hello, {}! You are {} years old.", "Tom", 25);
// "Hello, Tom! You are 25 years old."

// 按位置
std::format("{1} {0}", "world", "Hello");  // "Hello world"

// 格式说明
std::format("{:>10}", 42);    // 右对齐,占10位
std::format("{:08b}", 42);    // 8位二进制,前导零
std::format("{:.2f}", 3.14159);  // 保留2位小数

连续内存的视图,类似 string_view 但是通用的:

1
2
3
4
5
6
7
8
9
void printArray(std::span<int> arr) {
    for (int x : arr) std::cout << x << " ";
}

int a[] = {1, 2, 3};
printArray(a);              // 原生数组

std::vector v = {4, 5, 6};
printArray(v);              // vector

特性说明
std::jthread自动 join 的线程,支持停止令牌
std::stop_token协作式线程中断
std::source_location获取当前代码位置(文件名、行号)
std::bit_cast安全的类型双关,替代 memcpy
std::midpoint / std::lerp中点和线性插值
[[likely]] / [[unlikely]]给编译器分支预测提示
范围 for 初始化语句for (auto it = v.begin(); auto x : v)
1
2
3
4
5
6
// likely/unlikely 示例
if (likely(n > 0)) {
    // 大概率走这里,编译器优化
} else {
    // 小概率
}

C++20 是改变编程范式的大版本:

  • Concepts:模板终于好用了,报错终于能看懂了
  • Ranges:STL 算法的现代写法,链式调用爽
  • Coroutines:异步编程基础设施
  • Modules:告别头文件,编译起飞
  • format / span / jthread:各种小确幸

相关内容