C++ Ranges 完全指南
为什么需要 Ranges?
在 C++20 之前,处理数据序列是这样的:
// 传统 STL:冗长、易错
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
std::vector<int> result;
for (int n : nums) {
if (n % 2 == 0) {
result.push_back(n * n);
}
}
// 或者用算法(更复杂)
std::vector<int> result;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result),
[](int n) { return n % 2 == 0; });
std::transform(result.begin(), result.end(), result.begin(),
[](int n) { return n * n; });有了 Ranges,代码变得如此简洁:
// Ranges:声明式、可读性强
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::ranges::to<std::vector>(); // C++23核心优势:
- ✅ 管道式操作:用
|链式组合,像 Unix 管道一样直观 - ✅ 懒惰求值:只在需要时计算,避免不必要的中间容器
- ✅ 直接操作容器:无需
begin()/end()迭代器对 - ✅ 投影支持:轻松操作结构体成员,无需复杂 lambda
- ✅ 类型安全:编译期检查,防止悬空迭代器
核心概念速览
Range(范围)
任何可迭代的序列,只要提供 begin() 和 end():
- 容器:
std::vector、std::array、std::string - 视图:
std::string_view、std::span - C 风格数组
View(视图)
轻量级、懒惰的范围表示:
- 不复制数据,只保存引用
- 按需计算,直到迭代时才执行
- 可组合,用
|管道连接
Adaptor(适配器)
修改范围而不改变底层数据:
views::filter- 过滤views::transform- 转换views::take- 取前 N 个views::reverse- 反转
Algorithm(算法)
Ranges 版本的算法,直接接受范围:
std::ranges::sort(vec)而不是std::sort(vec.begin(), vec.end())- 支持投影:
std::ranges::sort(people, {}, &Person::age)
快速上手:5 分钟掌握 Ranges
基本过滤和转换
#include <print>
#include <vector>
#include <ranges>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
// 过滤偶数并平方
auto even_squares = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// for (int sq : even_squares) {
// std::cout << sq << " "; // 输出: 4 16 36
// }
std::println("{}", even_squares);
}关键点:
|操作符从左到右组合视图。- 计算是懒惰的,只在循环时执行。
even_squares是视图,不拥有数据。- 参数传递:在
filter和transform的 lambda 中,推荐使用auto&&。
为什么在 Adaptor 中使用 auto&&?
在 Ranges 的管道操作中,元素在不同视图间传递。使用 auto&&(转发引用)是处理 Ranges 的“黄金法则”。
参数传递方式对比:
| 传递方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
auto | 简单,安全(值拷贝) | 产生拷贝开销;无法处理不可拷贝类型 | 小型内置类型(int, double) |
const auto& | 避免拷贝;只读安全 | 无法绑定到某些代理对象(如 vector<bool> 的引用);无法修改元素 | 传统的只读大型对象 |
auto&& | 万能绑定;零拷贝;支持代理对象 | 语法稍显复杂 | Ranges Adaptor 的默认首选 |
核心优势:
- 避免拷贝:对于大型对象,直接引用比拷贝更高效。
- 支持修改:如果你需要通过
transform修改元素(虽然通常建议保持纯函数风格),auto&&可以绑定到左值。 - 处理代理类型:这是最关键的一点。某些视图(如
views::zip或vector<bool>)返回的是代理对象(proxy objects),这些对象通常是临时值但表现得像引用。const auto&可能会延长生命周期但无法调用非 const 成员,而auto会触发不必要的拷贝或编译错误。auto&&是捕获它们的唯一正确且通用的方式。
// 推荐写法:使用 auto&& 保持通用性和性能
auto result = nums
| std::views::filter([](auto&& n) { return n % 2 == 0; })
| std::views::transform([](auto&& n) { return n * n; });深入理解 auto&&:转发引用的机制
auto&& 是 C++11 引入的转发引用(Forwarding Reference),也称为万能引用。它能够根据初始化表达式的值类别自动推导为左值引用或右值引用。
推导规则:
template<typename T>
void func(T&& arg); // T&& 是转发引用
// 当传入左值时,T 推导为左值引用类型
int x = 42;
func(x); // T 推导为 int&,arg 类型为 int& && → int&(引用折叠)
// 当传入右值时,T 推导为非引用类型
func(42); // T 推导为 int,arg 类型为 int&&引用折叠规则:
| T 类型 | T&& 实际类型 |
|---|---|
int | int&&(右值引用) |
int& | int&(左值引用) |
int&& | int&&(右值引用) |
在 Ranges 中的应用:
std::vector<int> nums = {1, 2, 3};
// 元素通过迭代器传递给 lambda
auto filtered = nums | std::views::filter([](auto&& n) {
// n 的类型取决于迭代器的解引用结果
// 对于 vector<int>,*it 返回 int&,所以 n 是 int&
return n % 2 == 0;
});auto&& 的陷阱与注意事项
陷阱 1:意外修改原始数据
std::vector<int> nums = {1, 2, 3, 4, 5};
// ❌ 危险:auto&& 允许修改,但 filter 应该是纯函数
auto filtered = nums | std::views::filter([](auto&& n) {
n *= 2; // 修改了原始 vector!
return n % 2 == 0;
});
// nums 现在是 {2, 4, 6, 8, 10}解决方案:
// ✅ 使用 const auto& 确保只读(如果不需要处理代理对象)
auto filtered = nums | std::views::filter([](const auto& n) {
return n % 2 == 0;
});
// ✅ 或者使用 std::forward 保持值类别但明确意图
auto filtered = nums | std::views::filter([](auto&& n) {
auto val = std::forward<decltype(n)>(n); // 转发但不修改
return val % 2 == 0;
});陷阱 2:生命周期问题
// ❌ 危险:返回对临时对象的引用
auto getFiltered() {
std::vector<int> temp = {1, 2, 3};
return temp | std::views::filter([](auto&& n) {
return n % 2 == 0;
}); // temp 被销毁,视图悬空!
}
// ✅ 解决:立即收集结果
auto getFiltered() {
std::vector<int> temp = {1, 2, 3};
return temp
| std::views::filter([](auto&& n) { return n % 2 == 0; })
| std::ranges::to<std::vector>(); // 返回新 vector
}陷阱 3:过度使用 std::forward
// ❌ 不必要:在 filter 中使用 std::forward
auto filtered = nums | std::views::filter([](auto&& n) {
return std::forward<decltype(n)>(n) % 2 == 0; // 没有意义
});
// ✅ 简单直接:直接使用 n
auto filtered = nums | std::views::filter([](auto&& n) {
return n % 2 == 0;
});何时使用 std::forward:
// ✅ 需要转发时使用(如在 transform 中构造对象)
struct Widget {
Widget(int x) : value(x) {}
Widget(const Widget&) = delete; // 不可拷贝
Widget(Widget&&) = default; // 只能移动
int value;
};
std::vector<Widget> widgets = {Widget(1), Widget(2), Widget(3)};
// 使用 std::forward 保持移动语义
auto transformed = widgets | std::views::transform([](auto&& w) {
return Widget(std::forward<decltype(w)>(w).value * 2);
});实战指南:何时使用哪种参数类型
决策树:
是否需要修改元素?
├─ 是 → 使用 auto&&(谨慎使用)
└─ 否 → 是否需要处理代理对象?
├─ 是 → 使用 auto&&
└─ 否 → 元素类型是否小型可拷贝?
├─ 是 → 使用 auto(简单)
└─ 否 → 使用 const auto&具体场景示例:
// 场景 1:简单内置类型,只读
auto result = nums | std::views::filter([](auto n) { // auto 足够
return n > 0;
});
// 场景 2:大型对象,只读
struct BigData {
std::vector<int> data;
// ... 大量数据
};
std::vector<BigData> items;
auto result = items | std::views::filter([](const auto& item) { // const auto& 避免拷贝
return !item.data.empty();
});
// 场景 3:处理 vector<bool> 的代理对象
std::vector<bool> flags = {true, false, true};
auto result = flags | std::views::filter([](auto&& flag) { // 必须用 auto&&
return flag; // flag 是 vector<bool>::reference(代理对象)
});
// 场景 4:zip 返回的 tuple 引用
std::vector<int> a = {1, 2, 3};
std::vector<std::string> b = {"a", "b", "c"};
auto zipped = std::views::zip(a, b);
auto result = zipped | std::views::filter([](auto&& pair) { // 必须用 auto&&
auto& [num, str] = pair; // pair 是 tuple<int&, std::string&>
return num > 1;
});
// 场景 5:transform 中需要转发移动语义
auto result = widgets | std::views::transform([](auto&& w) {
return std::forward<decltype(w)>(w).value * 2; // 保持移动语义
});性能对比:不同参数类型的开销
#include <benchmark/benchmark.h>
#include <vector>
#include <ranges>
struct ExpensiveObject {
std::vector<int> data;
int key;
ExpensiveObject() : data(1000), key(0) {}
};
class BenchmarkFixture : public benchmark::Fixture {
public:
void SetUp(const benchmark::State& state) override {
objects.resize(10000);
for (int i = 0; i < 10000; ++i) {
objects[i].key = i;
}
}
void TearDown(const benchmark::State& state) override {
objects.clear();
objects.shrink_to_fit();
}
std::vector<ExpensiveObject> objects;
};
BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_AutoValue)(benchmark::State& state) {
for (auto _ : state) {
auto r = objects | std::views::filter([](auto obj) {
return obj.key % 2 == 0;
});
int count = 0;
for (auto& obj : r) count++;
benchmark::DoNotOptimize(count);
}
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_AutoValue);
BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_ConstAutoRef)(benchmark::State& state) {
for (auto _ : state) {
auto r = objects | std::views::filter([](const auto& obj) {
return obj.key % 2 == 0;
});
int count = 0;
for (auto& obj : r) count++;
benchmark::DoNotOptimize(count);
}
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_ConstAutoRef);
BENCHMARK_DEFINE_F(BenchmarkFixture, BM_Filter_AutoForwardRef)(benchmark::State& state) {
for (auto _ : state) {
auto r = objects | std::views::filter([](auto&& obj) {
return obj.key % 2 == 0;
});
int count = 0;
for (auto& obj : r) count++;
benchmark::DoNotOptimize(count);
}
}
BENCHMARK_REGISTER_F(BenchmarkFixture, BM_Filter_AutoForwardRef);
BENCHMARK_MAIN();运行结果:
| Benchmark | Time | CPU | Iterations |
|---|---|---|---|
| BenchmarkFixture/BM_Filter_AutoValue | 977061 ns | 971965 ns | 715 |
| BenchmarkFixture/BM_Filter_ConstAutoRef | 2837 ns | 2827 ns | 247517 |
| BenchmarkFixture/BM_Filter_AutoForwardRef | 2854 ns | 2844 ns | 246198 |
auto:最慢(每次调用都拷贝整个对象)const auto&:快(零拷贝)auto&&:与const auto&相当(零拷贝,但更通用)
高级技巧:完美转发在 Ranges 中的应用
技巧 1:在 transform 中完美转发
// 创建对象的工厂函数
template<typename T>
auto makeObject(T&& arg) {
return Object(std::forward<T>(arg));
}
// 在 transform 中使用
auto result = inputs | std::views::transform([](auto&& input) {
return makeObject(std::forward<decltype(input)>(input));
});技巧 2:处理可变参数
// 使用 zip 和 tuple 解构
auto pairs = std::views::zip(vec1, vec2);
auto result = pairs | std::views::transform([](auto&& pair) {
auto& [a, b] = pair;
return std::forward<decltype(a)>(a) + std::forward<decltype(b)>(b);
});技巧 3:条件性转发
// 根据条件决定是否转发
auto result = items | std::views::transform([](auto&& item) {
if (shouldCopy(item)) {
return item; // 拷贝
} else {
return std::move(item); // 移动
}
});总结:auto&& 最佳实践清单
✅ 推荐使用 auto&& 的场景:
- Ranges adaptor 的 lambda 参数(默认首选)
- 处理代理对象(
vector<bool>、zip、transform等) - 需要保持值类别进行完美转发
- 不确定元素类型或值类别时
❌ 避免使用 auto&& 的场景:
- 只需要简单读取且元素类型小型可拷贝(用
auto) - 只需要读取且确定是普通对象(用
const auto&) - 不需要转发语义(避免过度使用
std::forward)
⚠️ 注意事项:
auto&&允许修改,确保不会意外修改原始数据- 注意视图的生命周期,避免悬空引用
- 在
filter中通常不需要std::forward - 在
transform中需要转发时才使用std::forward
🎯 黄金法则:
在 Ranges adaptor 中,默认使用
auto&&。只有在明确知道元素类型且不需要处理代理对象时,才考虑使用const auto&或auto。
按结构体成员排序(投影)
#include <iostream>
#include <vector>
#include <ranges>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// 传统方式:需要写 lambda
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) { return a.age < b.age; });
// Ranges 方式:使用投影
std::ranges::sort(people, {}, &Person::age);
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ") "; // Bob (25) Alice (30) Charlie (35)
}
}投影的威力:
&Person::age告诉算法按哪个成员比较- 空花括号
{}使用默认比较(升序) - 适用于所有 Ranges 算法:
find、max_element等
std::views::split 的深度解析:char vs string 陷阱
std::views::split 是最常用的适配器之一,但它也是陷阱最多的地方,尤其是当你混用字符和字符串作为分隔符时。
1. 字符串字面量的“隐形”陷阱
这是最经典的坑。如果你写 split(" "),你可能以为是在用空格分割,但实际上:
std::string text{"hello world"};
// ❌ 错误:分隔符被推导为 const char[2],包含 {' ', '\0'}
auto parts{text | std::views::split(" ")}; 由于 " " 是一个 C 风格字符串(数组),它包含一个隐藏的空终止符 \0。views::split 会将其视为一个要匹配的范围。
正确做法:
// ✅ 使用字符:明确分割单个元素
auto parts{text | std::views::split(' ')};
// ✅ 使用 string_view:明确分割范围且不包含 \0
using namespace std::literals;
auto parts{text | std::views::split(" "sv)}; 2. C++20 vs C++23:split 与 lazy_split
在 C++20 中,views::split 的行为其实是“懒惰”的,这导致了性能问题。C++23 对此进行了重构:
std::views::lazy_split(C++20 的旧行为):- 适用于输入范围 (Input Range),如从文件流读取。
- 内部迭代器比较复杂,性能较低。
- 即使输入是连续内存(如
string),生成的子范围也不是contiguous_range。
std::views::split(C++23 新行为):- 要求前向范围 (Forward Range)。
- 性能更好,生成的子范围保留了原容器的特性(如
contiguous_range),可以直接构造string_view。
3. 返回类型的差异
当你使用 char 作为分隔符时,split 的实现通常更简单高效。
std::string s{"a,b,c"};
auto r{s | std::views::split(',')};
// 在 C++23 中,r 的元素(子范围)可以直接转换为 string_view
for (auto&& part : r) {
std::println("{}", std::string_view{part});
}4. 性能建议
- 优先使用
char:如果分隔符只是单个字符,永远优先使用' '而不是" "。 - 避免重复转换:如果你需要将结果转换回
string,使用 C++23 的std::ranges::to:
auto vec{s | std::views::split(',') | std::ranges::to<std::vector<std::string>>()};收集结果到容器(C++23)
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
// C++20 方式:需要手动收集
std::vector<int> evens;
for (int n : nums | std::views::filter([](int n) { return n % 2 == 0; })) {
evens.push_back(n);
}
// C++23 方式:使用 to
auto evens = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();
for (int n : evens) {
std::cout << n << " "; // 输出: 2 4 6
}
}常用 Adaptors 速查表
| Adaptor | 功能 | 示例 | 结果 |
|---|---|---|---|
filter | 过滤元素 | nums | filter(even) | 只保留偶数 |
transform | 转换元素 | nums | transform(square) | 每个元素平方 |
take | 取前 N 个 | nums | take(3) | 前 3 个元素 |
drop | 跳过前 N 个 | nums | drop(2) | 跳过前 2 个 |
reverse | 反转 | nums | reverse | 反序遍历 |
keys | 提取 map 键 | map | keys | 所有键 |
values | 提取 map 值 | map | values | 所有值 |
iota | 生成序列 | iota(1, 10) | 1,2,3,…,9 |
zip | 合并范围 | zip(vec1, vec2) | 元组对 |
实用组合示例
// 1. 取前 10 个正偶数
auto result = std::views::iota(1)
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::take(10);
// 2. 反转并跳过前 3 个
auto result = nums
| std::views::reverse
| std::views::drop(3);
// 3. 提取 map 的值并排序
std::map<int, std::string> m = {{3, "c"}, {1, "a"}, {2, "b"}};
auto sorted_values = m
| std::views::values
| std::views::transform([](const std::string& s) { return s; });
// 注意:map 的 values 视图已经是排序的
// 4. 处理嵌套容器
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
auto flat = nested | std::views::join; // 1,2,3,4,5,6Ranges Algorithms vs 传统 STL
对比表
| 操作 | 传统 STL | Ranges | 优势 |
|---|---|---|---|
| 排序 | sort(vec.begin(), vec.end()) | ranges::sort(vec) | 更简洁 |
| 查找 | find(vec.begin(), vec.end(), 42) | ranges::find(vec, 42) | 直接传容器 |
| 按成员查找 | find_if(..., [](auto& x) {return x.id==42;}) | ranges::find(vec, 42, &X::id) | 投影简化 |
| 复制 | copy(vec.begin(), vec.end(), ...) | ranges::copy(vec, ...) | 无需迭代器对 |
实际案例对比
案例 1:查找结构体成员
struct Item {
int id;
std::string name;
};
std::vector<Item> items = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};
// 传统 STL:需要 lambda
auto it = std::find_if(items.begin(), items.end(),
[](const Item& i) { return i.id == 2; });
// Ranges:使用投影
auto it = std::ranges::find(items, 2, &Item::id);案例 2:复杂管道操作
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 传统 STL:需要多个中间容器
std::vector<int> temp1, temp2, result;
std::copy_if(vec.begin(), vec.end(), std::back_inserter(temp1),
[](int n) { return n % 2 == 0; });
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
[](int n) { return n * n; });
std::copy_n(temp2.begin(), 3, std::back_inserter(result));
// Ranges:一行搞定
auto result = vec
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3)
| std::ranges::to<std::vector>();懒惰求值的威力
什么是懒惰求值?
视图不会立即执行操作,而是在迭代时才计算:
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// 创建视图(不执行任何计算)
auto filtered = nums
| std::views::filter([](int n) {
std::cout << "filter: " << n << "\n";
return n % 2 == 0;
})
| std::views::transform([](int n) {
std::cout << "transform: " << n << "\n";
return n * n;
});
std::cout << "开始迭代...\n";
// 只在迭代时计算
for (int n : filtered) {
std::cout << "结果: " << n << "\n";
}
}输出:
开始迭代...
filter: 1
filter: 2
transform: 2
结果: 4
filter: 3
filter: 4
transform: 4
结果: 16
filter: 5懒惰求值的优势
- 性能优化:只计算需要的元素
- 无限序列:可以处理无限数据流
- 组合灵活:视图可以无限组合而不产生中间结果
// 无限序列示例
auto squares = std::views::iota(1)
| std::views::transform([](int n) { return n * n; });
// 只取前 5 个平方数
for (int sq : squares | std::views::take(5)) {
std::cout << sq << " "; // 1 4 9 16 25
}迭代器概念与限制
Ranges 迭代器层次
| 概念 | 能力 | 典型容器 | 支持的操作 |
|---|---|---|---|
input_iterator | 单次读取 | istream_iterator | ++it, *it |
forward_iterator | 多次读取 | forward_list, unordered_map | ++it, 多次 *it |
bidirectional_iterator | 双向移动 | list, map, set | ++it, --it |
random_access_iterator | 随机访问 | vector, deque, array | it + n, it[n] |
contiguous_iterator | 连续内存 | vector, array, string_view | &*it 得到地址 |
Adaptor 对迭代器的要求
| Adaptor | 最低要求 | 原因 |
|---|---|---|
filter | forward_range | 需要多次检查元素 |
transform | input_range | 只需单次读取 |
reverse | bidirectional_range | 需要回退 |
take / drop | input_range | 只需单次遍历 |
join | input_range + 解引用返回 range | 需要能解引用出子范围 |
split | forward_range | 需要保存分隔符位置 |
常见陷阱
陷阱 1:forward_list 不能用 reverse
std::forward_list<int> list = {1, 2, 3};
// ❌ 编译错误:需要 bidirectional_iterator
// auto reversed = list | std::views::reverse;
// ✅ 解决:转换为支持反向的容器
auto vec = std::vector(list.begin(), list.end());
auto reversed = vec | std::views::reverse;陷阱 2:悬空迭代器保护
// 传统 STL:危险
auto it = std::find(std::vector{1, 2, 3}.begin(),
std::vector{1, 2, 3}.end(),
42);
// *it 悬空!临时 vector 已销毁
// Ranges:安全
auto it = std::ranges::find(std::vector{1, 2, 3}, 42);
// it 是 std::ranges::dangling,无法解引用C++23 新特性
std::ranges::to - 收集结果
#include <ranges>
#include <vector>
#include <map>
int main() {
std::vector<std::pair<int, std::string>> pairs = {{1, "a"}, {2, "b"}};
// 转换为 map
auto m = pairs | std::ranges::to<std::map>();
// 转换为 vector
auto v = pairs | std::ranges::to<std::vector>();
}std::ranges::contains - 检查包含
std::vector<int> vec = {1, 2, 3, 4, 5};
// 传统方式
bool found = std::find(vec.begin(), vec.end(), 3) != vec.end();
// Ranges 方式
bool found = std::ranges::contains(vec, 3); // 更清晰std::views::zip - 合并范围
std::vector<std::string> keys = {"a", "b", "c"};
std::vector<int> values = {1, 2, 3};
// 合并为元组对
auto zipped = std::views::zip(keys, values);
for (const auto& [k, v] : zipped) {
std::cout << k << ": " << v << " "; // a: 1 b: 2 c: 3
}
// 转换为 map
auto map = zipped | std::ranges::to<std::map>();std::views::chunk - 分块
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 每 3 个元素一组
auto chunks = vec | std::views::chunk(3);
for (auto chunk : chunks) {
for (int n : chunk) {
std::cout << n << " ";
}
std::cout << "\n";
}
// 输出:
// 1 2 3
// 4 5 6
// 7 8 9实战案例
案例 1:处理 CSV 数据
#include <iostream>
#include <sstream>
#include <vector>
#include <ranges>
#include <string>
struct Record {
std::string name;
int age;
double score;
};
int main() {
std::string csv = "Alice,30,95.5\nBob,25,87.0\nCharlie,35,92.3";
std::istringstream iss(csv);
auto records = std::views::istream<std::string>(iss)
| std::views::transform([](const std::string& line) {
std::istringstream line_ss(line);
std::string name, age_str, score_str;
std::getline(line_ss, name, ',');
std::getline(line_ss, age_str, ',');
std::getline(line_ss, score_str, ',');
return Record{name, std::stoi(age_str), std::stod(score_str)};
});
// 过滤分数 > 90 的记录
auto high_scorers = records
| std::views::filter([](const Record& r) { return r.score > 90; });
for (const auto& r : high_scorers) {
std::cout << r.name << ": " << r.score << "\n";
}
}案例 2:滑动窗口计算移动平均
#include <iostream>
#include <vector>
#include <ranges>
#include <numeric>
int main() {
std::vector<double> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int window_size = 3;
// 使用 C++23 的 slide
auto windows = data | std::views::slide(window_size);
for (auto window : windows) {
double sum = 0;
for (double val : window) {
sum += val;
}
std::cout << "平均: " << sum / window_size << "\n";
}
// 输出: 2, 3, 4, 5, 6, 7, 8, 9
}案例 3:生成斐波那契数列
#include <iostream>
#include <ranges>
#include <generator> // C++23
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
std::tie(a, b) = std::make_tuple(b, a + b);
}
}
int main() {
// 取前 10 个斐波那契数
auto fibs = fibonacci() | std::views::take(10);
for (int f : fibs) {
std::cout << f << " "; // 0 1 1 2 3 5 8 13 21 34
}
}最佳实践
✅ DO - 推荐做法
// 1. 优先使用 Ranges 算法
std::ranges::sort(vec);
std::ranges::find(vec, value);
// 2. 使用投影简化代码
std::ranges::sort(people, {}, &Person::age);
// 3. 管道式组合视图,并使用 auto&& 接收参数
auto result = vec
| std::views::filter([](auto&& n) { return n > 0; })
| std::views::transform([](auto&& n) { return n * 2; })
| std::ranges::to<std::vector>();
// 4. 使用 contains 而不是 find
if (std::ranges::contains(vec, value)) { ... }
// 5. 立即收集结果避免悬空迭代器
auto result = view | std::ranges::to<std::vector>();❌ DON’T - 避免做法
// 1. 不要对临时容器使用算法返回迭代器
auto it = std::ranges::find(std::vector{1, 2, 3}, 2); // it 是 dangling
// 2. 不要对不支持的操作使用视图
std::forward_list<int> list = {1, 2, 3};
auto reversed = list | std::views::reverse; // 编译错误
// 3. 不要忘记视图是懒惰的
auto filtered = vec | std::views::filter(pred);
// filtered 不会立即执行,直到迭代
// 4. 不要过度组合导致性能问题
auto complex = vec
| std::views::filter(...)
| std::views::transform(...)
| std::views::filter(...)
| std::views::transform(...); // 考虑是否需要中间容器性能考虑
零成本抽象
Ranges 的性能通常与手写循环相当:
// Ranges 版本
auto result = vec
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 编译器优化后,等价于
for (int n : vec) {
if (n % 2 == 0) {
// 处理 n * n
}
}何时使用容器收集
// ✅ 适合使用视图的场景
for (int n : vec | std::views::filter(pred)) {
// 单次遍历,不需要中间结果
}
// ✅ 需要多次遍历时收集到容器
auto filtered = vec
| std::views::filter(pred)
| std::ranges::to<std::vector>();
// 现在可以多次遍历 filtered
for (int n : filtered) { ... }
for (int n : filtered) { ... }总结
Ranges 的核心价值
- 可读性:声明式代码,意图清晰
- 组合性:管道操作,灵活组合
- 性能:懒惰求值,零成本抽象
- 安全性:编译期检查,防止悬空迭代器
- 简洁性:投影简化,减少样板代码










