一、C++20 对 STL 的增强:一场迟来的进化
说实话,C++20 是我这些年最期待的一个标准。为什么?因为它终于把 STL 从「好用但不够爽」的状态,推到了「嗯,这才对嘛」的层次。我个人习惯把 C++20 对 STL 的增强分成四大块:ranges 库、concepts、协程与 STL 的交互,以及 span 和 format 这两个实用工具。今天咱们就一个一个聊透。
核心观点:C++20 不是小修小补,而是让 STL 的「表达力」和「安全性」同时上了一个台阶。你写出来的代码,既更接近数学描述,又更难写出 bug。
1.1 知识体系总览
先看一张图,把今天要讲的内容串起来。我每次做技术分享都喜欢先画个框架,这样大家心里有数。
二、Ranges 库:终于不用手写循环了
Ranges 库,说白了就是给 STL 算法装上了「管道」和「视图」两个新武器。以前你要写 std::transform 套 std::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 惰性求值
这里有个关键点:视图是惰性的。什么意思?上面的 filter、transform、take 并不会立即执行,而是等到你遍历 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_iterator 和 std::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::print 和 std::println,可以直接打印到控制台,省去 std::cout 的麻烦。
我的习惯:从 C++20 开始,我在新项目中全面禁用 printf 和 sprintf,全部改用 std::format。为什么?因为 printf 的类型不匹配问题(比如用 %d 打印 size_t)在大型项目中真的很难排查。format 在编译期就帮你检查好了。
七、总结
C++20 对 STL 的增强,说白了就是让 C++ 更「现代」、更「安全」、更「好用」。Ranges 库让你少写循环,Concepts 让你少看天书错误,协程让异步生成器变得自然,span 让数组操作更安全,format 让字符串格式化更优雅。
我个人觉得,这些特性不是锦上添花,而是实实在在能提升代码质量和开发效率的。如果你还在用 C++17 甚至更早的标准,我建议你尽快升级编译器,开始尝试 C++20。你写出来的代码,自己看着舒服,别人 review 也轻松。
嗯,今天就聊到这里。希望这些内容对你有帮助。