40、STL与C++20新特性:概念与约束、范围库、协程、三路比较运算符

C++20 是近十年来改动最大的一次标准更新。说实话,我刚看到提案列表时也有点懵——概念、范围、协程、飞船运算符,这些东西到底跟 STL 有什么关系?

后来在项目里真正用上之后,我才发现:这些新特性不是花架子,它们让 STL 用起来更顺手、更安全、更高效。今天我就把这四个核心特性掰开揉碎,结合 STL 实战场景讲清楚。

一句话总结:C++20 新特性不是替代 STL,而是让 STL 更好用。概念约束模板参数,范围简化迭代操作,协程改变异步编程,三路比较运算符让排序更省事。

4.1 概念与约束:给模板戴上紧箍咒

先说说概念(Concepts)。这东西说白了就是给模板参数加个「类型检查」。以前模板编译报错能让你看半天,现在有了概念,错误信息直接告诉你「这个类型不满足要求」。

我个人习惯在写泛型容器或算法时,先用概念约束好参数类型。比如你要写一个只接受整数类型的函数:

#include <concepts>
#include <vector>
#include <algorithm>

template<std::integral T>
T sum_of_squares(const std::vector<T>& vec) {
    T result = 0;
    for (const auto& v : vec) {
        result += v * v;
    }
    return result;
}

// 调用
std::vector<int> vi{1, 2, 3};
auto s = sum_of_squares(vi);  // 正确

std::vector<double> vd{1.1, 2.2};
// auto s2 = sum_of_squares(vd);  // 编译错误:double 不满足 integral 概念

我在项目中遇到过一个问题:团队里有人把 string 传进了本应只处理数值的模板函数,结果运行时崩溃。后来加了概念约束,编译期就拦截了这种错误。嗯,这就是概念的价值——把运行时错误提前到编译期。

避坑指南:我曾经在概念约束里写了过于复杂的 requires 表达式,导致编译时间暴涨。建议优先使用标准库提供的概念(如 std::integral、std::ranges::range),实在不够用再自己写。

4.2 范围库:告别迭代器对

范围库(Ranges)是我个人最爱的 C++20 特性。你想想看,以前用 STL 算法时,每次都要传两个迭代器:

// 旧方式
std::vector<int> v{5, 2, 8, 1, 9};
std::sort(v.begin(), v.end());

现在直接传容器本身:

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

std::vector<int> v{5, 2, 8, 1, 9};
std::ranges::sort(v);  // 简洁多了

// 更酷的是视图(views)
auto even = v | std::views::filter([](int n) { return n % 2 == 0; })
              | std::views::transform([](int n) { return n * n; });

for (int x : even) {
    std::cout << x << " ";  // 输出:4 64
}

为什么说这个重要?因为视图是惰性求值的——它不会创建新容器,只是在遍历时动态计算。我在处理百万级数据时,用视图链式操作,内存占用几乎没增加,代码却清晰了很多。

注意:视图不拥有数据,如果原始容器被销毁,视图就悬空了。我曾经在函数返回视图时踩过这个坑,返回的视图引用了局部容器的数据,调用方直接崩溃。记住:视图的生命周期不能超过原始数据。

4.3 协程:异步编程的新范式

协程(Coroutines)跟 STL 的关系没那么直接,但它在处理异步 I/O 和生成器场景时,能跟 STL 容器配合得很好。

说白了,协程就是可以暂停和恢复的函数。C++20 的协程是无栈的,开销极小。我举个生成器的例子:

#include <coroutine>
#include <vector>
#include <generator>  // C++23 标准库,但很多编译器已支持

// 生成斐波那契数列
std::generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;  // 暂停并返回值
        auto next = a + b;
        a = b;
        b = next;
    }
}

// 配合 STL 使用
int main() {
    std::vector<int> fibs;
    for (auto f : fibonacci(10)) {
        fibs.push_back(f);
    }
    // fibs 现在包含前10个斐波那契数
}

我在项目中用协程写过数据流水线:从网络读取数据包,经过协程生成器逐层解析,最后存入 STL 容器。整个过程没有显式的状态机,代码像同步一样直白。

个人建议:协程的学习曲线比较陡,建议先从生成器场景入手。不要一上来就搞复杂的异步框架,容易把自己绕晕。

4.4 三路比较运算符:排序从未如此简单

三路比较运算符(<=>),俗称「飞船运算符」。它解决了一个老问题:每次写自定义类型排序时,都要重载六个比较运算符(<、<=、>、>=、==、!=)。

现在只需要一行:

#include <compare>
#include <vector>
#include <algorithm>

struct Person {
    std::string name;
    int age;
    
    // 自动生成所有比较运算符
    auto operator<=>(const Person&) const = default;
};

int main() {
    std::vector<Person> people{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35}
    };
    
    std::ranges::sort(people);  // 按 name 字典序排序
    // 如果想按 age 排序,可以自定义
    std::ranges::sort(people, {}, &Person::age);
}

为什么会这样?因为 =default 告诉编译器按成员声明顺序逐个比较。如果成员都是可比较的类型,编译器就能自动生成所有比较逻辑。

我记得有一次接手老项目,里面有个结构体重载了七八个比较运算符,还写错了逻辑。用飞船运算符重构后,代码从 50 行缩到 2 行,而且再也不会出现「< 和 > 行为不一致」的 bug。

注意:如果成员中有指针类型,=default 会比较指针地址而不是指向的内容。这时候需要手动实现 operator<=>。我曾经在比较智能指针的容器时踩过这个坑,排序结果完全不对。

知识体系总览

下面这张图总结了 C++20 这四个新特性与 STL 的关系:

C++20 新特性与 STL 知识体系 C++20 STL 新特性 概念与约束 范围库 协程 三路比较运算符 模板参数约束 编译期类型检查 惰性视图 链式操作 生成器模式 异步流水线 自动生成比较 排序简化 核心价值:让 STL 更安全、更简洁、更高效

总结

C++20 这四个新特性,每一个都在解决 STL 使用中的痛点:

  • 概念与约束:让模板错误信息更友好,编译期就能发现问题
  • 范围库:简化迭代操作,视图链式处理数据不产生额外开销
  • 协程:为 STL 容器提供惰性生成数据的能力
  • 三路比较运算符:一行代码搞定所有比较逻辑,排序再也不用写一堆重载

说实话,这些特性刚出来时我也觉得「学不动了」。但真正用起来后,你会发现它们不是新玩具,而是实实在在能提升代码质量和开发效率的工具。从今天开始,试着在你的 STL 代码里用上它们吧。

一句话记住:概念约束模板,范围简化迭代,协程生成数据,飞船搞定比较——C++20 让 STL 焕然一新。


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