一、C++20 到底带来了什么?
说实话,C++20 是我从业以来见过改动最大的一个版本。以前每次新标准出来,我顶多翻翻新特性列表,但 C++20 不一样——它直接改变了我的编码习惯。
这一章,我会带你快速过一遍 C++20 的五个核心新特性:概念、范围库、协程、模块、三向比较运算符。别怕,我不会一股脑扔给你一堆标准文档。我会用我自己的理解,告诉你这些东西到底解决了什么问题。
一句话总结:C++20 让 C++ 更现代、更安全、更好写。如果你还在用 C++17 甚至 C++11,看完这章你可能会想升级编译器。
二、概念(Concepts)——给模板加上“约束”
2.1 模板的痛点
模板是 C++ 的利器,但也是噩梦。我记得刚入行那会儿,写了一个模板函数,传了个不支持某种操作的类型进去,编译器给我吐了 200 行错误信息。我盯着屏幕看了十分钟,才找到真正的问题在哪。
为什么会这样?因为模板在实例化之前,编译器根本不知道你的类型合不合适。它只能等到实例化那一刻,才发现“哦,这个类型没有这个操作”,然后抛出一堆让人看不懂的报错。
2.2 概念怎么解决?
概念说白了,就是给模板参数加一个“门槛”。你必须在调用之前就告诉编译器:我这个类型必须支持某些操作。如果不满足,编译器直接给你一个清晰的错误,而不是一堆内部展开信息。
#include <concepts>
#include <iostream>
// 定义一个概念:类型 T 必须支持加法
template <typename T>
concept Addable = requires(T a, T b) {
a + b;
};
// 使用概念约束模板
template <Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(3, 4) << std::endl; // 正确
// std::cout << add("hello", "world"); // 错误:const char* 不支持加法
return 0;
}
你看,Addable 这个概念明确要求类型必须支持 + 操作。如果你传了一个不支持加法的类型,编译器会直接告诉你“这个类型不满足 Addable 概念”,而不是给你看一堆模板实例化的内部细节。
我的建议:在写模板时,尽量给每个模板参数加上概念约束。这不仅能让你自己少踩坑,也能让调用你代码的人少骂你几句。
三、范围库(Ranges)——让算法更“声明式”
3.1 传统 STL 算法的痛点
传统 STL 算法用起来其实挺别扭的。你想对一个容器做“过滤-转换-求和”三个操作,得写三个 std::copy_if、std::transform、std::accumulate,中间还得创建临时容器。代码又长又慢。
3.2 Ranges 怎么改?
Ranges 库的核心思想是“管道操作”。你可以把多个操作像流水线一样串起来,中间不需要临时变量。代码读起来就像在描述“你要做什么”,而不是“你怎么做”。
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 传统写法
std::vector<int> even;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(even),
[](int n) { return n % 2 == 0; });
std::vector<int> squared;
std::transform(even.begin(), even.end(), std::back_inserter(squared),
[](int n) { return n * n; });
int sum = std::accumulate(squared.begin(), squared.end(), 0);
// Ranges 写法
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::ranges::to<std::vector>();
std::cout << "Sum: " << std::ranges::fold_left(result, 0, std::plus{}) << std::endl;
return 0;
}
我个人特别喜欢这种写法。它让代码的意图一目了然:先过滤偶数,再平方,最后求和。中间没有临时变量,没有迭代器配对,干净利落。
注意:Ranges 的 to<std::vector>() 是 C++23 才加入的。在 C++20 中,你需要手动收集结果。不过核心的 views::filter 和 views::transform 在 C++20 中已经可用了。
四、协程(Coroutines)——异步编程的“语法糖”
4.1 协程是什么?
协程是一种可以暂停和恢复的函数。你想想看,传统的函数一旦调用,就必须从头执行到尾。但协程可以在中间某个点暂停,把控制权交回去,等条件满足了再回来继续执行。
我在做网络服务时经常遇到这种情况:一个请求需要等待数据库查询结果,传统做法要么用回调,要么用线程。回调多了代码就变成“回调地狱”,线程多了又浪费资源。协程正好解决了这个问题。
4.2 一个简单的协程例子
#include <coroutine>
#include <iostream>
#include <future>
// 一个简单的协程返回类型
struct SimpleTask {
struct promise_type {
SimpleTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 模拟异步操作
std::future<int> async_compute() {
std::promise<int> p;
auto f = p.get_future();
std::thread([p = std::move(p)]() mutable {
std::this_thread::sleep_for(std::chrono::seconds(1));
p.set_value(42);
}).detach();
return f;
}
// 协程函数
SimpleTask my_coroutine() {
std::cout << "Start coroutine" << std::endl;
auto result = co_await async_compute();
std::cout << "Got result: " << result << std::endl;
}
int main() {
my_coroutine();
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
这里的关键字是 co_await。它告诉编译器:在这里暂停,等异步操作完成后再回来继续执行。你不需要手动管理线程或回调,代码看起来就像同步的一样。
避坑指南:协程的 promise_type 结构体是必须的,它定义了协程的行为。我曾经因为忘记定义 final_suspend 导致协程结束后资源没释放,排查了半天。建议你写协程时先画清楚生命周期。
五、模块(Modules)——告别头文件地狱
5.1 头文件的问题
每个 C++ 程序员都经历过“头文件地狱”。一个 #include 会把整个头文件的内容复制进来,导致编译变慢、宏污染、符号冲突。我记得有个项目,改一行代码要等十分钟编译,就是因为头文件层层嵌套。
5.2 模块怎么解决?
模块把接口和实现分开了。你只需要导出你想暴露的东西,其他内容对外完全不可见。编译速度大幅提升,而且再也不用担心宏污染了。
// math.cppm (模块接口文件)
export module math;
export int add(int a, int b) {
return a + b;
}
// 这个函数不会被导出,外部看不到
int internal_helper(int x) {
return x * 2;
}
// main.cpp
import math;
#include <iostream>
int main() {
std::cout << add(3, 4) << std::endl; // 正确
// std::cout << internal_helper(5); // 错误:未导出
return 0;
}
你看,export module math; 定义了一个模块,export int add(...) 导出了函数。其他没加 export 的内容,外部完全看不到。这比头文件干净多了。
注意:模块目前对编译器版本要求较高。GCC 和 Clang 支持得比较好,MSVC 在 Visual Studio 2022 中也支持了。如果你还在用老编译器,可能暂时用不了。
六、三向比较运算符(<=>)——飞船运算符
6.1 比较操作的痛点
写一个自定义类型,如果要支持排序,你得重载 <、<=、>、>=、==、!= 六个运算符。写起来烦,还容易漏掉某个。
6.2 飞船运算符怎么用?
<=> 一次返回三种结果:小于、等于、大于。编译器可以根据它自动生成其他比较运算符。
#include <compare>
#include <iostream>
struct Point {
int x, y;
// 自动生成 <, <=, >, >=, ==, !=
auto operator<=>(const Point& other) const = default;
};
int main() {
Point p1{1, 2}, p2{3, 4};
if (p1 < p2) {
std::cout << "p1 is less than p2" << std::endl;
}
if (p1 == p2) {
std::cout << "p1 equals p2" << std::endl;
}
return 0;
}
加上 = default 后,编译器会按成员顺序逐个比较。如果你需要自定义比较逻辑,也可以自己实现 operator<=>。
我的习惯:只要自定义类型需要排序,我第一件事就是加上 auto operator<=>(const T&) const = default;。省事,而且不容易出错。
七、知识体系总览
下面这张图帮你理清这五个特性的定位和关系:
八、总结
C++20 的这五个特性,每一个都解决了一个长期困扰 C++ 程序员的问题:
- 概念让模板错误信息变得可读,约束更清晰
- 范围库让算法组合变得像流水线一样自然
- 协程让异步代码写起来像同步代码
- 模块终结了头文件地狱,提升编译速度
- 三向比较省去了写一堆比较运算符的麻烦
说实话,我刚接触这些特性时也觉得有点多。但用了一段时间后,我发现它们确实让代码质量上了一个台阶。如果你现在还在用 C++17 或更早的版本,我建议你尽快升级编译器,开始尝试 C++20。你可能会发现,原来 C++ 也可以写得这么舒服。
一句话记住:C++20 不是小修小补,而是一次质的飞跃。它让 C++ 从一个“老派”语言,真正迈入了现代编程语言的行列。
公众号:蓝海资料掘金营,微信 deep3321