C++23 std::views::repeat:无限与有限重复序列的生成器详解

发布时间:2026/7/18 8:26:08
C++23 std::views::repeat:无限与有限重复序列的生成器详解 1. 项目概述为什么我们需要一个“无限”的生成器在C的日常开发里我们经常会遇到一些看似简单但写起来却有点啰嗦的场景。比如你想初始化一个容器里面全是同一个值或者你想模拟一个无限循环的输入流用于测试某个算法的边界条件再或者你只是想生成一个简单的、重复的序列作为另一个复杂操作的输入源。在过去你可能得写个for循环或者用std::fill又或者更“现代”一点用std::generate_n配合一个lambda。这些方法都能用但总感觉不够“优雅”特别是当你身处一个range适配器的链条中时突然要插入一个循环就像在一段流畅的旋律里硬塞进一个不和谐的音符。C20引入的Ranges库给我们带来了全新的、声明式的编程体验。我们可以用管道操作符|将视图views像乐高积木一样连接起来构建出复杂的数据处理流水线。但在这个强大的工具箱里一直缺少一个最基础的“积木”一个能无限生成重复值的源头。std::views::iota可以生成递增的整数序列但它不是重复的。我们需要一个能说“给我无限个42”或者“给我10个‘hello’”的东西。这就是std::views::repeat诞生的背景它由提案P2474R2引入并在C23中成为标准库的一部分。简单来说views::repeat(value)会生成一个无限序列其中的每个元素都是那个给定的value。它纯粹、简单却无比强大是构建更复杂数据流的一个理想起点。你可以把它看作一个永不枯竭的泉水源源不断地涌出相同的泉水。接下来我们就深入这个“泉眼”看看它内部是如何工作的以及如何用它来简化我们的代码。2. 核心设计有限与无限的统一视图views::repeat的设计哲学体现了C Ranges库的一个重要理念统一处理有限序列和无限序列。它通过两个重载来实现这一目标这两个重载共同构成了其灵活性的基石。2.1 无限重复views::repeat(value)这是最基本的形式。你给它一个值它还你一个无限的range。#include ranges #include iostream int main() { // 创建一个无限重复42的视图 auto infinite_42 std::views::repeat(42); // 注意这是一个无限范围不能直接遍历全部 // 但我们可以取它的前几个元素 for (int x : infinite_42 | std::views::take(5)) { std::cout x ; // 输出: 42 42 42 42 42 } std::cout \n; }这个视图的类型通常是std::ranges::repeat_viewT其中T是所传递值的类型。它的迭代器解引用永远返回那个存储的或引用到的值的引用或副本。关键在于它的end()迭代器永远不与其他迭代器相等这意味着从逻辑上讲这个范围没有终点。注意直接尝试遍历一个无限的repeat_view例如for (auto x : infinite_42)会导致死循环。你必须通过其他视图如take、take_while或手动控制迭代次数来限制它。这是使用无限视图时需要时刻绷紧的一根弦。2.2 有限重复views::repeat(value, count)这是更常用、也更安全的形式。你指定一个值和一个重复次数它生成一个包含count个该元素的有限范围。#include ranges #include iostream #include vector int main() { // 创建一个包含7个字符串hello的视图 auto hello_seven_times std::views::repeat(hello, 7); // 这是一个有限范围可以直接遍历 for (const auto s : hello_seven_times) { std::cout s ; } std::cout \n; // 输出: hello hello hello hello hello hello hello // 可以直接用来初始化容器 std::vectorstd::string greetings(hello_seven_times.begin(), hello_seven_times.end()); // greetings 现在包含7个hello }当提供count参数时返回的视图类型是std::ranges::repeat_viewT的一个特化可能是一个不同的类型由实现定义但重要的是它的begin()和end()是有效的并且size()返回count如果count的类型允许计算大小。这使它成为一个标准的sized range可以安全地用于所有期望有限范围的算法和容器构造函数。2.3 设计背后的考量为什么是两个重载你可能会问为什么不只提供一个repeat(value, count)当count为某个特殊值比如-1或infinity时表示无限或者反过来只提供无限版本让用户自己用take来限制这里体现了C库设计的精确性和类型安全性。类型安全与接口清晰使用两个重载使得无限和有限版本在类型上可能有细微差别尽管对用户透明并且接口意图极其明确。repeat(x)一眼就知道是无限的repeat(x, n)一眼就知道是有限的。避免了通过“魔法值”来控制行为所带来的歧义和潜在错误。性能与表达能力有限版本的repeat在编译时就知道大小如果count是编译时常量这可以让编译器进行更多优化。例如std::vectorT vec(repeat(T{}, 100).begin(), repeat(T{}, 100).end());编译器可能能推导出大小并进行更高效的内存分配。而无限版本则明确放弃了“大小”这个概念。与其他视图的协同这种设计完美融入了Ranges的生态系统。无限版本是“源头”适合与take、take_while、zip等组合。有限版本本身就是一个完整的、可消费的range可以直接用于构造容器或作为算法输入。它们各司其职让代码的表达力更强。3. 实现原理与迭代器行为剖析要真正用好views::repeat理解它的迭代器行为是关键。这能帮你避免一些隐蔽的陷阱并写出更高效的代码。3.1 迭代器一个“静止”的哨兵repeat_view的迭代器可能是标准库中最简单的迭代器之一。对于无限版本迭代器本质上是一个“计数器”或干脆就是一个“空壳”因为它不需要状态来知道当前值是什么——值永远是那个绑定的值。解引用操作operator*直接返回对该值的常引用或一个副本。有限版本的迭代器会多一个“步数”计数器用来记录离终点还有多远。当迭代器递增时这个计数器递减直到与end()迭代器通常是一个表示计数为0的哨兵相等。一个关键点是对于无限repeat_view它的end()迭代器是一个特殊的“永不相等”的哨兵。这意味着auto inf std::views::repeat(1); auto it inf.begin(); // it ! inf.end() 永远为 true // it; // 迭代器可以递增但解引用值不变且永远不会等于end()所以std::ranges::distance(inf)这样的操作是无效的编译错误或运行时未定义行为因为它试图计算无限序列的长度。3.2 值的存储与引用语义这是repeat_view一个非常重要的细节直接关系到正确性和性能。repeat如何处理传入的值对于纯右值prvalue如repeat(42)或repeat(Foo{})视图存储该值的一个副本。这意味着如果你传入一个临时对象它的生命周期会被延长到和视图本身一样长只要还有迭代器引用着它。这是安全的。对于左值lvalue如int x5; repeat(x)视图默认存储该左值的一个引用通常是std::reference_wrapper。这意味着你必须确保被引用的对象x在视图被使用期间一直有效。如果x被销毁了再通过视图访问就是悬垂引用会导致未定义行为。#include ranges #include iostream #include vector int main() { std::vectorint data {1, 2, 3}; // 错误示范悬垂引用 auto bad_view std::views::repeat(data.back()); // data.back()返回引用但repeat存储了什么 // 更清晰的错误repeat一个临时向量的元素 auto get_temp []() - std::vectorint { return {4,5,6}; }; // auto dangerous std::views::repeat(get_temp().front()); // 危险临时向量在表达式结束后被销毁 // 正确做法1明确拷贝值 int last_value data.back(); auto safe_view_by_copy std::views::repeat(last_value); // 正确做法2使用std::ref明确要求引用并自己管理生命周期高级用法需谨慎 // auto explicit_ref_view std::views::repeat(std::ref(data)); // 引用整个容器 // 但必须确保data在view使用时存活 }实操心得我个人的经验法则是除非有明确的性能需求且能严格保证生命周期否则在views::repeat中优先传入值。对于简单的标量类型int,double,char拷贝开销可忽略不计。对于较大的对象如果确定需要重复引用同一个不变的对象可以考虑使用std::cref来获得常量引用但一定要画清楚对象的生命周期图。一个常见的坑是在lambda里捕获局部变量并返回一个repeat视图而该视图在lambda外被使用。3.3 与views::generate和views::iota的对比理解repeat的定位也需要看看它的“邻居”。views::generate接受一个可调用对象如lambda每次迭代时调用它来生成新值。这提供了最大的灵活性每个元素都可以不同。repeat可以看作是generate的一个特例generate([]{ return 42; })在功能上类似于repeat(42)但generate的调用开销可能更大且语义上强调“生成”而repeat强调“重复”。views::iota生成一个递增的整数或更一般地可递增类型的序列。它生成的是不同的值。repeat生成的是相同的值。选择哪一个需要完全相同的值 -repeat需要按固定步长递增的值 -iota需要每个元素都根据复杂逻辑计算 -generate4. 实战应用场景与代码示例理论说再多不如代码来得实在。下面我们看几个views::repeat在实际编码中发光发热的场景。4.1 场景一容器初始化与填充这是最直接的用途。过去你要用循环或std::fill_n现在一行搞定。#include ranges #include vector #include list #include iostream #include algorithm int main() { // 初始化一个vector包含100个0 std::vectorint zeros(std::views::repeat(0, 100).begin(), std::views::repeat(0, 100).end()); // 更简洁的C23方式如果构造函数支持range // std::vectorint zeros(std::views::repeat(0, 100)); // 用特定值填充已有容器的一部分 std::listdouble data(10); auto start std::ranges::begin(data); std::ranges::copy(std::views::repeat(3.14159, 5), start); // 将前5个元素设为π // 快速创建一个用于测试的“模拟数据”范围 auto mock_sensor_data std::views::repeat(25.5, 60); // 模拟1分钟60秒的恒定温度读数 for (auto temp : mock_sensor_data | std::views::take(10)) { std::cout temp ; } }4.2 场景二与其他视图组合构建复杂数据流这是repeat真正强大的地方。作为数据流的源头它可以被后续的视图转换、过滤、组合。#include ranges #include iostream #include vector #include format // C20 int main() { // 示例1生成一个坐标网格的索引 (0,0), (0,1), (0,2), (1,0), (1,1), (1,2)... int rows 3, cols 4; auto row_indices std::views::iota(0, rows); // 0,1,2 auto col_indices std::views::iota(0, cols); // 0,1,2,3 // 旧方法嵌套循环。新方法用repeat和iota组合。 // 思路将行索引重复cols次列索引平铺rows次然后zip。 auto repeated_rows row_indices | std::views::transform([cols](int r) { return std::views::repeat(r, cols); }) | std::views::join; // 得到 0,0,0,0, 1,1,1,1, 2,2,2,2 auto cycled_cols col_indices | std::views::cycle | std::views::take(rows * cols); // 得到 0,1,2,3, 0,1,2,3, 0,1,2,3 auto grid_indices std::views::zip(repeated_rows, cycled_cols); for (auto [r, c] : grid_indices) { std::cout std::format(({},{}) , r, c); } std::cout \n; // 示例2为一系列数据添加固定的标签头 std::vectorstd::string items {apple, banana, cherry}; auto header std::views::repeat(Item: , items.size()); auto labeled_items std::views::zip_transform( [](const std::string h, const std::string i) { return h i; }, header, items ); for (const auto labeled : labeled_items) { std::cout labeled \n; } }4.3 场景三算法测试与默认参数生成在编写泛型算法或进行单元测试时repeat可以快速生成测试数据。#include ranges #include algorithm #include vector #include cassert // 一个假设的算法计算范围内满足谓词的元素个数直到遇到连续N个不满足的为止。 templatestd::input_iterator Iter, std::sentinel_forIter Sent, typename Pred std::size_t count_until_stop(Iter first, Sent last, Pred p, std::size_t stop_n) { std::size_t count 0; std::size_t consecutive_fail 0; for (; first ! last consecutive_fail stop_n; first) { if (p(*first)) { count; consecutive_fail 0; } else { consecutive_fail; } } return count; } void test_count_until_stop() { // 测试用例1全是true应该一直数下去但被有限的repeat范围限制 auto all_true std::views::repeat(true, 100); auto result1 count_until_stop(all_true.begin(), all_true.end(), [](bool b){return b;}, 3); assert(result1 100); // 数了100个 // 测试用例2真假交替在遇到连续3个false时停止 // 构造序列true, false, true, false, false, false, true... // 手动构造有点烦用repeat和take组合 auto pattern std::views::repeat(true) | std::views::take(1) | std::views::repeat(false) | std::views::take(1) | std::views::repeat(true) | std::views::take(1) | std::views::repeat(false) | std::views::take(3); // 但这样写不优雅。更清晰的方式可能是用generate。这里用repeat演示另一种思路 // 我们有一个索引序列根据索引奇偶性决定true/false但需要特定的false连续模式。 // 这说明了repeat适合生成均匀数据复杂模式可能需结合其他视图或generate。 }4.4 场景四模拟无限流与惰性求值repeat的无限版本是模拟数据流或作为默认后备值的理想工具。#include ranges #include iostream #include optional #include vector // 模拟一个可能耗尽的传感器耗尽后返回nullopt std::optionalint simulated_sensor() { static int call_count 0; if (call_count 5) { return 100 call_count; // 前5次有读数 } else { return std::nullopt; // 之后“耗尽” } } int main() { // 创建一个数据流当传感器耗尽时用默认值-1填充 auto sensor_stream std::views::generate(simulated_sensor) | std::views::take_while([](const auto opt) { return opt.has_value(); }) | std::views::transform([](const auto opt) { return opt.value(); }); // 假设我们至少需要10个数据不足的用-1补足 constexpr std::size_t required_count 10; auto default_filler std::views::repeat(-1); // 将有效数据流和无限默认值流拼接起来然后取前required_count个 auto guaranteed_stream std::views::concat(sensor_stream, default_filler) | std::views::take(required_count); std::vectorint collected_data(guaranteed_stream.begin(), guaranteed_stream.end()); // collected_data 内容 [101,102,103,104,105, -1,-1,-1,-1,-1] for (int v : collected_data) std::cout v ; std::cout \n; }5. 性能考量、常见陷阱与最佳实践任何强大的工具都需要了解其特性才能避免误用。下面是一些关于views::repeat的重要注意事项。5.1 性能特点零开销抽象对于简单类型repeat_view的迭代器操作和解引用通常会被编译器高度优化内联后可能和直接写循环一样高效。它本身不产生额外的动态内存分配。惰性求值和所有标准库视图一样repeat是惰性的。auto r views::repeat(42, 1000000);这行代码不会立即创建一百万个42它只是创建了一个轻量的视图对象。元素只有在被遍历解引用时才会“出现”。这对于处理大规模虚拟序列至关重要。与std::vector初始化对比std::vectorint v(100, 42);会在构造时分配内存并填充100个42。auto r views::repeat(42, 100);则什么都不分配直到你开始拷贝它。如果你最终需要容器那么直接构造vector可能更高效。如果你只是中间处理或者需要无限序列那么repeat是唯一选择。5.2 常见陷阱与排查陷阱一无限循环auto infinite std::views::repeat(1); // 错误死循环 // for (auto x : infinite) { /* ... */ } // 错误试图获取大小 // auto size std::ranges::size(infinite); // 编译错误或UB排查编译器可能不会警告。务必清楚你创建的是无限视图。使用它时必须搭配take、take_while或在for循环中使用明确的终止条件如手动计数。陷阱二悬垂引用std::functionint() get_value []{ return rand() % 100; }; int current_val get_value(); auto view std::views::repeat(current_val); // 存储了current_val的引用或拷贝 current_val 999; // 修改原值 // view中的值取决于实现如果是拷贝还是旧值如果是引用变成999。行为不直观易出错。排查对于左值明确你的意图。如果需要快照先拷贝到局部变量。如果需要动态更新考虑用generate而不是repeat。陷阱三类型推导的意外auto v1 std::views::repeat(42); // repeat_viewint存储int auto v2 std::views::repeat(42, 10); // 同上有限版本 const int ci 42; auto v3 std::views::repeat(ci); // repeat_viewconst int 或类似可能存储const int引用 int i 42; auto v4 std::views::repeat(i); // 可能存储int通过reference_wrapper auto v5 std::views::repeat(std::as_const(i)); // 明确要常量引用排查在泛型代码或对视图元素类型有严格要求时使用decltype或std::ranges::range_value_t来检查视图的元素类型到底是什么。陷阱四与不支持的算法混用不是所有算法都支持无限范围或输入迭代器。例如std::ranges::sort要求随机访问范围而repeat_view的迭代器通常只是输入或前向迭代器。auto r std::views::repeat(3, 5) | std::views::take(10); // 一个有限范围 // std::ranges::sort(r); // 错误repeat_view的迭代器可能不满足随机访问迭代器要求排查了解你所使用的算法对迭代器类别的要求。repeat_view通常提供输入或前向迭代器。5.3 最佳实践总结优先使用有限形式除非确实需要无限序列否则总是使用repeat(value, count)。这更安全语义更清晰并且能与其他期望有限范围的组件更好地协作。对左值保持警惕传入左值时想清楚你是需要引用它的当前状态还是需要一份拷贝。如果不确定或者对象生命周期可能先于视图结束先做显式拷贝。善用管道操作符repeat天生适合作为管道操作的起点。repeat(x) | views::transform(f) | views::take(n)比手动写循环更清晰。理解它是视图不是容器repeat生成的是轻量级的视图不拥有数据。如果你需要持久化、随机访问或多次遍历应该将视图转换为容器如std::vector。在测试中大胆使用它是生成测试数据的利器无论是恒定值、边界值还是作为占位符。std::views::repeat可能不是Ranges库中最复杂的组件但它的简洁和基础性使其成为工具箱中不可或缺的一员。它填补了“重复数据生成”这一空白让声明式的范围操作链条更加完整。下次当你需要一堆相同的值作为起点时别再写for循环了试试views::repeat你会发现代码的意图变得更加清晰直白。