为什么需要 Ranges?

传统 STL 处理数据序列冗长且易错:

1
2
3
4
5
6
// 传统方式:需要中间容器或繁琐的迭代器对
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 用管道式、声明式语法替代:

1
2
3
4
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()
  • 投影支持&Person::age 直接按成员操作
  • 类型安全:编译期检查,防止悬空迭代器

核心概念

概念含义
Range任何提供 begin()/end() 的可迭代序列(容器、视图、C 数组)
View轻量、懒惰的范围表示,不复制数据,用 `
Adaptor修改范围不改底层数据,如 filter/transform/take/reverse
AlgorithmRanges 版算法,直接接受范围并支持投影

头文件与命名空间

Ranges 相关工具主要来自 <ranges>

1
#include <ranges>

<ranges> 提供:

  • std::ranges::rangestd::ranges::view 等 concepts
  • std::ranges::ref_viewstd::ranges::owning_view 等 view 类型
  • std::views::filterstd::views::transformstd::views::take 等 view adaptors
  • std::ranges::to(C++23,转换为容器)

std::views 是标准库提供的短命名空间,基本等价于:

1
namespace std::views = std::ranges::views;

所以这两种写法通常等价:

1
2
auto a = nums | std::views::filter(pred);
auto b = nums | std::ranges::views::filter(pred);

实际代码推荐写短的 std::views::...

注意:Ranges 算法在 <algorithm>

1
2
3
4
5
#include <algorithm>

std::ranges::sort(nums);
std::ranges::find(nums, 42);
std::ranges::contains(nums, 42); // C++23

容器、字符串、span 仍然包含各自头文件:

1
2
3
#include <vector>
#include <string>
#include <span>

快速上手

1
2
3
4
5
6
7
8
9
10
11
12
13
#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; });

std::println("{}", even_squares); // 4 16 36
}
  • | 从左到右组合视图
  • 计算是懒惰的,迭代时才执行
  • even_squares 是视图,不拥有数据

投影:按结构体成员操作

1
2
3
4
5
6
7
8
9
struct Person { std::string name; int age; };
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);

&Person::age 指定比较成员,{} 用默认(升序)比较。投影适用于所有 Ranges 算法(findmax_element 等)。

收集结果到容器(C++23)

1
2
3
auto evens = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>(); // 一行收集

Lambda 参数:auto&& 是默认首选

在 adaptor 的 lambda 中,元素经迭代器传递,参数类型影响性能与正确性:

传递方式适用场景
auto小型内置类型(int),值拷贝可接受
const auto&只读大型对象;无法处理代理对象(vector<bool>zip
auto&&默认首选:万能绑定、零拷贝、支持代理对象

auto&& 是转发引用(万能引用),能绑定左值和右值,是处理代理对象的唯一通用方式。实测对大对象,auto(拷贝)比 const auto&/auto&&(零拷贝)慢约两个数量级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 默认写法
auto result = nums
| std::views::filter([](auto&& n) { return n % 2 == 0; })
| std::views::transform([](auto&& n) { return n * n; });

// 必须用 auto&&:处理代理对象
std::vector<bool> flags = {true, false, true};
auto r = flags | std::views::filter([](auto&& flag) { return flag; });

auto zipped = std::views::zip(a, b);
auto r2 = zipped | std::views::filter([](auto&& pair) {
auto& [num, str] = pair; // pair 是 tuple<int&, string&>
return num > 1;
});

决策树:需要修改元素或处理代理对象 → auto&&;小型只读 → auto;大型只读且无代理对象 → const auto&

陷阱

  • auto&& 允许修改,filter 中别误改原始数据(应保持纯函数)
  • 视图不延长底层数据生命周期,跨函数返回前用 ranges::to 收集
  • filter 中无需 std::forward;transform 需要转发移动语义时才用

views::splitchar vs string 陷阱

1
2
3
4
5
6
7
8
std::string text{"hello world"};

// ❌ " " 被推导为 const char[2](含 '\0'),会匹配空终止符
auto parts{text | std::views::split(" ")};

// ✅ 用字符或 string_view
auto parts{text | std::views::split(' ')};
auto parts{text | std::views::split(" "sv)};

C++23 重构了 split:

  • lazy_split(C++20 旧行为):支持输入范围(如文件流),子范围非 contiguous_range
  • split(C++23):要求前向范围,子范围保留连续特性,可直接构造 string_view
1
auto vec{s | std::views::split(',') | std::ranges::to<std::vector<std::string>>()};

常用 Adaptors 速查

Adaptor功能示例
filter过滤nums | filter(even)
transform转换nums | transform(square)
take / drop取前 N / 跳过前 Nnums | take(3)
reverse反转nums | reverse
keys / values提取 map 键/值map | keys
iota生成序列iota(1, 10) → 1…9
zip合并为元组zip(a, b)
join展平嵌套nested | join
1
2
3
4
5
6
7
8
// 取前 10 个正偶数
auto result = std::views::iota(1)
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::take(10);

// 展平嵌套容器
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
auto flat = nested | std::views::join; // 1,2,3,4,5,6

各 Adaptor 使用要点

filter

谓词可能被调用多次,不应依赖副作用,最好是纯判断——相同输入返回相同结果:

1
2
3
4
int count = 0;
auto bad = nums | std::views::filter([&](int) {
return ++count % 2 == 0; // fragile:相同输入结果不稳定
});

transform

返回引用则后续视图继续操作原对象,返回值则每次迭代按需生成临时值:

1
2
3
4
5
auto names = users | std::views::transform(&User::name);  // 引用成员

auto upper = users | std::views::transform([](const User& u) {
return to_upper(u.name()); // 生成临时值
});

take / drop

取前 N 个或跳过前 N 个,常用于截断无限序列(见 懒惰求值):

1
2
auto first_10 = nums | std::views::take(10);
auto after_10 = nums | std::views::drop(10);

reverse

反向观察支持双向迭代的 range,不会真的倒置容器,只是反转迭代方向:

1
auto reversed = nums | std::views::reverse;

keys / values

观察 map、pair 序列中的 key 或 value:

1
2
3
4
5
6
7
8
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 82}};

for (std::string_view name : scores | std::views::keys) {
std::println("{}", name);
}
for (int score : scores | std::views::values) {
std::println("{}", score);
}

views::allref_view / owning_view

std::views::all(x) 把一个对象转成“适合管道使用的 view”,根据传入对象类型选择不同包装:

输入结果倾向含义
已经是 view原样或轻量包装直接使用
左值容器std::ranges::ref_view引用外部容器
右值可借用 range直接使用不会悬空
右值非借用 rangestd::ranges::owning_viewview 拥有这个临时 range
1
2
3
4
std::vector<int> nums = {1, 2, 3};

auto a = std::views::all(nums); // refers to nums
auto b = std::views::all(std::vector{1, 2}); // owns the temporary vector

这也是为什么一些管道表达式可以安全地从临时容器开始:

1
2
auto view = std::vector{1, 2, 3}
| std::views::transform([](int n) { return n * 2; });

现代 ranges 会尽量避免明显悬空,但不能替你解决所有生命周期问题,尤其是 lambda 捕获引用、返回 view、保存 view 时。

std::ranges::view 到底是什么

std::ranges::view 是一个 concept,大致可以理解为:

1
2
3
4
5
template <class T>
concept view =
std::ranges::range<T> &&
std::movable<T> &&
std::ranges::enable_view<T>;

重点只有三点:

  • 它首先是 range:能 begin() / end()
  • 它至少可移动:view 可以安全放进管道里传递
  • 它是轻量范围对象:适合拷贝 / 移动 / 按值返回

std::views::filter 里的 views 是命名空间;std::ranges::view 里的 view 是 concept。名字很像,但一个用来构造视图,一个用来约束 / 判断类型。

注意:view 不等于永远不拥有数据。多数 view 只是观察外部数据,但也有拥有数据或状态的 view。

view是否拥有数据说明
ref_view引用外部 range
owning_view持有右值 range
single_view持有一个元素
iota_view持有起点 / 终点状态
filter_view部分拥有持有底层 view 和谓词

viewrange 的区别

1
2
3
4
5
6
7
std::vector<int> nums = {1, 2, 3};

static_assert(std::ranges::range<decltype(nums)>);
static_assert(!std::ranges::view<decltype(nums)>); // vector 是容器,不是 view

auto v = nums | std::views::take(2);
static_assert(std::ranges::view<decltype(v)>);

容器负责存储,view 负责“看待这段数据的方式”。view 通常不分配、不复制元素,只保存迭代器、引用、谓词、转换函数等小状态。

view 的两个常见形态

立即可迭代的 view 对象

1
2
3
4
5
auto first_three = nums | std::views::take(3);

for (int n : first_three) {
std::println("{}", n);
}

等待接收 range 的 adaptor closure

1
2
3
4
5
auto only_even = std::views::filter([](int n) {
return n % 2 == 0;
});

auto r = nums | only_even | std::views::take(3);

std::views::filter(pred) 单独出现时还不是最终数据序列,它是“后面要接一个 range”的管道组件。

view 的可复制性别想当然

标准要求 view 可移动,不要求所有 view 都好复制。多数标准 view 拷贝成本很低,但业务代码不要依赖“随便复制复杂 view 没代价”。

1
2
3
4
5
auto pipeline = nums
| std::views::filter(is_valid)
| std::views::transform(to_record);

auto records = pipeline | std::ranges::to<std::vector>(); // 边界处物化

需要跨线程、跨对象成员、跨函数长期保存时,优先保存容器结果,或者保存原始数据和查询参数,临时组合 view。

单次遍历 view

不是所有 view 都能反复遍历。基于输入流的 range 通常是单次读取:

1
2
3
4
5
std::istringstream input{"1 2 3"};
auto ints = std::views::istream<int>(input);

for (int n : ints) { /* 读到 1 2 3 */ }
for (int n : ints) { /* 基本没东西了 */ }

如果后续逻辑要多次遍历,先收集:

1
2
auto values = std::views::istream<int>(input)
| std::ranges::to<std::vector>();

返回 view 的简单规则

返回 view 可以,但要保证它引用的数据活得够久:

1
2
3
4
5
6
auto active_users(std::span<User> users) {
return users
| std::views::filter([](const User& user) {
return user.active();
});
}

这里安全是因为 std::span 只是调用者传进来的视图,调用者负责保证底层 User 数组仍然存在。不要返回引用局部容器的 view:

1
2
3
4
auto bad() {
std::vector<int> nums = {1, 2, 3};
return nums | std::views::filter([](int n) { return n > 1; }); // ❌
}

subrange

std::ranges::subrange 是由迭代器和 sentinel 组成的 view,适合把一段 [begin, end) 包装成 range。常用于算法返回迭代器后,把后续区间重新变成可管道处理的 range:

1
2
3
4
5
6
7
auto first_even = std::ranges::find_if(nums, [](int n) {
return n % 2 == 0;
});

auto rest = std::ranges::subrange(first_even, nums.end())
| std::views::take(3)
| std::views::transform([](int n) { return n * 10; });

Ranges 算法 vs 传统 STL

操作传统 STLRanges
排序sort(v.begin(), v.end())ranges::sort(v)
按成员查找find_if(..., [](auto& x){ return x.id==42; })ranges::find(v, 42, &X::id)
查找find(v.begin(), v.end(), 42)ranges::find(v, 42)
1
2
3
4
5
6
// 一行替代多个中间容器
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>();

懒惰求值

视图在迭代时才计算,元素逐个流过管道:

1
2
3
4
auto filtered = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 创建视图时不执行任何计算,for 循环时才逐元素过滤+转换

优势:只算需要的元素、可处理无限序列、组合不产生中间结果。

1
2
3
4
// 无限序列 + take
auto squares = std::views::iota(1)
| std::views::transform([](int n) { return n * n; });
for (int sq : squares | std::views::take(5)) { /* 1 4 9 16 25 */ }

迭代器要求与陷阱

概念典型容器
input_iteratoristream_iterator(单次读)
forward_iteratorforward_listunordered_map
bidirectional_iteratorlistmapset
random_access_iteratorvectordequearray
contiguous_iteratorvectorarraystring_view

Adaptor 最低要求:reverse 需双向、filter/split 需前向、transform/take 仅需输入。

1
2
3
std::forward_list<int> list = {1, 2, 3};
// ❌ 编译错误:reverse 需 bidirectional_range
// auto r = list | std::views::reverse;

悬空保护与 borrowed_rangestd::ranges::find(std::vector{1,2,3}, 42) 返回 dangling,无法解引用——编译期阻止对临时容器的悬空引用。

Ranges 用 std::ranges::borrowed_range 处理悬空迭代器:如果一个 range 的迭代器可以在 range 对象销毁后继续安全使用,它就是 borrowed range。

  • std::span<T>std::string_view 是 borrowed range——迭代器指向外部数据。
  • std::vector<T> 右值不是 borrowed range——临时 vector 销毁后元素也没了。
1
2
auto it1 = std::ranges::find(std::span{data}, 42);      // returns iterator
auto it2 = std::ranges::find(std::vector{1, 2, 3}, 2); // returns dangling

std::ranges::dangling 是标准库在类型层面对你的提醒:这个迭代器不能安全返回。

C++23 新特性

1
2
3
4
5
6
// ranges::to:转换为任意容器
auto m = pairs | std::ranges::to<std::map>();
auto v = pairs | std::ranges::to<std::vector>();

// contains:替代 find != end
bool found = std::ranges::contains(vec, 3);

C++23 还给 views 补了一批实用工具:

view作用
std::views::zip并行遍历多个 range
std::views::enumerate遍历时附带下标
std::views::chunk按固定大小分块
std::views::slide滑动窗口
std::views::join展平一层嵌套 range
1
2
3
4
5
6
std::vector<std::string> names = {"Alice", "Bob"};
std::vector<int> scores = {95, 82};

for (auto [name, score] : std::views::zip(names, scores)) {
std::println("{}: {}", name, score);
}

实际可用性取决于标准库实现版本——编译器支持 C++23 不等于所有 C++23 library 组件都完整可用。

实战案例

1
2
3
4
5
6
7
8
9
10
// 滑动窗口移动平均(slide)
auto windows = data | std::views::slide(3);
for (auto window : windows) { /* 求和 / 求平均 */ }

// 斐波那契(C++23 generator + 无限序列)
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) { co_yield a; std::tie(a, b) = std::make_tuple(b, a + b); }
}
auto fibs = fibonacci() | std::views::take(10); // 0 1 1 2 3 5 8 13 21 34

自定义一个简单 view

多数业务代码不需要手写 view,用标准 adaptor 组合就够了。如果要封装可复用的懒惰序列,可以继承 std::ranges::view_interface。下面是一个简单的 repeat_n_view:重复生成同一个值 N 次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <cstddef>
#include <ranges>

template <typename T>
class repeat_n_view : public std::ranges::view_interface<repeat_n_view<T>> {
public:
repeat_n_view() = default;

repeat_n_view(T value, std::size_t count)
: value_(std::move(value)), count_(count) {}

class iterator {
public:
using value_type = T;
using difference_type = std::ptrdiff_t;

iterator() = default;

iterator(const T* value, std::size_t index)
: value_(value), index_(index) {}

const T& operator*() const {
return *value_;
}

iterator& operator++() {
++index_;
return *this;
}

void operator++(int) {
++*this;
}

bool operator==(const iterator&) const = default;

private:
const T* value_ = nullptr;
std::size_t index_ = 0;
};

iterator begin() const {
return iterator{&value_, 0};
}

iterator end() const {
return iterator{&value_, count_};
}

private:
T value_{};
std::size_t count_ = 0;
};

使用:

1
2
3
4
5
repeat_n_view hello{"hi", 3};

for (const auto& word : hello) {
std::println("{}", word);
}

这个例子为了说明结构刻意保持简单。真实标准库 view 还要考虑 const 迭代、sentinel、range category、异常保证、借用语义等问题。业务代码里更推荐先写普通函数返回标准 view 组合:

1
2
3
4
5
6
7
auto valid_names(std::span<const User> users) {
return users
| std::views::filter([](const User& user) {
return user.active();
})
| std::views::transform(&User::name);
}

Ranges View 的生命周期陷阱

视图最重要的规则:视图不能比它观察的数据活得更久(通用视图生命周期的完整讨论见 C++ 视图)。Ranges View 有两个典型陷阱:

小心 lambda 捕获引用

1
2
3
4
5
auto make_filtered(std::vector<int>& nums, int limit) {
return nums | std::views::filter([&](int n) { // ❌ limit 按引用捕获
return n > limit; // 函数返回后悬空
});
}

limit 是局部参数,函数返回后引用悬空。应按值捕获:

1
2
3
4
5
auto make_filtered(std::vector<int>& nums, int limit) {
return nums | std::views::filter([limit](int n) { // ✅ 按值捕获
return n > limit;
});
}

不要长期缓存复杂 view

复杂 view 类型会编码整个管道,类型长、生命周期关系复杂。短局部变量没问题,跨函数或成员变量保存时要谨慎。如果结果要跨作用域长期使用,通常更清晰的做法是物化:

1
2
3
4
auto records = nums
| std::views::filter(is_valid)
| std::views::transform(to_record)
| std::ranges::to<std::vector>();

最佳实践

✅ 推荐

  • 优先用 Ranges 算法:ranges::sort(vec)ranges::contains(vec, x)
  • 用投影简化:ranges::sort(people, {}, &Person::age)
  • lambda 参数默认 auto&&
  • 管道中间阶段用 view,边界处再物化成容器
  • 跨函数返回结果前用 ranges::to 收集,避免悬空

❌ 避免

  • 对临时容器取返回迭代器(得到 dangling
  • 对不支持的操作用视图(forward_list + reverse
  • 忘记视图是懒惰的——不迭代就不执行
  • 过度组合长管道:需多次遍历时先用 ranges::to 落盘

总结

Ranges 的核心价值:可读性(声明式)、组合性(管道)、性能(懒惰求值 + 零成本抽象)、安全性(编译期检查)、简洁性(投影消除样板)。