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 使用中的痛点:
- 概念与约束:让模板错误信息更友好,编译期就能发现问题
- 范围库:简化迭代操作,视图链式处理数据不产生额外开销
- 协程:为 STL 容器提供惰性生成数据的能力
- 三路比较运算符:一行代码搞定所有比较逻辑,排序再也不用写一堆重载
说实话,这些特性刚出来时我也觉得「学不动了」。但真正用起来后,你会发现它们不是新玩具,而是实实在在能提升代码质量和开发效率的工具。从今天开始,试着在你的 STL 代码里用上它们吧。
一句话记住:概念约束模板,范围简化迭代,协程生成数据,飞船搞定比较——C++20 让 STL 焕然一新。
公众号:蓝海资料掘金营,微信 deep3321