一、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_ifstd::transformstd::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::filterviews::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 五大新特性 概念(Concepts) 模板参数约束 范围库(Ranges) 声明式算法管道 协程(Coroutines) 异步暂停/恢复 模块(Modules) 告别头文件地狱 三向比较(<=>) 自动生成比较运算符 共同目标 让 C++ 更安全、更易读、更高效 降低心智负担,提升开发效率

八、总结

C++20 的这五个特性,每一个都解决了一个长期困扰 C++ 程序员的问题:

  • 概念让模板错误信息变得可读,约束更清晰
  • 范围库让算法组合变得像流水线一样自然
  • 协程让异步代码写起来像同步代码
  • 模块终结了头文件地狱,提升编译速度
  • 三向比较省去了写一堆比较运算符的麻烦

说实话,我刚接触这些特性时也觉得有点多。但用了一段时间后,我发现它们确实让代码质量上了一个台阶。如果你现在还在用 C++17 或更早的版本,我建议你尽快升级编译器,开始尝试 C++20。你可能会发现,原来 C++ 也可以写得这么舒服。

一句话记住:C++20 不是小修小补,而是一次质的飞跃。它让 C++ 从一个“老派”语言,真正迈入了现代编程语言的行列。


公众号:蓝海资料掘金营,微信 deep3321