C++ 范围循环与 Ranges 库:从 C++11 到 C++23 的演进 前言 如果你写过 C++,一定用过这样的代码:
1 2 3 for (size_t i = 0 ; i < vec.size (); ++i) { std::cout << vec[i] << std::endl; }
或者用迭代器:
1 2 3 for (auto it = vec.begin (); it != vec.end (); ++it) { std::cout << *it << std::endl; }
这些写法没错,但不够优雅。C++11 引入的基于范围的 for 循环 (Range-based for loop)让遍历容器变得前所未有的简洁。而 C++20 的 Ranges 库 则彻底改变了我们处理序列数据的方式。
本文将带你系统了解 C++ 在“范围”这一主题上的所有重要新特性,从 C++11 到 C++23,一路进化。
一、C++11:基于范围的 for 循环 1.1 基本语法 C++11 引入的基于范围的 for 循环,是传统 for 循环的“更具可读性的等效项”。它的语法非常直观:
1 2 3 for (declaration : range) { }
其中 range 可以是数组、容器,或者任何提供了 begin() 和 end() 的类型。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> #include <vector> int main () { std::vector<int > numbers = {1 , 2 , 3 , 4 , 5 }; for (int n : numbers) { std::cout << n << " " ; } std::cout << std::endl; for (int & n : numbers) { n *= 2 ; } for (const auto & n : numbers) { std::cout << n << " " ; } return 0 ; }
1.2 与 auto 结合 auto 是范围 for 循环的最佳搭档,让代码更加简洁:
1 2 3 4 5 std::vector<std::string> words = {"Hello" , "World" , "C++" };for (const auto & word : words) { std::cout << word << std::endl; }
编译器会自动推导出正确的类型,你无需关心元素的具体类型。
1.3 底层原理 基于范围的 for 循环在底层会被展开为类似这样的代码:
1 2 3 4 5 6 7 8 9 { auto && __range = range-initializer; auto __begin = begin-expr; auto __end = end-expr; for ( ; __begin != __end; ++__begin) { item-declaration = *__begin; statement } }
理解这个展开有助于你明白:range-initializer 中的临时对象的生命周期会被延长至整个循环结束 。
二、C++17:结构化绑定 + 范围 for C++17 允许在范围 for 循环的声明部分使用结构化绑定 (Structured Binding)。
这意味着你可以直接解构容器中的元素,比如遍历 std::map 时直接拿到 key 和 value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <map> #include <string> int main () { std::map<std::string, int > scores = { {"Alice" , 95 }, {"Bob" , 87 }, {"Charlie" , 72 } }; for (const auto & [name, score] : scores) { std::cout << name << ": " << score << std::endl; } return 0 ; }
在 C++17 之前,你只能写成:
1 2 3 for (const auto & pair : scores) { std::cout << pair.first << ": " << pair.second << std::endl; }
结构化绑定让代码更加清晰、语义更加直接。
另外,C++17 还放宽了范围 for 循环中 begin 和 end 的类型要求——它们不再必须是相同的类型。这为 Sentinel(哨位)机制奠定了基础,而 Sentinel 正是 C++20 Ranges 库的重要概念之一。
三、C++20:初始化语句 3.1 语法扩展 C++20 在范围 for 循环中引入了初始化语句 (Init Statement)。新语法如下:
1 2 3 for (init-statement; declaration : range-initializer) { }
3.2 使用场景 场景一:限制变量的作用域
在 C++20 之前,如果需要在循环中使用索引,必须在外部声明:
1 2 3 4 5 6 size_t i = 0 ;for (auto & d : data) { std::cout << i++ << ' ' << d << '\n' ; }
C++20 可以将 i 的作用域限制在循环内部:
1 2 3 4 5 6 7 8 9 10 11 12 #include <array> #include <string> int main () { std::array<std::string, 3> data = {"hello" , "," , "world" }; for (std::size_t i = 0 ; auto & d : data) { std::cout << i++ << ' ' << d << '\n' ; } return 0 ; }
场景二:安全地迭代临时对象
C++20 允许在循环中创建临时对象并迭代:
1 2 3 4 5 6 7 8 9 #include <iostream> #include <string> int main () { for (std::string v = std::to_string (42 ); auto c : v) { std::cout << c << '\n' ; } return 0 ; }
场景三:避免未定义行为
当迭代的 range 是临时对象的成员时,C++20 的初始化语句可以避免未定义行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Foo {public : const auto & items () const { return data; }private : std::vector<const char *> data{"hello" , "," , "world" }; };Foo getFoo () { return Foo (); }int main () { for (auto foo = getFoo (); auto & d : foo.items ()) { std::cout << d << '\n' ; } return 0 ; }
如果没有初始化语句,直接对 getFoo().items() 进行迭代,可能会因为临时对象提前销毁而导致未定义行为。
四、C++20:Ranges 库 如果说范围 for 循环是小试牛刀,那么 C++20 的 Ranges 库 就是一场革命。
4.1 核心概念 Ranges 库是算法和迭代器库的扩展与泛化,通过使其可组合且不易出错,从而让功能更强大。
范围(Range) :一个可迭代的序列。它不限于标准库容器,还可以是数组、输入输出流、指针区间,甚至是动态生成的序列。
视图(View) :范围的轻量级表示,不拥有数据,只是提供一种观察数据的方式。视图的计算是惰性 (Lazy)的——只有在实际迭代时才会进行计算。
4.2 范围工厂 Ranges 库提供了一系列范围工厂,用于创建各种范围:
工厂
说明
std::views::empty
空范围
std::views::single
单元素范围
std::views::iota
递增序列(如 0, 1, 2, ...)
std::views::repeat
无限重复的序列
std::views::istream
从输入流读取
示例:使用 iota 生成序列
1 2 3 4 5 6 7 8 9 10 11 #include <iostream> #include <ranges> int main () { for (int i : std::views::iota (0 , 10 )) { std::cout << i << " " ; } return 0 ; }
4.3 范围适配器 范围适配器(Range Adaptor)是 Ranges 库的核心武器,它们可以对范围进行各种变换,并且可以组合成管道 (Pipeline)。
常用适配器包括:
适配器
说明
std::views::filter
筛选满足条件的元素
std::views::transform
对每个元素应用变换函数
std::views::take
取前 N 个元素
std::views::drop
丢弃前 N 个元素
std::views::reverse
反转
std::views::keys / values
取 map 的 key 或 value
示例:管道操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <vector> #include <ranges> int main () { std::vector<int > numbers = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }; auto result = numbers | std::views::filter ([](int n) { return n % 2 == 0 ; }) | std::views::transform ([](int n) { return n * 2 ; }) | std::views::take (3 ); for (int n : result) { std::cout << n << " " ; } return 0 ; }
注意:所有这些操作都是惰性 的。result 只是一个视图,实际的筛选、变换和截取操作只在 for 循环迭代时才发生。没有中间容器被创建,这带来了显著的性能优势。
4.4 范围算法 C++20 还在 std::ranges 命名空间中提供了算法的新版本,直接接受范围作为参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <algorithm> #include <vector> #include <ranges> int main () { std::vector<int > numbers = {5 , 2 , 8 , 1 , 9 , 3 }; std::sort (numbers.begin (), numbers.end ()); std::ranges::sort (numbers); auto even = numbers | std::views::filter ([](int n) { return n % 2 == 0 ; }); return 0 ; }
4.5 关于命名空间 C++20 提供了命名空间别名 std::views = std::ranges::views,所以你既可以写 std::ranges::views::filter,也可以简写为 std::views::filter。本文统一使用 std::views。
五、C++23:Ranges 库的进一步扩展 C++20 的 Ranges 库已经很强大,但还有一些“拼图”缺失——比如同时遍历多个容器、获取元素索引等。C++23 填补了这些空白。
5.1 std::views::zip —— 同步遍历多个范围 std::views::zip 是 C++23 引入的范围适配器,它接受一个或多个视图,生成一个视图,其第 i 个元素是由所有视图的第 i 个元素组成的元组。
关键特性 :
结果视图的长度等于最短 的输入范围
元素是引用 (std::tuple<T&, U&, ...>),修改会反映到原始数据
完美支持结构化绑定
惰性求值 ,不创建中间容器
示例:同时遍历两个向量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> #include <vector> #include <string> #include <ranges> int main () { std::vector<std::string> names = {"Alice" , "Bob" , "Charlie" }; std::vector<int > scores = {95 , 87 , 72 }; for (const auto & [name, score] : std::views::zip (names, scores)) { std::cout << name << " scored " << score << std::endl; } return 0 ; }
对比 C++23 之前 ,你需要手动管理索引:
1 2 3 4 5 auto size = std::min (names.size (), scores.size ());for (size_t i = 0 ; i < size; ++i) { std::cout << names[i] << " scored " << scores[i] << std::endl; }
zip 也支持两个以上的范围:
1 2 3 4 5 6 7 std::vector<int > ids = {101 , 102 , 103 }; std::vector<std::string> names = {"Alice" , "Bob" , "Charlie" }; std::vector<double > scores = {95.5 , 88.0 , 92.3 };for (const auto & [id, name, score] : std::views::zip (ids, names, scores)) { std::cout << "ID: " << id << ", Name: " << name << ", Score: " << score << std::endl; }
如果调用 std::views::zip() 而不传入任何参数,它会生成一个空的 tuple 视图。
zip_transform 是 zip 的升级版——它接受一个可调用对象,直接将 zipped 的元素转换为结果,而不是产生元组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <vector> #include <ranges> int main () { std::vector<double > prices = {10.5 , 20.0 , 30.75 }; std::vector<int > quantities = {3 , 5 , 2 }; auto totals = std::views::zip_transform ( [](double price, int qty) { return price * qty; }, prices, quantities ); for (double total : totals) { std::cout << total << " " ; } return 0 ; }
zip_transform 等价于 zip(r1, r2) | transform(apply(f)),但更高效——避免构造中间的元组。
5.3 std::views::enumerate —— 带索引遍历 enumerate 将范围包装为 {index, value} 对的视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <vector> #include <ranges> int main () { std::vector<std::string> fruits = {"apple" , "banana" , "cherry" }; for (const auto & [idx, fruit] : std::views::enumerate (fruits)) { std::cout << idx << ": " << fruit << std::endl; } return 0 ; }
每个元素是一个 std::tuple<difference_type, range_reference_t<R>>,在实践中可以用结构化绑定解构为 auto [idx, value]。
5.4 std::views::join_with —— 带分隔符的拼接 join_with 将嵌套范围(如 vector<vector<int>>)扁平化,并在每个子范围之间插入指定的分隔符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include <vector> #include <ranges> int main () { std::vector<std::vector<int >> matrix = { {1 , 2 }, {3 }, {}, {4 , 5 } }; auto joined = matrix | std::views::join_with (-1 ); for (int n : joined) { std::cout << n << " " ; } return 0 ; }
对于字符串集合也同样适用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <vector> #include <string> #include <ranges> #include <iostream> int main () { std::vector<std::string> words = {"Hello" , "World" , "C++23" }; auto joined = words | std::views::join_with (' ' ); for (char c : joined) { std::cout << c; } std::cout << '\n' ; return 0 ; }
5.5 std::views::chunk —— 分块处理 chunk 将范围按指定大小分块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> #include <vector> #include <ranges> int main () { std::vector<int > numbers = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }; auto chunks = numbers | std::views::chunk (3 ); for (const auto & chunk : chunks) { std::cout << "[ " ; for (int n : chunk) { std::cout << n << " " ; } std::cout << "] " ; } return 0 ; }
5.6 std::ranges::to —— 将视图转换为容器 C++23 还引入了 std::ranges::to,可以方便地将视图转换为具体的容器类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <vector> #include <ranges> #include <string> int main () { auto result = std::views::iota (1 , 10 ) | std::views::filter ([](int n) { return n % 2 == 0 ; }) | std::views::transform ([](int n) { return std::to_string (n); }) | std::ranges::to <std::vector>(); return 0 ; }
这在需要将视图的惰性计算结果“物化”为实际容器时非常有用。
六、最佳实践与注意事项 6.1 视图的生命周期 视图不拥有数据,只是数据的“观察者”。必须确保原始数据在视图使用期间保持有效 。
1 2 3 4 5 6 7 8 9 10 11 auto get_view () { std::vector<int > vec = {1 , 2 , 3 }; return vec | std::views::filter ([](int n) { return n > 1 ; }); } std::vector<int > vec = {1 , 2 , 3 };auto view = vec | std::views::filter ([](int n) { return n > 1 ; });
6.2 避免过度组合 虽然视图组合很优雅,但过长的管道链可能增加编译时间和运行时开销。对于复杂操作,考虑适当拆分:
1 2 3 4 5 6 7 8 9 10 11 auto result = data | views::filter (pred1) | views::transform (func1) | views::filter (pred2) | views::transform (func2) | views::take (n);auto filtered = data | views::filter (pred1) | views::transform (func1);auto result = filtered | views::filter (pred2) | views::transform (func2) | views::take (n);
6.3 选择合适的工具
场景
推荐方案
简单遍历单个容器
范围 for 循环
遍历时需索引
C++23: views::enumerate / C++20: 初始化语句
筛选 + 变换
views::filter + views::transform
同时遍历多个容器
views::zip
扁平化嵌套容器
views::join_with
分块处理
views::chunk
需要实际容器结果
ranges::to<T>()
七、编译器支持一览
特性
标准
GCC
Clang
MSVC
范围 for 循环
C++11
✅
✅
✅
结构化绑定
C++17
✅
✅
✅
初始化语句
C++20
✅
✅
✅
Ranges 库
C++20
GCC 10+
Clang 13+
MSVC 19.28+
views::zip
C++23
GCC 13+
Clang 15+
MSVC 19.34+
views::enumerate
C++23
GCC 13+
Clang 15+
MSVC 19.34+
views::join_with
C++23
GCC 14+
Clang 17+
MSVC 19.34+
ranges::to
C++23
GCC 14+
Clang 17+
MSVC 19.34+
使用前请确认你的编译器版本支持相应特性。
总结 从 C++11 的基于范围的 for 循环,到 C++17 的结构化绑定,再到 C++20 的 Ranges 库和 C++23 的进一步完善,C++ 在“范围”这条路上越走越远:
C++11 :用 for (auto x : range) 替代了繁琐的索引和迭代器循环
C++17 :用结构化绑定让 map 等容器的遍历更加优雅
C++20 :用 Ranges 库引入了视图、管道和范围算法,让数据处理变得声明式
C++23 :用 zip、enumerate、join_with、chunk 填补了最后的拼图
这些特性共同指向一个方向:让 C++ 代码更加简洁、安全、富有表达力 。掌握它们,你的 C++ 编程体验将提升到一个全新的层次。
本文代码均可在支持相应 C++ 标准的编译器中编译运行。欢迎在评论区交流讨论!