本文承接《C++ Ranges(一):核心概念与算法》,聚焦视图工厂、适配器与管道组合。上一篇介绍 range/view 概念、借用与所有权、算法投影及 C++23 范围算法;本文示例统一以 C++23 为编译基线,章节中的 C++20/C++23 表示 ranges 设施所属的标准版本。库设施不可仅凭 -std=c++23 推断为已实现。

示例约定

本文只展示与讲解相关的代码片段,省略头文件和 main。除特别说明外,nums 表示元素为 {1, 2, 3, 4, 5, 6}、在视图使用期间一直存活的整数数组。输出默认使用 <print> 中的 std::print 和 std::println 直接格式化 range。包含 <version> 后,分别用 __cpp_lib_print >= 202207L 和 __cpp_lib_format_ranges >= 202207L 检查打印与范围格式化支持。std::print 不自动换行,std::println 在输出末尾添加换行;仅输出换行时使用 std::println("")。

算法特性宏见上一篇,C++23 views 的宏见下表。

输出约定:std::println("{}", range) 默认将序列输出为 [1, 2, 3],空范围为 [];嵌套范围保留嵌套方括号,tuple 元素使用圆括号,字符串和字符元素带引号。字符范围需要按文本输出时使用 {:s},如 std::println("{:s}", text)。范围及其元素须满足格式化约束;打印会遍历并消费 range,单遍输入范围不能因此重复使用,无限范围必须先限制消费数量。

管道表达式如何工作

标准视图适配器通常可用两种形式调用。直接调用形式接收 range 及适配参数:

1
2
auto first_three = std::views::take(nums, 3);
std::println("{}", first_three); // [1, 2, 3]

也可以先用参数构造一个适配器闭包,再通过 | 应用于 range:

1
2
auto first_three = nums | std::views::take(3);
std::println("{}", first_three); // [1, 2, 3]

range | closure 表示把左侧 range 交给右侧闭包;closure_a | closure_b 则组合两个闭包,所得闭包再接收 range。此机制使复杂操作可命名、传递和复用。zip、cartesian_product 等接收多个 range 的设施不是单一 range 到闭包的适配器,不能把它们当作 range | zip(...) 使用。

1
2
3
4
5
6
7
auto select = std::views::filter(
[](int n) { return n % 2 == 0; });
auto scale = std::views::transform(
[](int n) { return n * 10; });
auto even_times_ten = select | scale;
std::println("{}", nums | even_times_ten); // [20, 40, 60]
std::println("{}", nums | even_times_ten); // [20, 40, 60]

适配器的输入仍须满足其自身约束。例如 reverse 要求双向 range,slide 要求前向 range。闭包可以重复应用,但 filter 的谓词和 transform 的函数必须满足相应的等值保持等语义要求;不要用会改变自身状态、使同一输入得到不同答案的 callable 表达这些转换。

求值顺序、惰性与缓存

视图适配器按表达式顺序建立视图层次。迭代外层视图时,所请求的元素才沿内层视图逐步产生;中间视图通常不另存一份全量元素。下面图示的是按需拉取,而不是创建管道时预先执行全部转换。

Ranges 管道的惰性求值:创建 view 时只组合规则,for 循环每请求一个结果,元素才依次经过 filter、transform 与 take

1
2
3
4
5
auto pipeline = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(2);
std::println("{}", pipeline); // [4, 16]

重新遍历可能再次执行筛选和变换,view 并不缓存全部结果。底层为前向 range 时,filter_view 会缓存首个匹配位置;drop_view 也可能缓存推进后的起点。这些局部缓存不是物化。不要依赖转换函数的精确调用次数;日志、计数等副作用更适合放在终端消费步骤。

惰性让短路和有限消费自然发生,但不能改变无限序列本身的边界。对 iota_view 等无限范围,只有在有限消费后才有界:遍历完整无限范围将不会结束。反过来,惰性并不意味着免费;嵌套变换、谓词与迭代器适配层会带来调用和抽象成本,按实际需求测量。

源 range、所有权与消费边界

views::all 将 range 转为适合构造视图的形式:左值非 view 通常包装为 ref_view,右值非 view 则由 owning_view 持有;传入 view 通常按值保留。因而 view 并不必然借用数据,也不必然拥有数据。把基于左值容器的视图返回给调用者时,源容器必须比视图活得久且不得发生使迭代器失效的操作。

1
2
3
4
auto owned = std::vector{1, 2, 3}
| std::views::filter([](int n) { return n > 1; });
std::println("{}", owned); // [2, 3]
// 右值 vector 由视图链持有

左值源的视图通常只借用:

1
2
3
auto borrowed = nums | std::views::take(3);
std::println("{}", borrowed); // [1, 2, 3]
// nums 必须继续存活

把惰性结果变成独立容器时可用 C++23 std::ranges::to(参见上一篇):

1
2
3
4
auto squares = nums
| std::views::transform([](int n) { return n * n; });
auto stored = std::ranges::to<std::vector<int>>(squares);
std::println("{}", stored); // [1, 4, 9, 16, 25, 36]

上例中 nums 为左值数组;stored 独立拥有生成的整数。ranges::to 是容器转换设施,不是 view adaptor。视图的迭代器/引用可能仍借用底层对象;生命周期安全需逐层检查,Ranges 不会自动阻止所有悬空引用。

以下部分依照标准库设施的名字列出 C++20 和 C++23 views。factory 负责创建 view;adaptors 将一个 range 转成另一个 range。

C++20 视图工厂

empty

views::empty<T> 产生元素类型为 T 的空 view,长度为零。它不接收一个已有 range,也不是用来清空容器。

使用场景:向 ranges 算法或接收 range 的泛型接口传递“没有元素”的输入,或测试空输入时的边界行为。相比为此创建空 vector,empty<T> 直接表达元素类型与零长度语义,无需引入容器。注意 empty_view<T> 与其他 view 通常是不同类型,不能直接用普通条件表达式在它们之间切换。

1
2
auto none = std::views::empty<int>;
std::println("{}", none); // []

single

views::single(value) 产生恰含一个元素的 view。它持有传入值的副本/移动结果,不会引用临时实参。

使用场景:接口只接收 range,而调用方只有一个值,例如让单个任务复用批量处理逻辑,或将一个字符串作为一个元素交给范围算法。single 将值包装为单元素序列,不会把字符串展开为字符;它按值持有对象,若需要操作原对象而非副本,应使用借用该对象的范围。

1
2
auto one = std::views::single(42);
std::println("{}", one); // [42]

iota

views::iota(start) 产生不设上界的递增序列;views::iota(start, bound) 则在到达上界时结束。整数示例的结束边界不包含在结果中。对有符号整数使用时,必须避免实际递增越过可表示范围。

1
2
auto finite = std::views::iota(3, 7);
std::println("{}", finite); // [3, 4, 5, 6]
1
2
3
auto infinite = std::views::iota(1)
| std::views::take(5);
std::println("{}", infinite); // [1, 2, 3, 4, 5]

istream

views::istream<T>(stream) 从输入流逐项读取 T,属于单遍输入范围。流必须存活到视图消费完毕;遍历会推进流,不能假设可以重新从头遍历。

1
2
3
4
std::istringstream input{"8 13 21"};
auto values = std::views::istream<int>(input);
std::println("{}", values); // [8, 13, 21]
// input 存活至遍历结束

C++20 没有 views::repeat。若需要重复常量,旧标准可用有界 iota 加 transform;C++23 的 views::repeat 更直接,见下文。

C++20 View Adaptors

all

views::all(range) 将输入正规化为 view。它是视图组合的基础;C++20 的 views::all_t<R> 是由该类型推导的类型别名,不是另一个 adaptor。

1
2
auto view = std::views::all(nums);
std::println("{}", view); // [1, 2, 3, 4, 5, 6]

对左值容器,结果借用源对象;对右值容器,结果可拥有其内容。请参见前文的所有权边界和上一篇的所有权示意图。

filter

views::filter(pred) 保留满足谓词的元素,底层只需满足 input_range;仅当底层是前向范围时,结果才支持多遍遍历及起点缓存。谓词应对同一输入保持稳定;通过前向 filter_view 修改元素,使其不再满足谓词,会导致未定义行为。

1
2
3
auto even = nums | std::views::filter(
[](int n) { return n % 2 == 0; });
std::println("{}", even); // [2, 4, 6]

transform

views::transform(fun) 对访问到的元素调用函数并暴露结果。函数可返回值,也可返回引用;后者会把引用生命周期带入结果视图。它不自动缓存转换结果。

1
2
3
auto squares = nums | std::views::transform(
[](int n) { return n * n; });
std::println("{}", squares); // [1, 4, 9, 16, 25, 36]

take

views::take(count) 最多产生 count 个元素;源更短则只产生实际存在的元素。count 必须非负,负值违反前置条件,并不会自动按零处理。

1
2
auto first = nums | std::views::take(3);
std::println("{}", first); // [1, 2, 3]

take_while

views::take_while(pred) 从起点连续取元素,首次遇到不满足谓词的元素时停止;之后即使有满足条件的元素也不会越过该边界。

1
2
3
auto prefix = nums | std::views::take_while(
[](int n) { return n < 4; });
std::println("{}", prefix); // [1, 2, 3]

drop

views::drop(count) 跳过至多 count 个起始元素,源短于计数时结果为空。count 必须非负,负值违反前置条件。

1
2
auto rest = nums | std::views::drop(3);
std::println("{}", rest); // [4, 5, 6]

drop_while

views::drop_while(pred) 跳过起点处连续满足谓词的元素,遇到首个不满足元素后保留剩余部分。

1
2
3
auto rest = nums | std::views::drop_while(
[](int n) { return n < 4; });
std::println("{}", rest); // [4, 5, 6]

reverse

views::reverse 反向遍历 range,要求双向 range。底层必须支持递减迭代器;单向 forward_list 不满足约束。

1
2
auto reversed = nums | std::views::reverse;
std::println("{}", reversed); // [6, 5, 4, 3, 2, 1]

common

views::common 将 sentinel 类型不同于 iterator 的 range 转成首尾迭代器同类型的 range(如有需要)。它不复制元素,也不保证底层 range 原本就有 common iterator。

1
2
3
4
auto common = std::views::iota(1)
| std::views::take(3)
| std::views::common;
std::println("{}", common); // [1, 2, 3]

counted

views::counted(iterator, count) 用迭代器与非负数量构造 view,不是 adaptor closure。底层必须存在从该迭代器开始的有效 count 个元素;它不会检查真实边界,数量过大会越界。

1
2
auto part = std::views::counted(nums.begin() + 1, 3);
std::println("{}", part); // [2, 3, 4]

split

views::split(delimiter) 按子范围分割 range,产生子范围的范围。基于 P2210R2 的修订语义从 C++20 起适用(作为缺陷报告回溯修正),不是 C++23 才加入。std::string_view 用作源可避免 C 字符串终止符参与分割。

1
2
3
4
5
6
constexpr std::string_view text{"red,,blue,"};
auto pieces = text | std::views::split(',');
for (auto piece : pieces) {
std::print("{:s}|", piece);
}
std::println(""); // red||blue||

delimiter 可以是单个值或一个 pattern range。空字段会保留;结尾 delimiter 后的尾部空子范围也会出现。split_view 的子范围共享源序列,不拥有字符。

1
2
3
4
5
6
7
8
constexpr std::string_view text{"a::b::c"};
constexpr std::string_view delimiter{"::"};
for (auto part : text | std::views::split(delimiter)) {
std::println("{:s}", part);
}
// a
// b
// c

字符串字面量作为数组 range 时包含末尾 \0;分隔模式如果也是字面量,也包含终止符。按文本字符分割时使用 std::string_view 或 std::string 明确边界。上例使用 {:s} 直接将每个字符子范围格式化为文本,无需先构造 string_view。

lazy_split

views::lazy_split(delimiter) 也按 delimiter 分割,但用于更宽松的输入范围情形,尤其是仅满足 input range 的源。它生成的内层子范围本身可能只是输入范围,迭代子范围与外层推进具有更强的单遍限制;不能像 split_view 那样假设子范围可独立保存和重复遍历。

1
2
3
4
5
6
7
constexpr std::string_view text{"a,b,c"};
for (auto part : text | std::views::lazy_split(',')) {
std::println("{:s}", part);
}
// a
// b
// c

对前向 range,优先选 split;选择 lazy_split 的价值在于其对输入 range 的适用性,不是普遍更快。与 split 一样,若输入来自含终止符的 C 字符串数组,终止符也是 range 元素。

join

views::join 将 range-of-ranges 展平为单个 range。它是 C++20 设施,要求外层及内层符合其迭代约束;结果引用内层元素,不会把它们拼接复制到新容器。

1
2
3
4
5
std::array rows{
std::array{1, 2},
std::array{3, 4}};
auto flat = rows | std::views::join;
std::println("{}", flat); // [1, 2, 3, 4]

elements

views::elements<N> 从 tuple-like 元素序列中选取第 N 个成员。索引从零开始;底层元素须支持相应 tuple-like 访问。

1
2
3
4
5
std::array pairs{
std::pair{1, 10},
std::pair{2, 20}};
auto second = pairs | std::views::elements<1>;
std::println("{}", second); // [10, 20]

keys

views::keys 等价于 elements<0>,可用于 pair 序列和 map。map 的键依然不可通过结果 view 修改。

1
2
std::array pairs{std::pair{1, 10}, std::pair{2, 20}};
std::println("{}", pairs | std::views::keys); // [1, 2]

values

views::values 等价于 elements<1>。对可写的 pair 序列或 map,它保留映射值的可写引用,不复制元素。

1
2
3
4
5
std::array pairs{std::pair{1, 10}, std::pair{2, 20}};
for (int& value : pairs | std::views::values) {
value += 1;
}
std::println("{}", pairs | std::views::values); // [11, 21]

C++23 视图工厂

repeat

views::repeat(value) 产生无限个重复值;带第二个参数的 views::repeat(value, count) 产生有限数量。它持有值,不会从一个源 range 逐项复制。该工厂为 C++23;无限形式应配合有限消费。

1
2
auto repeated = std::views::repeat(7, 4);
std::println("{}", repeated); // [7, 7, 7, 7]
1
2
3
auto first = std::views::repeat(7)
| std::views::take(3);
std::println("{}", first); // [7, 7, 7]

C++23 View Adaptors

这些设施都在 <ranges>,但实现进度并不一致。包含 <version> 后检查对应宏;__cpp_lib_ranges 不能替代下列逐项检查。宏未达到所需值时,应更换标准库或明确不使用该设施,而不是用语言模式假定支持。

特性宏C++23 最低值对应设施
__cpp_lib_ranges_as_const202207Las_const、常量迭代支持
__cpp_lib_ranges_as_rvalue202207Las_rvalue
__cpp_lib_ranges_enumerate202302Lenumerate
__cpp_lib_ranges_zip202110Lzip、zip_transform、adjacent 及其 transform/别名
__cpp_lib_ranges_chunk202202Lchunk
__cpp_lib_ranges_slide202202Lslide
__cpp_lib_ranges_chunk_by202202Lchunk_by
__cpp_lib_ranges_stride202207Lstride
__cpp_lib_ranges_join_with202202Ljoin_with
__cpp_lib_ranges_cartesian_product202207Lcartesian_product
__cpp_lib_ranges_repeat202207Lrepeat 工厂

as_const

views::as_const 将元素以只读访问形式暴露,不复制底层容器。普通引用变成 const 引用;值和代理引用则按相应常量引用规则处理。它不保证底层对象无法通过其他路径修改。

1
2
3
4
5
6
std::vector values{1, 2, 3};
auto readonly = values | std::views::as_const;
static_assert(std::same_as<
std::ranges::range_reference_t<decltype(readonly)>,
const int&>);
std::println("{}", readonly); // [1, 2, 3]

该设施可能尚未由某些 C++23 标准库实现提供。一般 ranges 算法对代理引用的支持也不能简单归结为 const auto& 一定不可用;应按具体 range 的引用类型和操作约束判断。

as_rvalue

views::as_rvalue 令解引用结果表现为右值引用,使后续消费操作可以移动元素。创建该 view 本身不移动任何元素;只有执行移动构造/赋值的操作才会消费源元素。对拥有数据的左值容器使用它,随后物化结果可能令容器元素处于 moved-from 状态。

1
2
3
4
5
6
std::vector<std::string> words{"red", "green"};
auto rvalues = words | std::views::as_rvalue;
auto moved = std::ranges::to<std::vector<std::string>>(
rvalues);
std::println("{}", moved); // ["red", "green"]
// words 中对应字符串已被移动;不要假设原值保留

enumerate

views::enumerate 为每个元素配上从零开始的索引,结果是包含索引与元素引用的 tuple。索引类型是底层 view 的 range_difference_t,而非固定的 std::size_t;结构化绑定得到的元素仍可保留底层引用语义。

1
2
3
auto indexed = nums | std::views::enumerate;
std::println("{}", indexed);
// [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]

zip

views::zip(ranges...) 同步组合多个输入,结果是对应元素引用的 tuple,长度取最短输入。它不能部分绑定为单源管道闭包,应直接调用。

1
2
3
constexpr std::array letters{'a', 'b', 'c'};
auto pairs = std::views::zip(nums, letters);
std::println("{}", pairs); // [(1, 'a'), (2, 'b'), (3, 'c')]

zip_transform

views::zip_transform(fun, ranges...) 对各输入相同位置的元素调用函数,直到最短输入结束。与 zip 一样,应直接传入多个 ranges。

1
2
3
4
constexpr std::array other{10, 20, 30};
auto sums = std::views::zip_transform(
[](int a, int b) { return a + b; }, nums, other);
std::println("{}", sums); // [11, 22, 33]

adjacent

views::adjacent<N> 产生由连续 N 个元素引用组成的 tuple,要求前向 range。N > 0 时,长度为 LL 的输入产生 max⁡(L−N+1,0)\max(L - N + 1, 0) 个重叠窗口;输入短于 N 时为空。adjacent<0> 是特殊情况,返回空 view,而不是每个位置一个空窗口。

1
2
3
auto triples = nums | std::views::adjacent<3>;
std::println("{}", triples);
// [(1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6)]

pairwise

views::pairwise 是 views::adjacent<2> 的别名,产生重叠相邻对。长度为 LL 时结果数为 max⁡(L−1,0)\max(L - 1, 0)。

1
2
3
auto pairs = nums | std::views::pairwise;
std::println("{}", pairs);
// [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]

adjacent_transform

views::adjacent_transform<N>(fun) 将函数应用于每个由连续 N 个元素组成的窗口,直接产生转换结果,要求前向 range。N > 0 时结果数为 max⁡(L−N+1,0)\max(L - N + 1, 0)。N == 0 时返回空 view,但函数必须可无参调用且返回可作为元素的对象类型。

1
2
3
auto sums = nums | std::views::adjacent_transform<2>(
[](int a, int b) { return a + b; });
std::println("{}", sums); // [3, 5, 7, 9, 11]

pairwise_transform

views::pairwise_transform(fun) 是 adjacent_transform<2>(fun) 的别名,适合相邻差值、两两求和等操作。

1
2
3
4
auto differences = nums
| std::views::pairwise_transform(
[](int a, int b) { return b - a; });
std::println("{}", differences); // [1, 1, 1, 1, 1]

chunk

views::chunk(width) 按固定宽度切成不重叠子范围,最后一块可以更短。要求输入范围,不一定要求前向范围;单遍输入的内层块会共享消费状态,必须按顺序处理,不能保存下来独立重放。width 必须为正;有限长度 LL、宽度 ww 时块数为 ⌈L/w⌉\lceil L / w \rceil。

1
2
auto groups = nums | std::views::chunk(4);
std::println("{}", groups); // [[1, 2, 3, 4], [5, 6]]

slide

views::slide(width) 产生固定宽度的重叠窗口,每次向前移动一个元素,要求前向 range。width 必须为正;长度 LL、宽度 ww 时窗口数为 max⁡(L−w+1,0)\max(L - w + 1, 0)。它与 adjacent<N> 的区别是宽度可在运行时确定,窗口本身是 range,而非固定大小 tuple。

1
2
3
auto windows = nums | std::views::slide(3);
std::println("{}", windows);
// [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]

chunk_by

views::chunk_by(pred) 检查相邻元素:谓词为真时留在同一组,为假时开始新组。比较的是相邻的一对,不是每个候选元素与组首;要求前向 range,组大小不固定。

1
2
3
4
constexpr std::array values{1, 3, 5, 2, 4, 7, 9};
auto groups = values | std::views::chunk_by(
[](int a, int b) { return a % 2 == b % 2; });
std::println("{}", groups); // [[1, 3, 5], [2, 4], [7, 9]]

谓词应对同一对值保持稳定。例如用 a <= b 分组时,1, 3, 2 会分为 1, 3 和 2,因为 3 <= 2 为假;即使 1 <= 2 为真,也不会让 2 留在首组。

stride

views::stride(step) 每隔 step 个位置取一个元素并保留起点,支持输入范围。step 必须为正;有限长度 LL、步长 ss 时元素数为 ⌈L/s⌉\lceil L / s \rceil。步长大于长度时,非空输入仍保留首元素。

1
2
auto every_other = nums | std::views::stride(2);
std::println("{}", every_other); // [1, 3, 5]

join_with

views::join_with(separator) 展平 range-of-ranges,并在相邻内层 range 之间插入分隔元素或分隔 range。分隔 range 要求前向遍历,外层与内层还须满足元素兼容等约束。空子范围也参与相邻关系,例如 {"a", "", "b"} 用 '-' 连接得到 a--b。

1
2
3
4
std::array words{
std::string{"red"}, std::string{"blue"}};
auto text = words | std::views::join_with('-');
std::println("{:s}", text); // red-blue

cartesian_product

views::cartesian_product(ranges...) 产生笛卡尔积,每个元素是引用 tuple,最右侧输入变化最快。第一个输入只需为 input range,后续输入要求前向 range,因为会反复从头遍历。任一输入为空则结果为空;零个输入时也为空。它应直接调用,不是单源 adaptor closure。

1
2
3
4
5
constexpr std::array xs{1, 2};
constexpr std::array ys{'a', 'b', 'c'};
auto products = std::views::cartesian_product(xs, ys);
std::println("{}", products);
// [(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c')]

views::concat、views::cache_latest 和 views::as_input 属于 C++26,不是 C++23 设施。ref_view 和 owning_view 是包装类型,通常由 views::all 选择,并没有对应的 views::ref 或 views::owning adaptor。

滑动窗口实战

将 slide、transform 和 fold_left 组合,可以逐窗口计算移动平均,再用 ranges::to 保存结果:

1
2
3
4
5
6
7
8
9
10
11
constexpr int width = 3;
auto averages = nums
| std::views::slide(width)
| std::views::transform([](auto window) {
auto sum = std::ranges::fold_left(
window, 0.0,
[](double acc, int n) { return acc + n; });
return sum / width;
})
| std::ranges::to<std::vector<double>>();
std::println("{}", averages); // [2, 3, 4, 5]

长度为 LL、窗口宽度为 ww 时,逐窗口求和耗时为 O(Lw)O(Lw),不是自动优化的滚动和算法。窗口较大或数据量很大时,可维护滑入/滑出元素的滚动和,达到 O(L)O(L);需要考虑浮点误差。此例展示组合语义,不宣称其性能总是更优。

自定义 adaptor closure

C++23 的 std::ranges::range_adaptor_closure<D> 使自定义闭包参与 range | closure 和闭包组合。下面先筛选偶数,再乘以保存的系数;该接口由 __cpp_lib_ranges >= 202202L 标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct scale_even
: std::ranges::range_adaptor_closure<scale_even> {
int factor;

explicit scale_even(int value) : factor(value) {}

template <std::ranges::viewable_range R>
auto operator()(R&& range) const {
return std::forward<R>(range)
| std::views::filter(
[](int n) { return n % 2 == 0; })
| std::views::transform(
[factor = factor](int n) { return n * factor; });
}
};

使用该闭包,并继续组合转换:

1
2
3
4
5
auto double_even = scale_even{2};
std::println("{}", nums | double_even); // [4, 8, 12]
auto shifted_even = double_even
| std::views::transform([](int n) { return n + 1; });
std::println("{}", nums | shifted_even); // [5, 9, 13]

闭包以 std::forward 保留输入的左值/右值性,以值捕获 factor 避免保存局部引用。viewable_range 只表示输入可包装为 view,具体 filter 和 transform 的元素约束仍须满足。自定义闭包可以管理自己的状态,但其中传给标准 adaptor 的 callable 仍要遵守其语义要求。

自定义 View:有限重复值

实际代码应优先使用 C++23 views::repeat。下面的有限重复 view 仅用于说明 view_interface 与前向迭代器的最小组合:按值保存一个对象,迭代时重复返回其 const 引用。T 要求可默认构造且可移动;迭代器依赖 view 的地址,view 必须一直存活。

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
template <std::movable T>
requires std::default_initializable<T>
class repeat_n_view
: public std::ranges::view_interface<repeat_n_view<T>> {
class iterator {
const repeat_n_view* owner_ = nullptr;
std::size_t index_ = 0;

public:
using value_type = T;
using difference_type = std::ptrdiff_t;
using iterator_concept = std::forward_iterator_tag;

iterator() = default;
iterator(const repeat_n_view* owner, std::size_t index)
: owner_(owner), index_(index) {}

const T& operator*() const { return owner_->value_; }
iterator& operator++() {
++index_;
return *this;
}
iterator operator++(int) {
auto old = *this;
++*this;
return old;
}
friend bool operator==(const iterator&, const iterator&) =
default;
};

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

public:
repeat_n_view() = default;
repeat_n_view(T value, std::size_t count)
: value_(std::move(value)), count_(count) {}

iterator begin() const { return {this, 0}; }
iterator end() const { return {this, count_}; }
};

创建并消费这个 view:

1
2
repeat_n_view<int> repeated{9, 3};
std::println("{}", repeated); // [9, 9, 9]

标准工厂的等价代码更短:

1
std::println("{}", std::views::repeat(9, 3));  // [9, 9, 9]

迭代器以 view 地址判断身份;移动 view 后,旧迭代器不会自动关联新对象。该例不声明 borrowed_range,也不尝试实现标准 repeat_view 的随机访问、大小查询等完整接口。

C++23 std::generator:有界 Fibonacci 序列

std::generator<T> 是 <generator> 中的协程生成器,也是只能单遍消费的 view,不是 views adaptor。通过 <version> 中的 __cpp_lib_generator >= 202207L 检查支持。下面只生成十项,避免无限 Fibonacci 序列最终溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
std::generator<unsigned> fibonacci() {
unsigned a = 0;
unsigned b = 1;
for (int i = 0; i < 10; ++i) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}

std::println("{}", fibonacci());
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

std::println 在格式化期间消费 generator,不保存迭代器;打印后不能重新从头遍历该 generator。最大中间值仍在 unsigned 的可表示范围内;若扩展成无限序列,必须另外处理整数溢出边界。

选用管道的检查清单

  1. 检查源数据的存活时间与迭代器失效规则。view 身份不等于拥有数据,也不等于 borrowed range。
  2. 区分单源闭包与直接调用:counted 接迭代器和数量;zip、zip_transform、cartesian_product 接多个 ranges;all_t 是类型别名。
  3. 检查最低迭代能力:filter、chunk 支持单遍输入,slide、adjacent、chunk_by 要求前向遍历,reverse 要求双向遍历。
  4. 检查边界:非负 take/drop,正的 chunk/slide/stride,split 的空字段,chunk 的短尾块,以及 zip 的最短输入。
  5. 需要独立拥有结果时物化;as_rvalue 只改变元素访问类别,真正的移动发生在后续消费时。将副作用放在终端操作,不依赖惰性转换的调用次数。

参考资料