C++20 是 C++ 史上最大的一次版本更新,被很多人称为"又一个 C++11"。概念让模板约束第一次有了"名字",协程把异步代码写得像同步, ranges 让容器操作变成声明式管道。这三板斧,正在重新定义现代 C++ 的写法。本文逐个讲透,并给出可直接编译的示例。
1. 概念(concepts):给模板约束起名字
模板报错一直是"天书"级别的体验:几百行错误信息里翻找真正的原因。概念把"类型必须满足什么条件"写成可命名的约束,编译器能直接告诉你"add 不满足 Numeric 约束",代码本身也更像一份可执行的文档。
#include <concepts>
#include <iostream>
#include <string>
// 定义一个概念:Numeric = 整型或浮点型
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// 用概念约束模板参数
template <Numeric T>
T add(T a, T b) {
return a + b;
}
// 等价写法:requires 子句
template <typename T>
requires Numeric<T>
T mul(T a, T b) {
return a * b;
}
int main() {
std::cout << add(1, 2) << std::endl; // 3
std::cout << mul(2.5, 4.0) << std::endl; // 10
// add(std::string("a"), std::string("b")); // 编译错误:不满足 Numeric
return 0;
}
概念还能组合、约束 auto 变量(Numeric auto x)、用在 requires 表达式里做更细的检查。它最大的价值不是"跑得更快",而是让错误信息可读、让接口意图自解释——这对泛型库的维护者来说是无价之宝。概念还支持 requires 表达式做"能力检查":requires(T a) { a + a; } 表示 T 必须支持加法运算。这种"检查表达式是否合法"的能力,让泛型代码能精确描述"我需要什么",而不是靠一堆 traits 拼凑。约束失败时,编译器会列出候选概念和哪个要求没满足,排查效率比 SFINAE 时代高一个数量级。
2. 协程:异步代码的"同步写法"
回调地狱是异步编程的老大难。协程让函数可以"暂停/恢复":co_await 挂起等待、co_yield 逐个产出值、co_return 结束。C++20 协程是"无栈协程",由编译器生成状态机,配合生成器可以写出简洁的惰性序列。下面的极简生成器演示了完整机制:
#include <coroutine>
#include <iostream>
// 极简生成器:惰性产出 T 类型序列
template <typename T>
struct Generator {
struct promise_type {
T current;
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {} // 真实代码应保存并重新抛出异常
std::suspend_always yield_value(T v) {
current = v;
return {};
}
void return_void() {}
};
using handle = std::coroutine_handle<promise_type>;
explicit Generator(handle h) : h_(h) {}
~Generator() { if (h_) h_.destroy(); }
Generator(const Generator&) = delete; // 不可拷贝
bool next() {
if (!h_ || h_.done()) return false;
h_.resume(); // 恢复执行到下一个挂起点
return !h_.done();
}
T value() const { return h_.promise().current; }
private:
handle h_;
};
// co_yield 让这个普通函数变成惰性序列
Generator<int> range(int n) {
for (int i = 0; i < n; ++i) {
co_yield i; // 产出 i 并挂起
}
}
int main() {
auto gen = range(5);
while (gen.next()) {
std::cout << gen.value() << ' '; // 输出 0 1 2 3 4
}
std::cout << std::endl;
return 0;
}
读这段代码抓住三条主线:promise_type 是协程与调用者的"信使",负责存放产出值;co_yield 把值交给 promise 并挂起;调用者通过 coroutine_handle 恢复或销毁协程。理解这三件事,再看任何协程库都不会发怵。协程真正的用武之地是异步 I/O:网络请求、文件读写、定时器等待。用 co_await 挂起后,线程可以去处理别的任务,数据就绪再回来继续——一个线程就能管理成千上万个并发连接,这是高性能服务器常见的架构。代价是学习曲线陡峭:promise_type、awaiter、coroutine_handle 三件套需要时间消化,建议先用标准库或成熟框架的封装,再自己动手写。
3. ranges:容器操作的声明式管道
以前做"筛选 + 映射 + 求和",要写一堆迭代器循环,还要纠结中间容器。ranges 库提供视图(view)和管道操作符 |,把数据处理写成数据流:不创建中间容器、惰性求值,代码像在描述意图而不是操作步骤。
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
// 筛选偶数 -> 平方 -> 输出,全程无中间容器
auto even_squares = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : even_squares) {
std::cout << x << ' '; // 输出 4 16 36 64
}
std::cout << std::endl;
int sum = 0;
for (int x : even_squares) sum += x;
std::cout << "sum = " << sum << std::endl; // 120
return 0;
}
视图是惰性的:管道构建时不做任何计算,遍历到哪个元素才算哪个。这意味着处理一百万个元素的流水线,内存占用仍是常数级别。视图还支持组合,views::take、views::reverse、views::iota 等几十个现成组件随取随用。ranges 不止有视图,还有算法:std::ranges::sort、std::ranges::find 等可以直接作用于容器,还支持投影(projection)——比如按 pair 的第二个元素排序,写 std::ranges::sort(v, {}, &std::pair<int,int>::second) 一行搞定。视图加算法的组合,让"描述数据怎么处理"和"执行处理"彻底分离,代码意图一目了然。
4. 如何在编译器中启用 C++20
# 编译并运行(GCC 建议 12+,协程支持较新)
g++ -std=c++20 main.cpp -o main
./main
# Clang 建议 16+,协程需配合 libc++
clang++ -std=c++20 -stdlib=libc++ main.cpp -o main
注意:协程和 ranges 对工具链要求较高,GCC 10 只能算"部分支持",强烈建议 GCC 12+/Clang 16+;CMake 项目设置 CMAKE_CXX_STANDARD 20。想确认工具链支持到什么程度,可以写个两行小程序打印 __cplusplus 宏:201703L 表示 C++17,202002L 表示 C++20,202302L 表示 C++23,一测便知当前编译器能开哪个标准。还有一个常见疑问:为什么 ranges 库要等 C++20 才进标准?因为视图的惰性求值和生命周期管理对库的设计要求极高,委员会为此打磨了十几年。好消息是,进入标准后的 ranges 已经相当稳定,主流标准库实现也在持续修复边缘情况,可以放心使用。
5. 升级到 C++20 值不值
要分情况看。concepts 和 ranges 是"升级成本低、收益高"的典型:它们不改变老代码的写法,新代码直接用即可,建议有条件就上。协程则要慎重——C++20 协程给的是"积木"而非"成品",标准库没有现成的 task/generator 类型(要等 C++23 的 std::generator),自己搭基础设施成本不低,生产环境建议先用成熟封装(cppcoro、asio 的协程支持等)。
整体结论:值得升,但按需引入。先把 concepts 用到模板接口上、用 ranges 重写几段数据处理,协程等项目确实需要异步或惰性序列时再深入。如果你所在团队还在用 C++17,也不必焦虑:concepts 和 ranges 的核心思想(约束接口、惰性视图)在 C++17 里可以用模板和手写迭代器近似实现,提前用这些思想写代码,等工具链升级后迁移成本几乎为零。下表是三大特性的速览:
| 特性 | 解决什么问题 | 典型语法 |
|---|---|---|
| concepts | 模板约束可读、报错友好 | template <Numeric T> |
| coroutines | 异步/生成器代码大幅简化 | co_await / co_yield |
| ranges | 数据处理流水线化、零中间容器 | views::filter | views::transform |
💡 学习建议:先从 concepts 和 ranges 入手,把你项目里最乱的一个模板函数加上概念约束,把一个三层 for 循环改成管道式 ranges,立刻能感受到"代码即文档"的快乐;协程等读懂了本文的生成器示例、理解了暂停/恢复机制后再上生产。