1. 项目概述为什么我们需要 Ranges如果你写过几年 C尤其是用过 STL 算法那你肯定对std::sort(vec.begin(), vec.end())这种模式再熟悉不过了。每次调用算法都得手动传一对迭代器指明操作的范围。代码写多了你会发现这其实挺啰嗦的而且容易出错——比如不小心传了vec.end()和vec.begin()或者迭代器类型不匹配编译器报错信息能让你看半天。更重要的是当你想把几个操作组合起来比如先筛选再转换代码就会变得冗长且需要中间变量既不优雅性能上也可能有额外的开销。C20 引入的 Ranges 程序库就是为了解决这些问题。它不是一个孤立的新功能而是一套对 STL 算法和迭代器模型的全面革新。简单来说Ranges 让“范围”成为一等公民。你可以直接对一个容器比如std::vector调用算法而不用再显式地传递迭代器对。更重要的是它引入了“视图”和“管道操作符|”让你能以声明式、函数式的风格来组合数据转换操作代码瞬间变得清晰、简洁并且因为视图的惰性求值特性性能上往往还有惊喜。这不仅仅是语法糖它背后是一整套新的概念、约束和编译期检查机制也就是标题里提到的“受约束算法”。这意味着编译器能在你写代码的早期就捕获更多错误给出更清晰的错误信息。对于日常开发Ranges 能显著提升代码的表达力和可维护性对于库作者和追求极致性能与安全的开发者它提供了更强大、更安全的抽象工具。接下来我们就深入拆解这个强大的新武器。2. 核心概念拆解Range, View, 算法与概念约束要玩转 Ranges必须先理解它的几个核心基石。它们共同构建了一套比传统迭代器更安全、更富表达力的体系。2.1 什么是 Range范围在 Ranges 的世界里一个Range就是任何可以被迭代的东西。更正式地说一个 Range 需要提供一对迭代器或一个迭代器和一个哨兵来标识其开始和结束。传统迭代器对std::pairiterator, sentinel或std::pairbegin, end。这是最直接的理解。Sentinel哨兵这是一个关键创新。哨兵是一个可以与迭代器进行比较的“标记”用于标识范围的结束。它的类型不一定要与迭代器相同。例如一个以空字符\0结尾的 C 风格字符串其迭代器是char*而哨兵可以是一个特殊的类型用于与\0比较。这带来了更大的灵活性。所有 STL 容器vector,list,map等天然都是 Range。一个初始化列表{1, 2, 3}也可以被适配成一个 Range。Ranges 库提供了一系列函数如std::ranges::begin(),std::ranges::end()来统一地获取任何 Range 的起始和结束。2.2 View视图惰性求值与组合魔法View是 Ranges 库的灵魂它是一种轻量级的、非拥有的 Range。你可以把它想象成一个“透镜”或“滤镜”透过它去看或转换底层的数据但 View 本身并不存储数据副本。View 的核心特性惰性求值View 定义的操作不会立即执行。只有当你真正去迭代例如在 for 循环中取值时转换才会发生。这避免了不必要的中间存储和计算。可组合性多个 View 可以通过管道操作符|连接起来形成一个操作链。数据像流水一样通过这个管道每个 View 对其进行一次处理。常量时间复杂度移动、复制、赋值一个 View 都是 O(1) 操作与底层元素数量无关。例如std::views::filter和std::views::transform都是生成 View 的适配器。input | views::filter(pred) | views::transform(f)这行代码并没有创建任何新的容器只是定义了一个“计算规则”。注意由于 View 不拥有数据你必须确保底层 Range 的生命周期长于 View。使用临时容器创建 View 然后存储起来是未定义行为是新手常踩的坑。2.3 受约束算法更安全的 STL 算法传统的 STL 算法如std::sort是模板函数对迭代器类型的约束很弱主要依赖“命名要求”。如果传入错误的迭代器比如对std::list的迭代器调用std::sort错误信息可能晦涩难懂或者直到运行时才会出问题比如std::list迭代器不支持随机访问但编译能过链接或运行时报错。C20 的受约束算法位于std::ranges命名空间下如std::ranges::sort。它们有两个关键改进接受 Range 作为参数你可以直接写std::ranges::sort(my_vec)而不是std::sort(my_vec.begin(), my_vec.end())。使用概念进行约束算法通过 C20 的 Concept 对迭代器或 Range 的类型施加编译期约束。例如ranges::sort要求传入的 Range 满足random_access_range且元素可比较。如果你传一个std::list给它编译器会在调用处直接给出清晰的错误信息指出“此处需要随机访问迭代器但提供的迭代器不满足”将错误发现提前到了编译期并且定位更准确。2.4 Range 概念与迭代器类别Ranges 库定义了一套层次化的概念用于对 Range 的能力进行分类。这比传统的迭代器分类更直观并与容器特性直接关联。概念含义支持的 STL 容器示例std::ranges::input_range至少可以单向遍历一次。std::istream_view(从输入流读取)std::ranges::forward_range可以多次单向遍历。迭代器支持多趟算法。std::forward_list,std::unordered_setstd::ranges::bidirectional_range可以向前和向后遍历。std::list,std::set,std::mapstd::ranges::random_access_range支持常数时间的随机访问[]操作符。std::vector,std::deque,std::array,std::stringstd::ranges::contiguous_range元素在内存中连续存储。std::vector,std::array,std::string这些概念形成了“细化”关系contiguous_range-random_access_range-bidirectional_range-forward_range-input_range。一个满足random_access_range的容器自动也满足其下的所有概念。算法可以根据需要的最弱概念来约束参数使接口既通用又安全。3. 核心工具详解视图适配器与管道操作视图适配器是 Ranges 库中用于创建 View 的工具函数或对象。它们通常以std::views::为前缀或者通过std::ranges::下的视图工厂函数访问。管道操作符|则是连接它们的胶水。3.1 常用视图适配器实战让我们通过代码示例来感受几个最常用的视图适配器。1.filter过滤元素#include iostream #include ranges #include vector int main() { std::vectorint numbers {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 获取所有偶数视图 auto even_view numbers | std::views::filter([](int n) { return n % 2 0; }); std::cout Even numbers: ; for (int n : even_view) { // 惰性求值在此发生 std::cout n ; } // 输出: Even numbers: 2 4 6 8 10 }filter接受一个谓词返回bool的函数或可调用对象创建一个只包含满足谓词元素的视图。2.transform转换元素#include iostream #include ranges #include vector #include string int main() { std::vectorint nums {1, 2, 3}; auto squared_view nums | std::views::transform([](int n) { return n * n; }); std::cout Squares: ; for (auto s : squared_view) { std::cout s ; // 输出: Squares: 1 4 9 } // 结合使用先过滤再转换 std::vectorstd::string words {hello, world, ranges, cpp, twenty}; auto result words | std::views::filter([](const std::string s) { return s.size() 4; }) // 长度4 | std::views::transform([](const std::string s) { return s.size(); }); // 取其长度 std::cout \nLength of long words: ; for (int len : result) { std::cout len ; // 输出: Length of long words: 5 5 6 } }3.take与drop取子范围#include iostream #include ranges #include vector int main() { std::vectorint vec {10, 20, 30, 40, 50, 60}; // 取前3个元素 auto first_three vec | std::views::take(3); // 丢弃前2个元素取剩下的 auto after_two vec | std::views::drop(2); for (int v : first_three) std::cout v ; // 输出: 10 20 30 std::cout \n; for (int v : after_two) std::cout v ; // 输出: 30 40 50 60 }take(n)创建一个包含前n个元素的视图drop(n)创建一个跳过前n个元素的视图。它们对于处理无限序列或流式数据特别有用例如与std::views::iota生成无限序列结合。4.iota生成序列#include iostream #include ranges int main() { // 生成从0开始的无限整数序列取前5个 auto infinite_view std::views::iota(0) | std::views::take(5); for (int i : infinite_view) { std::cout i ; // 输出: 0 1 2 3 4 } std::cout \n; // 生成一个有限范围 [5, 10) auto finite_view std::views::iota(5, 10); for (int i : finite_view) { std::cout i ; // 输出: 5 6 7 8 9 } }iota是一个强大的视图工厂用于生成一个单调递增的序列。只给一个参数时生成从该值开始的无限序列给两个参数时生成[begin, end)的序列。3.2 管道操作符|的魔力与求值顺序管道操作符|的引入让代码从“嵌套函数调用”的风格转变为“从左到右的数据流”风格极大地提升了可读性。// 传统风格需要中间变量或嵌套 std::vectorint temp; std::copy_if(src.begin(), src.end(), std::back_inserter(temp), pred); std::transform(temp.begin(), temp.end(), dest.begin(), func); // Ranges 管道风格声明式无中间变量 auto result_view src | std::views::filter(pred) | std::views::transform(func); // 如果需要具体容器可以再从这个视图构造 std::vectorint dest(result_view.begin(), result_view.end());求值顺序管道操作符是左结合的求值顺序是从左到右。这意味着数据先经过最左边的适配器处理再传递给右边的适配器。这与 Unix 的管道|语义一致非常直观。实操心得当视图链较长时适当换行并对齐管道符号可以让数据流的转换步骤一目了然这是提升现代 C 代码可读性的一个小技巧。4. 受约束算法深度解析与对比现在让我们深入看看std::ranges命名空间下的算法并与传统算法进行对比理解其“约束”体现在何处。4.1 算法签名与约束一个典型的受约束算法签名如下namespace std::ranges { template std::random_access_range R, typename Comp std::ranges::less, typename Proj std::identity requires std::sortablestd::ranges::iterator_tR, Comp, Proj constexpr std::ranges::borrowed_iterator_tR sort(R r, Comp comp {}, Proj proj {}); }解读模板参数R它被std::random_access_range概念约束。这意味着编译器会检查你传入的r是否满足随机访问 Range 的要求。如果传一个std::list这里就会编译失败。requires子句进一步约束迭代器必须是“可排序的”。std::sortable是一个组合概念它要求迭代器是随机访问的并且元素在给定的比较器和投影下可以建立严格弱序。参数Proj这是受约束算法新增的强大功能——投影Projection。它允许你在比较元素之前先对每个元素应用一个转换。这避免了在谓词中重复写转换逻辑使代码更清晰。4.2 投影Projection的妙用投影是受约束算法的一大亮点它极大地简化了基于成员或特定属性进行操作的代码。传统方式繁琐struct Person { std::string name; int age; }; std::vectorPerson people {{Alice, 30}, {Bob, 25}, {Charlie, 35}}; // 按年龄排序 std::sort(people.begin(), people.end(), [](const Person a, const Person b) { return a.age b.age; }); // 按名字长度查找 auto it std::find_if(people.begin(), people.end(), [](const Person p) { return p.name.size() 3; });使用 Ranges 和投影简洁#include algorithm #include ranges // 按年龄排序 std::ranges::sort(people, {}, Person::age); // 等价于 std::ranges::sort(people, std::ranges::less{}, Person::age); // 第一个参数是 Range第二个是比较器默认less第三个是投影指向成员的指针 // 按名字长度查找 auto it std::ranges::find_if(people, [](std::size_t len) { return len 3; }, Person::name); // 投影 Person::name 先作用于每个元素将 Person 映射为 name.size()再传给谓词判断可以看到通过投影我们无需在 lambda 中手动访问成员代码意图更清晰也减少了重复。投影可以是成员指针、成员函数指针或者任何可调用对象。4.3 算法返回值与安全性传统算法通常返回一个迭代器如std::find返回找到位置的迭代器或end()。受约束算法则返回一个更安全的类型std::ranges::dangling或一个subrange。dangling悬垂如果算法返回的迭代器可能指向一个临时 Range其生命周期已结束则返回std::ranges::dangling这个特殊标记防止你误用。例如对临时容器的视图进行ranges::find操作。subrange很多算法如ranges::search,ranges::equal_range返回一个std::ranges::subrange它本身就是一个 Range包含起止迭代器可以直接用于后续的 Range 操作或构造容器比返回std::pairiterator, iterator更方便、更现代。#include ranges #include vector #include algorithm auto get_temp_vector() { return std::vectorint{1, 2, 3}; } int main() { // 传统方式危险返回的迭代器指向已销毁的临时容器。 // auto bad_it std::find(get_temp_vector().begin(), get_temp_vector().end(), 2); // 未定义行为 // Ranges 方式安全。返回 dangling防止误用。 auto safe_result std::ranges::find(get_temp_vector(), 2); // safe_result 的类型可能是 std::ranges::dangling编译期或使用时会提示安全问题。 static_assert(std::same_asdecltype(safe_result), std::ranges::dangling); // 可能成立 // 返回 subrange 的例子 std::vectorint vec {5, 2, 8, 2, 9}; // 查找子序列 [2, 8] std::arrayint, 2 sub {2, 8}; if (auto found std::ranges::search(vec, sub); !found.empty()) { // found 是一个 subrange指向 vec 中匹配的位置 for (int v : found) { // 可以直接遍历 std::cout v ; // 输出: 2 8 } // 也可以获取其迭代器 auto [begin_it, end_it] found; } }5. 实战从旧代码迁移到 Ranges 风格将现有代码迁移到使用 Ranges不仅能简化代码还能借助概念约束提升安全性。我们来看几个常见的重构场景。5.1 场景一替换简单的算法调用这是最直接的替换。找到std::算法调用加上ranges::命名空间并将迭代器对替换为整个容器或 Range。重构前std::vectorint data /* ... */; std::sort(data.begin(), data.end()); auto it std::find(data.begin(), data.end(), 42); if (it ! data.end()) { /* ... */ }重构后#include algorithm // ranges 算法也在 algorithm 中 #include ranges // 需要视图适配器时才必须包含 std::ranges::sort(data); // 简洁 auto it std::ranges::find(data, 42); if (it ! data.end()) { /* 用法不变 */ }如果data是const的ranges::find会自动返回const_iterator更安全。5.2 场景二重构多步骤的数据处理管道这是 Ranges 优势最明显的场景。消除中间变量用管道组合操作。重构前过滤并转换std::vectorint input {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::vectorint intermediate; std::vectorint output; // 1. 过滤出能被3整除的数 std::copy_if(input.begin(), input.end(), std::back_inserter(intermediate), [](int n) { return n % 3 0; }); // 2. 计算平方 std::transform(intermediate.begin(), intermediate.end(), std::back_inserter(output), [](int n) { return n * n; }); // output: {0, 9, 36, 81}重构后使用视图管道#include ranges #include vector #include iostream int main() { std::vectorint input {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 定义计算规则惰性求值无中间存储 auto result_view input | std::views::filter([](int n) { return n % 3 0; }) | std::views::transform([](int n) { return n * n; }); // 如果需要具体容器 std::vectorint output(result_view.begin(), result_view.end()); // 或者直接使用视图 for (int val : result_view) { std::cout val ; // 输出: 0 9 36 81 } }代码行数减少意图更清晰且避免了intermediate的额外内存分配和拷贝。如果最终不需要存储所有结果只是遍历一次那么视图方案在性能和内存上都有优势。5.3 场景三处理非标准容器或自定义迭代器Ranges 库的设计目标之一就是统一迭代接口。如果你的自定义容器提供了begin()和end()它就能自动作为 Range 使用。#include ranges class MyContainer { // ... 内部数据存储 public: int* begin() { /* ... */ } int* end() { /* ... */ } const int* begin() const { /* ... */ } const int* end() const { /* ... */ } }; int main() { MyContainer c; // 现在可以直接对其使用 ranges 算法 if (std::ranges::find(c, 100) ! c.end()) { // ... } // 或者使用视图 for (int v : c | std::views::take(5)) { // ... } }注意事项迁移时要特别注意算法对迭代器类别的要求。ranges::sort要求random_access_range如果你的容器只提供双向迭代器如std::list编译器会报错这时你应该改用container.sort()成员函数或ranges::stable_sort如果支持。受约束算法的错误信息通常会比传统模板错误更友好直接指出哪个概念不满足。6. 性能考量、陷阱与最佳实践Ranges 带来了表达力的飞跃但若使用不当也可能引入性能陷阱或难以察觉的 Bug。下面是一些关键的实践要点。6.1 惰性求值的代价与缓存视图的惰性求值是一把双刃剑。它节省了不必要的计算和存储但如果你多次遍历同一个复杂视图每次遍历都会重新计算。auto expensive_view big_data | std::views::filter(complex_predicate) | std::views::transform(heavy_transform); // 第一次遍历计算发生 for (const auto elem : expensive_view) { /* 使用 elem */ } // 第二次遍历所有过滤和转换会再发生一次 for (const auto elem : expensive_view) { /* 再次使用 elem */ }最佳实践如果需要对结果进行多次随机访问或频繁遍历考虑将视图物化materialize到一个容器中std::vectorResultType cached_result(expensive_view.begin(), expensive_view.end()); // 现在可以高效地多次使用 cached_result6.2 视图的生命周期问题这是新手最容易踩的坑。视图不拥有数据它只是底层 Range 的一个“观察者”。如果底层数据被销毁或失效视图就变成了“悬垂引用”。auto get_dangerous_view() { std::vectorint local_data {1, 2, 3}; return local_data | std::views::filter([](int n) { return n 1; }); // 大坑 } // local_data 被销毁 int main() { auto view get_dangerous_view(); // view 持有指向已销毁数据的“指针” for (int i : view) { // 未定义行为访问已释放内存。 std::cout i; } }如何避免确保视图依赖的原始数据的生命周期完全覆盖视图的使用期。对于返回视图的函数如果其底层数据是函数内的局部变量绝对不要返回该视图。要么返回物化后的容器要么让调用者提供数据源。使用std::ranges::owning_viewC20或std::ranges::ref_view来明确视图的所有权或引用语义但需谨慎。6.3 管道操作的求值顺序与副作用虽然管道操作符|从左到右求值但每个元素的处理是“拉取”式的。在filter后接transform的链中对于每个元素会先应用filter的谓词如果通过再立即应用transform的函数然后结果被消费例如打印。这通常符合直觉。但要小心带有副作用的操作如修改外部状态、打印日志。由于视图的惰性和可能的优化副作用的执行次数和时机可能与直观的“先全部过滤再全部转换”不同。int counter 0; auto view std::views::iota(0, 5) | std::views::transform([counter](int i) { counter; // 副作用 return i * 2; }) | std::views::filter([](int i) { return i % 4 0; }); // 此时 counter 仍然是 0因为还没有迭代 std::cout Before loop, counter counter \n; for (int v : view) { std::cout v ; } // 输出可能是0 4 // counter 的值是多少是 5所有元素都被转换了还是 2只有通过 filter 的元素被转换了 // 实际上由于惰性求值transform 只对通过 filter 的元素执行所以 counter 最终为 2。 std::cout \nAfter loop, counter counter \n;建议尽量避免在视图适配器的函数对象中引入可观察的副作用。如果必须要有务必清楚理解惰性求值和短路求值带来的影响。6.4 编译时间与调试Ranges 和 Concept 的广泛使用可能会增加编译时间尤其是在大量使用模板和复杂视图组合的项目中。这是为了获得更安全的类型检查和更优的运行时性能所付出的代价。在调试时由于视图是惰性的在调试器中直接查看一个视图变量可能不会显示其元素内容因为它可能只是一个包含迭代器和函数对象的轻量级对象。你需要通过迭代或将其转换为容器来观察结果。7. 进阶话题与自定义扩展当你熟悉了基础用法后可以探索更强大的功能甚至创建自己的视图适配器。7.1 组合复杂视图视图适配器可以任意组合创建出强大的数据处理管道。#include ranges #include vector #include iostream #include string int main() { using namespace std::string_literals; std::vectorstd::string texts {apple, banana, cherry, date, elderberry}; // 一个复杂的管道取长度5的字符串反转它们然后取前3个 auto complex_view texts | std::views::filter([](const std::string s) { return s.size() 5; }) | std::views::transform([](std::string s) { std::ranges::reverse(s); return s; }) | std::views::take(3); for (const auto s : complex_view) { std::cout s \n; } // 输出 // elppa // ananab // yrrehc }7.2 创建自定义视图适配器虽然标准库提供了丰富的适配器但有时你需要特定的转换逻辑。你可以通过编写一个范围适配器闭包对象来创建自定义视图。一个简单的方式是组合现有的适配器或者实现一个满足std::ranges::view_interface的类。这里展示一个利用现有工具创建“相邻元素对”视图的例子#include ranges #include vector #include iostream // 一个返回“相邻对”视图的适配器 inline constexpr auto pairwise std::views::transform([](auto range) { // 这里需要一些技巧实际中可以使用 ranges::views::adjacent (C23) 或 ranges::views::slide (C23) // 以下是一个简化示例假设 range 是 size 2 的 return std::views::iota(0uz, std::ranges::size(range) - 1) | std::views::transform([range](size_t i) { return std::pair{range[i], range[i1]}; }); }); // 更实用的使用 ranges::views::slide (C23) // #include ranges // C23 // inline constexpr auto pairwise std::views::slide(2); int main() { std::vectorint nums {1, 2, 3, 4, 5}; // 使用自定义适配器假设实现正确 // for (auto [a, b] : nums | pairwise) { // std::cout ( a , b )\n; // } // 输出: (1, 2) (2, 3) (3, 4) (4, 5) // C23 更简单 #if __cplusplus 202002L for (auto window : nums | std::views::slide(2)) { std::cout ( window[0] , window[1] )\n; } #endif }创建生产级别的自定义适配器需要对 Range 概念、迭代器、哨兵有深入理解通常是为库开发人员准备的。对于大多数应用组合标准适配器已足够。7.3 与协程和生成器结合C20 的协程为惰性序列生成提供了另一种机制。Ranges 视图可以与协程生成器很好地配合。例如你可以写一个协程来生成斐波那契数列然后将其适配为一个 Range进而使用所有的 Ranges 算法和视图。#include ranges #include iostream #include generator // C23 标准库 generator // C23 示例协程生成器作为 Range #if __cplusplus 202002L std::generatorint fibonacci() { int a 0, b 1; while (true) { co_yield a; std::tie(a, b) std::pair{b, a b}; } } int main() { // 取斐波那契数列的前10个偶数 for (int num : fibonacci() | std::views::filter([](int n) { return n % 2 0; }) | std::views::take(10)) { std::cout num ; } // 输出: 0 2 8 34 144 610 2584 10946 46368 196418 } #endif这展示了函数式编程、惰性求值和协程在现代 C 中融合的强大能力。8. 总结与未来展望C20 Ranges 程序库不仅仅是一套新算法它代表着 C 标准库向更安全、更声明式、更易于组合的方向迈出的重要一步。通过将“范围”作为核心抽象引入视图和管道操作符并利用概念强化接口约束它显著提升了代码的简洁性、可读性和安全性。核心收获直接对容器操作告别冗长的begin()/end()对。声明式管道使用|组合filter,transform,take等操作代码如流水般清晰。惰性求值避免不必要的中间存储和计算提升性能。编译期安全受约束算法通过概念提前捕获类型错误错误信息更友好。投影简化基于数据成员的操作。当前挑战编译时间开销、对编译器支持要求高需要完全支持 C20、以及视图生命周期的管理需要格外小心。未来C23及以后Ranges 库仍在持续进化。C23 引入了更多实用的视图适配器如zip,zip_transform,slide,chunk_by,cartesian_product等使得数据处理能力更加强大。std::generator的加入也让协程与 Ranges 的集成更加无缝。随着生态的完善和开发者经验的积累Ranges 必将成为现代 C 高性能、高表达力代码的标准配置。对于开发者而言现在就是开始学习和尝试 Ranges 的最佳时机。从小的代码片段开始逐步将旧的迭代器循环和算法调用替换为 Ranges 风格你会逐渐体会到它带来的效率与优雅。