28、C++20对STL的增强:std::span、std::ranges库、std::format、协程与STL的交互
C++20 是 C++ 发展史上一个里程碑式的版本。说实话,它给 STL 带来的变化,比过去两个版本加起来还要多。我当年从 C++17 迁移到 C++20 时,第一感觉是「代码可以写得这么舒服?」。这一章,我们就来聊聊四个最实用的增强点:std::span、std::ranges、std::format,以及协程怎么和 STL 配合。
核心要点:C++20 让 STL 更安全、更易读、更现代化。你不需要一次性掌握全部,但理解这几个特性,能让你在日常开发中少写很多 bug。
28.1 std::span:轻量级数组视图
先说说 std::span。它本质上就是一个「不拥有所有权的数组切片」。你想想看,以前我们要传递一个数组或 std::vector 的连续区间,要么传指针+长度,要么传迭代器对。这两种方式都容易出错——指针可能悬空,长度可能对不上。
std::span 解决了这个问题。它只保存一个指针和一个长度,不管理内存生命周期。我个人习惯在函数参数中用 std::span 替代 const std::vector<T>&,这样调用方可以传入数组、std::array、std::vector,甚至 C 风格数组。
#include <span>
#include <vector>
#include <iostream>
void process_data(std::span<const int> data) {
for (auto v : data) {
std::cout << v << ' ';
}
}
int main() {
std::vector vec = {1, 2, 3, 4, 5};
int arr[] = {10, 20, 30};
process_data(vec); // 传入 vector
process_data(arr); // 传入 C 数组
process_data({100, 200}); // 传入初始化列表
}
避坑指南:我曾经在项目中用 std::span 保存了一个临时 std::vector 的引用,结果 vector 销毁后 span 变成了野指针。记住:std::span 不拥有数据,你必须保证原始数据的生命周期比 span 长。
28.2 std::ranges 库:算法的新范式
这是 C++20 最让我兴奋的部分。std::ranges 库重新定义了 STL 算法的使用方式。以前写 std::sort(v.begin(), v.end()),现在可以直接写 std::ranges::sort(v)。更关键的是,它引入了「视图(views)」的概念,支持惰性求值和管道操作。
为什么会这样?因为传统 STL 算法需要你手动管理迭代器对,而 ranges 库把「范围」作为一个整体概念。你想想看,这就像从「面向过程」升级到了「面向对象」——代码意图更清晰了。
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};
// 传统方式
std::vector<int> even;
std::copy_if(nums.begin(), nums.end(),
std::back_inserter(even),
[](int n) { return n % 2 == 0; });
// ranges 方式:管道操作,一目了然
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int v : result) {
std::cout << v << ' '; // 输出:4 16 36 64
}
}
个人经验:我在重构一个日志分析工具时,用 std::views::filter 和 std::views::take 替代了原来手写的循环。代码从 30 行缩减到 5 行,而且可读性大幅提升。不过要注意,视图是惰性求值的——只有当你遍历它时,才会真正执行过滤和变换。
28.3 std::format:终于有了现代格式化
说实话,C++ 的 << 流式输出用了这么多年,我一直觉得它又啰嗦又容易出错。C++20 引入了 std::format,它借鉴了 Python 的 str.format 语法,但更安全、更高效。
#include <format>
#include <iostream>
#include <string>
int main() {
std::string name = "Alice";
int age = 30;
double score = 95.5;
// 传统方式:流式输出
std::cout << "Name: " << name
<< ", Age: " << age
<< ", Score: " << score << '\n';
// C++20 方式:清晰、类型安全
std::cout << std::format("Name: {}, Age: {}, Score: {:.1f}\n",
name, age, score);
}
注意:std::format 在 GCC 和 Clang 中需要较新版本(GCC 13+,Clang 14+)。如果编译器不支持,可以考虑使用 {fmt} 库——它就是 std::format 的前身。我在一个嵌入式项目中就用了 {fmt} 库,因为它比 << 生成的代码更小。
28.4 协程与 STL 的交互
协程是 C++20 最复杂的新特性之一。它允许你编写「可暂停和恢复」的函数。但协程怎么和 STL 配合呢?最常见的场景是:用协程生成一个序列,然后通过 STL 算法消费它。
嗯,这里要注意:标准库目前没有直接提供「协程适配器」,但你可以自己写一个简单的生成器。我个人习惯用 std::generator(C++23 正式引入,但很多编译器已经支持了),或者自己封装一个。
#include <generator>
#include <ranges>
#include <iostream>
// 一个简单的协程生成器:生成斐波那契数列
std::generator<int> 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() {
// 用 ranges 算法消费协程生成的序列
auto fib_seq = fibonacci(10)
| std::views::filter([](int x) { return x % 2 == 0; });
for (int v : fib_seq) {
std::cout << v << ' '; // 输出:0 2 8 34
}
}
避坑指南:我曾经在协程中捕获了一个局部变量的引用,结果协程恢复时那个变量已经销毁了。记住:协程的局部变量在 co_yield 后仍然存活,但如果你捕获了外部栈上的引用,一定要确保外部对象的生命周期足够长。
28.5 知识体系总览
下面这张图总结了 C++20 对 STL 的四个核心增强,以及它们之间的关系。你可以看到,std::span 和 std::ranges 都关注「如何更安全地操作数据」,std::format 关注「输出」,而协程则提供了一种新的「数据生成」方式。
28.6 总结与建议
C++20 的这些增强,说白了就是让 C++ 更接近现代语言的使用体验。我个人建议你从 std::span 和 std::format 开始入手,因为它们改动最小、收益最直接。然后逐步尝试 std::ranges 的视图操作,最后再研究协程——毕竟协程的学习曲线确实有点陡。
记住一点:这些新特性不是为了炫技,而是为了减少 bug、提高可读性。我在代码审查中经常看到有人用传统方式写了一大堆迭代器操作,其实用 std::ranges::views 三行就能搞定。嗯,这就是进步。