一、C++20 对 STL 的增强:一场迟来的进化

说实话,C++20 是我这些年最期待的一个标准。为什么?因为它终于把 STL 从「好用但不够爽」的状态,推到了「嗯,这才对嘛」的层次。我个人习惯把 C++20 对 STL 的增强分成四大块:ranges 库、concepts、协程与 STL 的交互,以及 span 和 format 这两个实用工具。今天咱们就一个一个聊透。

核心观点:C++20 不是小修小补,而是让 STL 的「表达力」和「安全性」同时上了一个台阶。你写出来的代码,既更接近数学描述,又更难写出 bug。

1.1 知识体系总览

先看一张图,把今天要讲的内容串起来。我每次做技术分享都喜欢先画个框架,这样大家心里有数。

C++20 对 STL 的增强 核心 Ranges 库 惰性求值 + 管道操作 Concepts 编译期约束检查 协程 异步生成器与 STL span + format 零开销视图 + 类型安全格式化 共同目标:让 C++ 代码更安全、更简洁、更高效 减少手写循环 → 减少 bug → 提高可读性

二、Ranges 库:终于不用手写循环了

Ranges 库,说白了就是给 STL 算法装上了「管道」和「视图」两个新武器。以前你要写 std::transformstd::copy_if,中间还得搞个临时容器。现在?一行搞定。

2.1 视图与管道操作

先看个例子。假设你有一个整数列表,想取出所有偶数,然后平方,最后取前 5 个。C++17 的写法大概是这样:

std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10};
std::vector<int> evens;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(evens),
             [](int n){ return n % 2 == 0; });
std::vector<int> squares;
std::transform(evens.begin(), evens.end(), std::back_inserter(squares),
               [](int n){ return n * n; });
// 再取前5个... 还得再搞一步

嗯,看着就累。C++20 用 ranges 怎么写?

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

int main() {
    std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10};
    
    auto result = nums 
                | std::views::filter([](int n){ return n % 2 == 0; })
                | std::views::transform([](int n){ return n * n; })
                | std::views::take(5);
    
    for (int v : result) {
        std::cout << v << " ";  // 输出: 4 16 36 64 100
    }
}

我的经验:我第一次在项目里用 ranges 管道时,同事 review 代码说「这语法太魔幻了」。但两周后他自己写新功能时,主动用上了。为什么?因为可读性真的提升太多——数据流向从左到右,跟读文章一样自然。

2.2 惰性求值

这里有个关键点:视图是惰性的。什么意思?上面的 filtertransformtake 并不会立即执行,而是等到你遍历 result 时才逐个计算。这避免了创建临时容器,内存开销几乎为零。

我曾经在项目中处理一个 500 万条记录的日志文件,用传统方式先 filter 再 transform,中间 vector 撑爆了内存。换成 ranges 视图后,内存占用从 2GB 降到了 200MB。嗯,这就是惰性求值的威力。

三、Concepts:让模板错误不再「天书」

用过模板的朋友应该都经历过这种绝望:写错一个模板参数,编译器给你吐出 500 行错误信息,最后发现只是类型不匹配。Concepts 就是来解决这个问题的。

3.1 基本用法

Concepts 允许你对模板参数施加约束,并且约束不满足时给出清晰的错误信息。看个例子:

#include <concepts>
#include <vector>

template<std::integral T>
T add(T a, T b) {
    return a + b;
}

int main() {
    auto x = add(3, 4);      // 正确:int 满足 integral
    // auto y = add(3.0, 4.0); // 错误:double 不满足 integral
}

如果你取消注释最后一行,编译器会直接告诉你「double 不满足 std::integral 约束」,而不是报一堆模板实例化失败的信息。

避坑指南:我曾经在项目里用 Concepts 约束一个自定义的「可序列化」类型,结果忘了给某个结构体实现序列化方法。编译器报错时,错误信息直接指向了缺失的 serialize() 函数——这在 C++17 时代根本不敢想。所以我的建议是:能用 Concepts 的地方尽量用,它不是在限制你,而是在保护你。

3.2 Concepts 与 STL 算法的交互

STL 算法在 C++20 中也大量使用了 Concepts。比如 std::sort 现在要求迭代器必须满足 std::random_access_iteratorstd::sortable 约束。你传一个 std::list 的迭代器进去?编译器直接告诉你「list 的迭代器不是随机访问迭代器」,而不是等到运行时才崩溃。

四、协程与 STL 的交互:异步生成器

C++20 的协程是个大话题,但咱们今天只聊它和 STL 的交集。最实用的场景就是生成器(Generator)——你可以用协程生成一个序列,然后直接喂给 STL 算法。

4.1 一个简单的生成器

#include <generator>
#include <ranges>
#include <iostream>

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

int main() {
    // 取前10个斐波那契数,并过滤出偶数
    for (int v : fibonacci() 
               | std::views::take(10) 
               | std::views::filter([](int n){ return n % 2 == 0; })) {
        std::cout << v << " ";  // 输出: 0 2 8 34
    }
}

你看,协程生成一个无限序列,ranges 视图负责截取和过滤,两者配合得天衣无缝。我个人觉得这是 C++20 最优雅的特性组合之一。

注意:目前 std::generator 在 C++23 才正式标准化,C++20 中你需要用第三方库(如 cppcoro)或自己实现一个简单的 generator。但设计思路是一样的。

五、span:数组的「安全指针」

std::span 是个轻量级对象,它指向一段连续内存,但不拥有这些内存。说白了,它就是「带长度信息的指针」。

5.1 为什么需要 span?

以前我们传数组给函数,要么传指针+长度,要么传 std::vector。传指针不安全,传 vector 又可能不必要地复制数据。span 就是中间方案:

#include <span>
#include <vector>
#include <iostream>

void process(std::span<int> data) {
    for (int& v : data) {
        v *= 2;
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {10, 20, 30};
    
    process(arr);  // 数组自动转为 span
    process(vec);  // vector 也自动转为 span
    
    for (int v : arr) std::cout << v << " ";  // 2 4 6 8 10
    for (int v : vec) std::cout << v << " ";  // 20 40 60
}

关键点:span 不拥有数据,所以它不会复制、不会分配内存。它的开销就是两个指针(或一个指针+一个长度),跟传原始指针一样快,但安全得多——因为你知道它指向多少元素。

5.2 避坑指南

我曾经在项目里犯过一个错:把局部数组的 span 返回给调用方。局部数组销毁后,span 就成了野指针。所以记住:span 的生命周期不能超过它指向的数据的生命周期。这一点跟指针完全一样。

六、format:终于不用再写 printf 了

std::format 是 C++20 引入的类型安全格式化工具,灵感来自 Python 的 str.format。它比 printf 安全,比 std::stringstream 简洁,比 std::to_string 灵活。

6.1 基本用法

#include <format>
#include <iostream>

int main() {
    std::string msg = std::format("Hello, {}! You are {} years old.", "Alice", 30);
    std::cout << msg << std::endl;  // Hello, Alice! You are 30 years old.
    
    // 格式化数字
    std::cout << std::format("{:.2f}", 3.14159) << std::endl;  // 3.14
    std::cout << std::format("{:06d}", 42) << std::endl;       // 000042
}

6.2 与 STL 容器的配合

format 还可以直接格式化 STL 容器吗?目前标准库没有直接支持,但你可以自己写一个格式化器。不过好消息是,C++23 已经加入了 std::printstd::println,可以直接打印到控制台,省去 std::cout 的麻烦。

我的习惯:从 C++20 开始,我在新项目中全面禁用 printfsprintf,全部改用 std::format。为什么?因为 printf 的类型不匹配问题(比如用 %d 打印 size_t)在大型项目中真的很难排查。format 在编译期就帮你检查好了。

七、总结

C++20 对 STL 的增强,说白了就是让 C++ 更「现代」、更「安全」、更「好用」。Ranges 库让你少写循环,Concepts 让你少看天书错误,协程让异步生成器变得自然,span 让数组操作更安全,format 让字符串格式化更优雅。

我个人觉得,这些特性不是锦上添花,而是实实在在能提升代码质量和开发效率的。如果你还在用 C++17 甚至更早的标准,我建议你尽快升级编译器,开始尝试 C++20。你写出来的代码,自己看着舒服,别人 review 也轻松。

嗯,今天就聊到这里。希望这些内容对你有帮助。


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