C++ 专家(14):Ranges 深入——视图所有权、适配器组合、自定义视图
更新时间:2026-09-01。本文是
languages/cpp/expert/专家层第 14 篇,接 C++20 模块深入。C++20 的 Ranges 库不仅仅是方便的组合算法,底层有严格的 concept 体系和所有权模型。理解 view 的 ownership、range concepts 的分类、以及自定义适配器,才能写出高效且正确的 ranges 代码。
本文要回答的问题
- view 的所有权模型是什么?view 什么时候拥有数据,什么时候不拥有?
- range concepts 的分类:input_range、forward_range、random_access_range 分别是什么?
- 怎么自定义一个 view adapter?
- C++23 的 ranges 新增了什么适配器?
一、view 的所有权模型
cpp
// view 不拥有数据,引用外部数据
std::vector<int> data = {1, 2, 3, 4, 5};
auto view = data | std::views::filter([](int x) { return x % 2 == 0; });
// view 引用 data,data 修改后 view 也会变化
// 但有些 view 拥有数据!
auto owned = std::views::single(42); // 拥有数据
auto iota = std::views::iota(1, 10); // 生成数据,不引用外部
// 大部分 adapters 不拥有数据,只是引用ownership 规则:
filter、transform、take、drop、reverse:不拥有,引用原始 rangesingle、iota、empty:拥有/生成数据owning_view(C++23):通过移动构造拥有数据
二、range concepts 分类
cpp
// 六个 range concept,从弱到强
// 1. input_range:只读一次
// 2. forward_range:可多次遍历
// 3. bidirectional_range:可双向遍历
// 4. random_access_range:随机访问
// 5. contiguous_range:连续内存(如 vector、array)
// 检查
static_assert(std::ranges::random_access_range<std::vector<int>>);
static_assert(std::ranges::contiguous_range<std::vector<int>>);
static_assert(std::ranges::bidirectional_range<std::list<int>>);
// 视图的分类
auto view = data | std::views::filter(...);
// filter 的输出是 input_range(不是 random_access,因为不知道过滤后有多少元素)分类影响: 如果你写了一个算法,要求 random_access_range,但传入的是 filter 的结果(input_range),编译会报错。这就是 concept 的作用。
三、自定义 view adapter
cpp
// 简单的自定义 view:取偶数索引元素
// step_by 在 C++23 有标准实现,这里演示怎么写
// 自定义 view 类
template<std::ranges::view V>
class StepView : public std::ranges::view_interface<StepView<V>> {
V base_;
std::ptrdiff_t step_;
std::ptrdiff_t start_ = 0;
public:
StepView() = default;
StepView(V base, std::ptrdiff_t step)
: base_(std::move(base)), step_(step) {}
// 迭代器
struct iterator {
std::ranges::iterator_t<V> current;
std::ptrdiff_t step;
using value_type = std::ranges::range_value_t<V>;
using difference_type = std::ranges::range_difference_t<V>;
iterator& operator++() {
std::ranges::advance(current, step);
return *this;
}
auto& operator*() { return *current; }
bool operator!=(const iterator& other) const {
return current != other.current;
}
};
auto begin() { return iterator{std::ranges::begin(base_), step_}; }
auto end() { return iterator{std::ranges::end(base_), step_}; }
auto size() { return (std::ranges::size(base_) + step_ - 1) / step_; }
};
// 自定义适配器对象
struct StepFn {
std::ptrdiff_t step_;
constexpr auto operator()(std::ranges::viewable_range auto&& r) const {
return StepView<std::views::all_t<decltype(r)>>(
std::views::all(std::forward<decltype(r)>(r)), step_);
}
};
inline constexpr auto step = StepFn{};
// 使用
auto result = data | step(2); // 取偶数索引元素四、C++23 新增适配器
cpp
// C++23 新增的适配器
#include <ranges>
// 1. views::stride——步进
auto step = v | std::views::stride(2); // 1, 3, 5, 7, 9
// 2. views::chunk——分块
auto chunks = v | std::views::chunk(3); // {{1,2,3}, {4,5,6}, {7,8,9}, {10}}
// 3. views::slide——滑动窗口
auto slides = v | std::views::slide(3); // {{1,2,3}, {2,3,4}, ...}
// 4. views::join_with——带分隔符的展平
auto joined = v | std::views::join_with(0); // 每个子元素后加 0
// 5. views::enumerate——带索引
for (auto [idx, val] : v | std::views::enumerate) {
std::cout << idx << ": " << val << "\n";
}五、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 视图引用悬空 | 原始数据销毁后使用视图,UB | 确保视图生命周期内数据有效 |
| 多次遍历消耗型视图 | 第二次遍历为空 | 检查 range concept 类型 |
| 将 view 传给需要所有权的函数 | 编译错误或 UB | 用 std::ranges::to<vector>() 消费 |
| 视图的迭代器类型复杂 | 编译错误看不懂 | 用 auto 推导迭代器类型 |
相关与延伸
下一篇:分配器深入——PMR、内存池、arena 分配器;Ranges 入门,见 Ranges 基础。
一句话总结
C++20 Ranges 深入:view 不拥有数据(引用外部),但 single/iota 拥有;range concepts 从 input_range 到 contiguous_range 逐级增强,算法按 concept 约束传入参数;自定义 view adapter 继承 view_interface 并实现迭代器;C++23 新增 stride(步进)、chunk(分块)、slide(滑动窗口)、enumerate(带索引)等适配器;视图迭代器类型复杂,用 auto 推导。