50、C++20新特性:概念(concepts)、协程(coroutines)、范围(ranges)

C++20 是 C++ 语言发展史上一个里程碑式的版本。说实话,我当年从 C++11 一路用到 C++20,每次标准更新都像在学一门新语言。但 C++20 给我的冲击最大——它不再是简单的语法糖,而是真正改变了我们写代码的方式。

今天咱们就聊聊 C++20 的三个核心新特性:概念、协程和范围。这三个东西,说白了就是让 C++ 更安全、更高效、更优雅。我一个个来讲。

1. 概念(Concepts)—— 给模板加上约束

模板是 C++ 的利器,但也是痛点。你想想看,写一个模板函数,传进去一个不支持某种操作的类型,编译器能给你吐出一堆天书般的错误信息。我早期做项目时,最怕的就是模板编译错误——那几百行的报错,找半天都不知道问题出在哪。

概念(Concepts)就是为了解决这个问题的。它允许你给模板参数加上明确的约束,告诉编译器:「这个类型必须支持这些操作」。如果传错了,错误信息直接告诉你「不满足某个概念」,而不是一堆内部实例化的细节。

来看个例子:

#include <concepts>
#include <iostream>

// 定义一个概念:类型 T 必须支持加法运算
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

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

int main() {
    std::cout << add(3, 4) << std::endl;       // 正确:int 支持加法
    // std::cout << add("hello", "world");     // 错误:const char* 不支持加法
    return 0;
}

你看,Addable 这个概念明确要求类型必须支持 + 运算,并且结果能转换回原类型。如果传了不满足条件的类型,编译器会直接告诉你「不满足 Addable 概念」,而不是报一堆模板实例化的内部错误。

我的习惯:在写模板库时,我会先定义好概念,再写模板函数。这样既能让代码自文档化,也能在编译期就捕获错误。我建议你也试试——从简单的概念开始,比如 std::integralstd::floating_point,慢慢过渡到自定义概念。

注意:概念不是运行时检查,而是编译期约束。它不会影响性能,但能显著提升代码的可读性和健壮性。我曾经在一个大型项目中用概念重构了模板代码,编译错误信息从几百行变成了几行——那种感觉,真的很爽。

2. 协程(Coroutines)—— 异步编程的新范式

协程,说白了就是可以暂停和恢复的函数。你想想看,传统的函数调用是「一去不复返」——调用一个函数,它执行完才返回。但协程可以在执行过程中暂停,把控制权交还给调用者,等条件满足后再恢复执行。

我最早接触协程是在做网络服务器的时候。那时候用回调函数处理异步 I/O,代码写得跟意大利面条似的。后来用协程重构,代码变得像同步代码一样直观,但底层却是异步执行的。

C++20 的协程是无栈协程,它不依赖操作系统线程,而是由编译器生成状态机。来看个简单的例子:

#include <coroutine>
#include <iostream>
#include <optional>

// 一个简单的协程返回类型
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;
    
    explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }
    
    bool next() {
        handle.resume();
        return !handle.done();
    }
    
    int value() { return handle.promise().current_value; }
};

// 协程函数:生成斐波那契数列
Generator fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;  // 暂停并返回当前值
        int next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    auto gen = fibonacci(10);
    while (gen.next()) {
        std::cout << gen.value() << " ";
    }
    std::cout << std::endl;
    return 0;
}

这个例子中,fibonacci 是一个协程。每次 co_yield 都会暂停执行,返回当前值,然后等待下一次 resume() 继续。你想想看,如果用传统方式实现一个生成器,你得手动维护状态——而协程自动帮你做了这一切。

核心要点:协程的关键字有三个:co_await(等待异步操作)、co_yield(返回一个值并暂停)、co_return(结束协程)。它们让异步代码写起来像同步代码一样自然。

避坑指南:我曾经在项目里滥用协程,把每个小函数都写成协程。结果发现性能反而下降了——因为协程的创建和销毁是有开销的。记住:协程适合 I/O 密集型或需要延迟计算的场景,不适合 CPU 密集型的简单计算。

3. 范围(Ranges)—— 更优雅的算法组合

范围(Ranges)是 C++20 对 STL 算法的一次重大升级。传统的 STL 算法需要你传入迭代器对(begin 和 end),而范围让你直接传入一个容器或视图。更重要的是,它支持管道操作符(|),可以把多个算法串联起来。

我刚开始用范围时,最大的感受就是:代码变得像流水线一样清晰。你想想看,以前写一个「过滤偶数、取平方、取前5个」的操作,得写好几层循环和临时容器。现在一行搞定。

来看个例子:

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

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 使用范围:过滤偶数,取平方,取前3个
    auto result = nums 
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(3);
    
    for (int n : result) {
        std::cout << n << " ";  // 输出:4 16 36
    }
    std::cout << std::endl;
    
    return 0;
}

你看,filtertransformtake 通过 | 串联起来,每个步骤做什么一目了然。而且这些操作是惰性求值的——只有在遍历结果时才会真正计算。这意味着你不会创建任何临时容器,性能更好。

我的建议:范围最强大的地方在于视图(views)。视图不拥有数据,只是对数据的变换。你可以组合任意多个视图,而不会产生额外的拷贝开销。我在处理大数据集时特别喜欢用范围——它让代码既简洁又高效。

4. 三者如何协同工作?

概念、协程、范围这三个特性不是孤立的。它们可以很好地配合使用。比如,你可以用概念来约束范围算法中的类型,用协程来实现惰性生成的范围视图。

来看一个综合例子:

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

// 定义一个概念:可排序的范围
template <typename R>
concept SortableRange = std::ranges::range<R> && 
    requires(R& r) {
        std::ranges::sort(r);
    };

// 使用概念约束函数
template <SortableRange R>
void process_and_print(R& r) {
    std::ranges::sort(r);
    for (auto v : r | std::views::take(5)) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> data = {5, 3, 1, 4, 2, 6, 7, 8, 9, 10};
    process_and_print(data);  // 输出:1 2 3 4 5
    return 0;
}

这个例子中,SortableRange 概念确保传入的范围支持排序,然后我们用范围视图取前5个元素。概念保证了类型安全,范围提供了优雅的算法组合——两者相得益彰。

总结一下:

  • 概念:让模板错误信息更友好,代码更自文档化
  • 协程:让异步代码写起来像同步代码,适合 I/O 和生成器场景
  • 范围:让算法组合更优雅,惰性求值提升性能

这三个特性,我建议你从范围开始入手——它最容易上手,也最立竿见影。然后慢慢尝试概念,最后再挑战协程。嗯,C++20 确实让 C++ 变得更现代了,但学习曲线也陡峭了不少。不过别担心,一点点来,总会掌握的。

C++20 三大新特性知识体系 C++20 新特性 概念 (Concepts) 协程 (Coroutines) 范围 (Ranges) 模板约束 编译期检查 自文档化 co_await co_yield co_return 无栈协程 视图 (Views) 管道操作 惰性求值 三者协同:更安全、更高效、更优雅的 C++

学习路径建议:如果你刚开始接触 C++20,我建议按这个顺序学习:先学范围(最直观,马上能用),再学概念(提升模板代码质量),最后学协程(需要理解异步模型)。别贪多,一个一个来。

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