一、C++20/23 新特性全景:为什么我劝你早点上手

说实话,C++11 之后的几个标准,C++14 和 C++17 都算是「小修小补」。但 C++20 不一样——它带来的改变,在我看来堪比当年 C++11 的爆发。而 C++23 则是在这个基础上继续打磨。

我最早接触 C++20 是在一个大型分布式系统的底层库重构项目里。当时团队里争论了很久:要不要用新标准?我的态度很明确——新特性不是花架子,它们能实实在在地减少 bug、提升编译速度、让代码更清晰。今天我就把这几个核心特性掰开揉碎讲给你听。

核心观点:C++20/23 不是「语法糖」,而是编程范式的升级。尤其是概念与约束、协程、模块化这三样,直接改变了我们组织代码的方式。

1.1 概念与约束(Concepts)——让模板错误不再「天书」

用过模板的朋友都知道,模板报错信息有多「感人」。几十行甚至上百行的错误提示,核心问题可能只是传错了类型。C++20 的 Concepts 就是来解决这个问题的。

说白了,Concepts 就是给模板参数加了一道「安检门」。你可以在编译前就声明:这个模板参数必须满足什么条件。比如「必须能比较大小」「必须能拷贝构造」等等。

// C++17 写法:报错信息让人崩溃
template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

// C++20 写法:明确约束,报错清晰
template <typename T>
    requires std::integral<T>  // 只接受整数类型
T max(T a, T b) {
    return a > b ? a : b;
}

我在项目中遇到过这样一个坑:一个同事写了个泛型排序函数,传入了自定义类型但忘了重载 operator<。编译报错信息长达 200 多行,排查了半小时。如果用 Concepts,编译器直接告诉你「这个类型不满足可比较约束」,一行搞定。

我的建议:刚开始用 Concepts 时,别想着定义太复杂的约束。从 std::integralstd::floating_point 这些内置概念入手,慢慢再写自己的概念。

1.2 范围库(Ranges)——告别手写循环的「脏活累活」

你想想看,我们平时写代码,有多少时间花在「遍历容器→过滤→转换→收集结果」这种流水线上?C++20 的 Ranges 库就是专门干这个的。它把 STL 算法和范围(range)的概念结合,支持惰性求值和管道操作。

#include <ranges>
#include <vector>
#include <iostream>

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

// 传统写法:嵌套循环 + 临时容器
std::vector<int> result;
for (auto x : data) {
    if (x % 2 == 0) {
        result.push_back(x * 2);
    }
}

// Ranges 写法:一行搞定,清晰直观
auto result = data 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * 2; });

这里有个关键点:Ranges 的管道操作是惰性求值的。也就是说,上面的代码并不会立即执行 filter 和 transform,而是等到你真正遍历 result 时才计算。这在处理大数据集时能省下大量内存和 CPU。

注意:Ranges 的视图(views)不拥有数据,它们只是数据的「窗口」。如果你把原始容器销毁了,视图就悬空了。我曾经在项目里踩过这个坑——视图还在用,但底层 vector 已经被释放了。

1.3 协程(Coroutines)——异步编程的「降维打击」

协程这个概念,在 Python、Go 里早就有了。C++20 终于把它带了进来。但 C++ 的协程和其他语言不太一样——它是无栈协程,更轻量,更灵活。

我个人的理解是:协程让你可以用同步的写法,写出异步的代码。不用再写回调地狱,不用再手动管理状态机。

#include <coroutine>
#include <iostream>

// 一个简单的协程示例
struct Generator {
    struct promise_type {
        int current_value;
        
        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 {}; }
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    
    std::coroutine_handle<promise_type> handle;
    
    ~Generator() {
        if (handle) handle.destroy();
    }
    
    int next() {
        handle.resume();
        return handle.promise().current_value;
    }
};

Generator fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    auto gen = fibonacci();
    for (int i = 0; i < 10; ++i) {
        std::cout << gen.next() << " ";
    }
}

嗯,这里要注意:C++20 的协程是「框架级」的特性,它只提供了底层机制,没有提供高级封装。像 generatortask 这些常用模式,需要你自己实现或者用第三方库。C++23 在这方面有所改进,但还不够完善。

1.4 模块化编程(Modules)——编译速度的「救星」

如果你维护过大型 C++ 项目,一定对编译速度深恶痛绝。头文件层层包含,改一行代码就要重新编译半个项目。C++20 的 Modules 就是为了解决这个问题。

Modules 把代码组织成「模块单元」,而不是头文件。模块之间可以显式声明导入和导出,编译器可以缓存模块的编译结果,避免重复解析。

// math.cppm (模块接口文件)
export module math;

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

export int multiply(int a, int b) {
    return a * b;
}

// main.cpp (使用模块)
import math;
import <iostream>;

int main() {
    std::cout << add(3, 4) << std::endl;      // 7
    std::cout << multiply(3, 4) << std::endl; // 12
}

我在一个百万行级别的项目里试过 Modules,编译时间从 45 分钟降到了 12 分钟。当然,迁移过程并不轻松——需要重构头文件结构,处理循环依赖。但长期来看,这个投入是值得的。

避坑指南:Modules 目前(2024年)的主流编译器支持还不完全一致。MSVC 支持最好,Clang 次之,GCC 还在追赶。如果你要跨平台,建议先小范围试用。

1.5 std::span 与 std::format——日常编程的「小确幸」

这两个特性虽然不如协程、Modules 那么「重磅」,但日常使用频率极高,能显著提升代码质量和开发效率。

std::span 是一个「非拥有式」的数组视图。它不管理内存,只是指向一段连续内存的指针+长度。用来替代 C 风格的数组指针传参,安全又方便。

#include <span>
#include <vector>
#include <array>

void process_data(std::span<int> data) {
    for (auto& x : data) {
        x *= 2;
    }
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4};
    std::array<int, 4> arr = {5, 6, 7, 8};
    int raw[] = {9, 10, 11, 12};
    
    process_data(vec);   // 可以传 vector
    process_data(arr);   // 可以传 array
    process_data(raw);   // 可以传 C 数组
}

std::format 则是 Python 风格字符串格式化的 C++ 实现。比 printf 类型安全,比 std::stringstream 性能好,比 std::to_string 灵活。

#include <format>
#include <iostream>

int main() {
    int age = 30;
    double salary = 12345.6789;
    std::string name = "张三";
    
    // 传统写法
    char buf[100];
    sprintf(buf, "姓名: %s, 年龄: %d, 薪资: %.2f", name.c_str(), age, salary);
    
    // std::format 写法
    auto msg = std::format("姓名: {}, 年龄: {}, 薪资: {:.2f}", name, age, salary);
    std::cout << msg << std::endl;
}

性能对比:在我的一次基准测试中,std::formatstd::stringstream 快约 3-5 倍,比 sprintf 略慢但类型安全得多。日常开发中,我建议优先使用 std::format

知识体系总览

下面这张图概括了 C++20/23 新特性的核心脉络,以及它们各自解决的核心问题:

C++20/23 新特性知识体系 C++20/23 新特性 概念与约束 编译期类型检查 范围库 惰性求值 + 管道 协程 无栈异步编程 模块化编程 编译加速 std::span 非拥有式数组视图 std::format 类型安全格式化 六大特性覆盖:类型安全 · 性能优化 · 代码可读性 · 编译加速

总结与建议

C++20/23 的这六个特性,我建议你按这个优先级去学:

  1. 先上手 std::format 和 std::span——它们改动最小,收益立竿见影,几乎零风险。
  2. 再学 Concepts 和 Ranges——这两个能显著提升模板代码的质量和可读性。
  3. 最后攻克协程和 Modules——它们改变的是项目架构和编程范式,需要更多实践和思考。

说实话,C++ 这些年一直在「变重」,但 C++20/23 让我看到了它「变轻」的可能。Concepts 让模板更安全,Ranges 让算法更简洁,协程让异步更直观,Modules 让编译更快。这些特性加在一起,C++ 终于不再是那个「只有高手才能驾驭」的语言了。

下一章,我会深入讲解 Concepts 的高级用法——如何定义自己的概念、如何组合约束、以及在实际项目中的最佳实践。我们到时候见。


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