28、C++20对STL的增强:std::span、std::ranges库、std::format、协程与STL的交互

C++20 是 C++ 发展史上一个里程碑式的版本。说实话,它给 STL 带来的变化,比过去两个版本加起来还要多。我当年从 C++17 迁移到 C++20 时,第一感觉是「代码可以写得这么舒服?」。这一章,我们就来聊聊四个最实用的增强点:std::spanstd::rangesstd::format,以及协程怎么和 STL 配合。

核心要点:C++20 让 STL 更安全、更易读、更现代化。你不需要一次性掌握全部,但理解这几个特性,能让你在日常开发中少写很多 bug。

28.1 std::span:轻量级数组视图

先说说 std::span。它本质上就是一个「不拥有所有权的数组切片」。你想想看,以前我们要传递一个数组或 std::vector 的连续区间,要么传指针+长度,要么传迭代器对。这两种方式都容易出错——指针可能悬空,长度可能对不上。

std::span 解决了这个问题。它只保存一个指针和一个长度,不管理内存生命周期。我个人习惯在函数参数中用 std::span 替代 const std::vector<T>&,这样调用方可以传入数组、std::arraystd::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::filterstd::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::spanstd::ranges 都关注「如何更安全地操作数据」,std::format 关注「输出」,而协程则提供了一种新的「数据生成」方式。

C++20 STL 增强 std::span 轻量级数组视图 不拥有数据,只提供视图 替代指针+长度,更安全 std::ranges 算法新范式 惰性视图、管道操作 直接操作范围,无需迭代器对 std::format 现代格式化输出 类型安全,语法简洁 借鉴 Python 的 str.format 协程与 STL 可暂停恢复的函数 生成序列,配合 ranges 消费 注意生命周期管理 四个增强点共同提升了 C++ 的现代化编程体验

28.6 总结与建议

C++20 的这些增强,说白了就是让 C++ 更接近现代语言的使用体验。我个人建议你从 std::spanstd::format 开始入手,因为它们改动最小、收益最直接。然后逐步尝试 std::ranges 的视图操作,最后再研究协程——毕竟协程的学习曲线确实有点陡。

记住一点:这些新特性不是为了炫技,而是为了减少 bug、提高可读性。我在代码审查中经常看到有人用传统方式写了一大堆迭代器操作,其实用 std::ranges::views 三行就能搞定。嗯,这就是进步。