
1. 问题起源为什么需要 Ranges传统 STL 算法使用迭代器对 [begin, end) 表达范围这种方式存在三个根本性问题1.1 迭代器对分离是万恶之源// 传统写法begin/end 必须成对传递容易出错 std::vectorint v {1, 2, 3, 4, 5, 6, 7, 8}; auto it std::find_if(v.begin(), v.end(), [](int x) { return x 5; }); // 稍有不慎就会写出 // std::find_if(v.begin(), w.end(), pred); // 不同容器的迭代器UB // Ranges 写法范围是一个整体 auto it std::ranges::find_if(v, [](int x) { return x 5; });1.2 组合操作需要中间容器// 传统写法每一步都需要中间容器产生大量临时分配 std::vectorint temp1, temp2, result; std::copy_if(v.begin(), v.end(), std::back_inserter(temp1), [](int x) { return x % 2 0; }); std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2), [](int x) { return x * x; }); std::copy_n(temp2.begin(), 4, std::back_inserter(result)); // 三次内存分配大量数据拷贝 // Ranges 写法零开销组合一次遍历完成 auto result v | std::views::filter([](int x) { return x % 2 0; }) | std::views::transform([](int x) { return x * x; }) | std::views::take(4); // 零中间分配惰性求值1.3 排序必须传递整个范围// 传统写法无法对部分范围排序而不影响原容器 std::sort(v.begin(), v.end()); // 只能全排 // Ranges 写法投影Projection按成员排序无需手写比较器 struct Person { std::string name; int age; }; std::vectorPerson people {{Alice, 30}, {Bob, 25}, {Charlie, 35}}; std::ranges::sort(people, {}, Person::age); // 范围 比较器默认 投影按age字段排序维度传统 STLC20 Ranges范围表达begin/end 迭代器对单一 range 对象组合方式中间容器 多次遍历管道运算符 惰性求值比较器手写 lambda 或函数对象投影 projection 简化安全检查无迭代器失效是 UBborrowed_range 概念约束类型推导复杂的迭代器类型auto Concept 约束2. 核心概念Range / View / Sentinel / Projection2.1 std::ranges::range 概念// range 概念的核心定义简化版 templatetypename T concept range requires(T t) { std::ranges::begin(t); // 必须有 begin std::ranges::end(t); // 必须有 end }; // 注意begin 和 end 可以返回不同类型sized_sentinel任何满足了 begin() 和 end() 的类型都是 range。与经典 STL 的关键区别是begin 和 end 的返回类型可以不同这催生了 sentinel哨兵概念。2.2 View轻量级的 Range 包装器// View 是 O(1) 拷贝/移动/析构的 range templatetypename T concept view rangeT std::movableT std::default_initializableT; // View 的语义保证拷贝视图不拷贝底层数据 auto v1 std::views::iota(0, 100); // iota_viewint auto v2 v1; // O(1) 拷贝v2 是 v1 的独立副本 // 两者可以独立迭代互不影响View 的设计精髓View 本身存储的是如何生成元素的逻辑而非元素本身。拷贝 View 仅仅是拷贝迭代状态不触发底层数据的任何拷贝。2.3 Sentinel非对称范围的终点// 经典示例C风格字符串 const char* str hello world; // 传统写法需要先 strlen 才能构造 end 迭代器 auto traditional_end str strlen(str); // 必须先知道长度 // Rangessized_sentinel 允许 begin 和 end 类型不同 // std::unreachable_sentinel_t 可以表示无限范围 auto sentinel std::unreachable_sentinel; // 永不等于任何迭代器 // 实际用例以 \0 为终止条件 auto cstr_view std::ranges::subrange(str, std::unreachable_sentinel); // begin(char*) end(哨兵)2.4 Projection排序 / 查找的无侵入投影// 投影是 C20 Ranges 最被低估的特性 std::vectorstd::string words {apple, banana, Cherry, Date}; // 按字符串长度排序大小写不敏感查找 std::ranges::sort(words, {}, std::string::size); // 范围 比较器 投影按长度排序 auto it std::ranges::find(words, cherry, [](char a, char b) { return std::tolower(a) std::tolower(b); }, [](const std::string s) { return s; } // 投影identity ); // 等价于先对每个元素调用 projection再传给比较器投影的底层实现// 标准库的 invoke 投影机制模拟实现 templatetypename Pred, typename Proj struct projected_predicate { Pred pred; Proj proj; templatetypename T constexpr bool operator()(T val) const { return std::invoke(pred, std::invoke(proj, std::forwardT(val))); } }; // 每次比较前先调用 proj 提取关键字段再交给 pred 比较3. 视图适配器的实现原理视图适配器View Adaptor是 Ranges 的核心组件。以 filter_view 为例逐层剖析其实现。3.1 filter_view 的完整骨架// 标准库 filter_view 的简化实现GCC libstdc 风格 templatestd::input_range V, std::indirect_unary_predicateiterator_tV Pred requires viewV class filter_view : public std::ranges::view_interfacefilter_viewV, Pred { private: V base_; // 底层视图 Pred pred_; // 过滤谓词使用 [[no_unique_address]] 优化空谓词 public: // 迭代器类 —— 核心复杂度所在 class iterator { private: iterator_tV current_; // 当前位置 iterator_tV end_; // 底层范围的结束 const filter_view* parent_; // 反向指针访问谓词 // 关键方法跳过不满足谓词的元素 void satisfy() { while (current_ ! end_ !std::invoke(parent_-pred_, *current_)) current_; } public: using iterator_category std::bidirectional_iterator_tag; using value_type std::iter_value_titerator_tV; using difference_type std::iter_difference_titerator_tV; iterator() default; iterator(filter_view parent, iterator_tV current) : current_(std::move(current)) , end_(std::ranges::end(parent.base_)) , parent_(parent) { satisfy(); // 构造时立即跳到第一个满足条件的元素 } decltype(auto) operator*() const { return *current_; } iterator operator() { current_; satisfy(); // 每次前进后重新定位 return *this; } iterator operator(int) { auto tmp *this; *this; return tmp; } // 双向范围支持 -- iterator operator--() { do { --current_; } while (!std::invoke(parent_-pred_, *current_)); return *this; } bool operator(const iterator other) const { return current_ other.current_; } // 关键和底层范围的 sentinel 比较 bool operator(std::default_sentinel_t) const { return current_ end_; } friend class filter_view; }; // begin() —— 需要找到第一个满足条件的元素 constexpr auto begin() { return iterator{*this, std::ranges::begin(base_)}; } // end() —— 返回哨兵 constexpr auto end() { return std::default_sentinel; } };3.2 satisfy() 的 O(N) 隐患与缓存策略filter_view::iterator 的 satisfy() 在每次 时线性扫描导致以下场景出现性能下降std::vectorint v(1000000); std::iota(v.begin(), v.end(), 1); auto even v | std::views::filter([](int x) { return x % 2 0; }); // 每次 都要扫描到下一个偶数 // 遍历 50 万个元素时satisfy() 累计比较约 100 万次每个元素判断两次 // 但因为每次 只需要前进 1~2 步找到下一个偶数实际开销仍然可控优化策略对稠密谓词大部分元素满足filter_view 开销近似 O(1)对稀疏谓词极少数元素满足 的摊还成本升高。如果大量随机访问优先使用 views::take_while 索引访问替代 filter_view。3.3 transform_view 的零开销抽象templatestd::input_range V, std::copy_constructible F requires viewV class transform_view : public view_interfacetransform_viewV, F { private: V base_; [[no_unique_address]] F fun_; // 空 lambda 不占空间 class iterator { iterator_tV current_; const transform_view* parent_; public: // transform 的核心operator* 时才调用变换函数 decltype(auto) operator*() const { return std::invoke(parent_-fun_, *current_); } iterator operator() { current_; return *this; // 注意没有 satisfy } // ... }; };特性filter_viewtransform_view 开销O(到下一个满足条件的距离)O(1)* 开销O(1)O(F 的调用开销)随机访问不支持条件支持若 V 支持已知大小不支持 sized_range支持继承 V 的大小适合场景筛选子集值映射3.4 take_view 与 drop_view窗口操作// take_view取前 N 个元素 auto first_five std::views::iota(0) | std::views::take(5); // iota(0) 是无限范围但 take(5) 限制了迭代次数 // 内部用计数器实现每 计数器 1达到 N 次后 sentinel // drop_view跳过前 N 个元素 // begin() 直接调用 ranges::next(base_.begin(), count_) auto skip_five v | std::views::drop(5); // 注意drop_view 的 begin() 是 O(N)关键实现细节——drop_view::begin() 的缓存templatetypename V class drop_view { V base_; std::iter_difference_titerator_tV count_; // 缓存 begin 以避免重复 O(N) 计算 mutable std::optionaliterator_tV cached_begin_; constexpr auto begin() { if (!cached_begin_) { cached_begin_ std::ranges::next(std::ranges::begin(base_), count_); } return *cached_begin_; } };4. 管道运算符 | 的魔法管道运算符是 Ranges 最具表现力的语法特性。其实现依赖两个关键机制范围适配器闭包对象和管道运算符重载。4.1 范围适配器闭包对象Range Adaptor Closure Object// 概念定义C23 正式引入C20 中已有隐式使用 templatetypename T concept range_adaptor_closure std::copy_constructibleT requires(T t, auto r) { { t(r) } - std::ranges::range; // 可调用接受 range 返回 range }; // views::filter 返回一个闭包对象不是 view auto even_filter std::views::filter([](int x) { return x % 2 0; }); // even_filter 的类型是 __filter_closure持有谓词等待一个 range 参数 // 以下两种写法完全等价 auto r1 even_filter(v); // 函数调用 auto r2 v | even_filter; // 管道语法4.2 管道运算符的实现// 标准库中管道运算符的简化实现 templatestd::ranges::range R, std::copy_constructible Adaptor requires std::invocableAdaptor, R constexpr auto operator|(R r, Adaptor adaptor) { return std::invoke(std::forwardAdaptor(adaptor), std::forwardR(r)); } // 关键operator| 仅仅是把右边的东西以左边为参数调用4.3 闭包组合views::filter(odd) | views::transform(square) 如何工作// 闭包对象之间也可以管道组合 auto composed std::views::filter([](int x) { return x % 2 ! 0; }) | std::views::transform([](int x) { return x * x; }); // composed 本身也是一个闭包对象 // 这是如何实现的—— 闭包之间管道运算符的另一个重载 templatestd::copy_constructible A, std::copy_constructible B requires std::invocableA, std::ranges::range auto std::invocableB, std::invoke_result_tA, std::ranges::range auto constexpr auto operator|(A a, B b) { // 返回一个新的闭包对象等价于 b(a(x)) return compose_closure(std::forwardA(a), std::forwardB(b)); } // compose_closure 的实现简化版 templatetypename A, typename B struct compose_closure { A a; B b; templatestd::ranges::range R constexpr auto operator()(R r) const { return std::invoke(b, std::invoke(a, std::forwardR(r))); } };4.4 管道链求值流程图注意整个管道链在编译期构建完成没有任何运行时开销。类型虽然复杂但用 auto 接收完全无感。5. 延迟求值惰性链的构建与执行5.1 惰性求值 vs 及早求值std::vectorint v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 及早求值Eager—— 创建 actions 的每个步骤都立即执行 namespace views std::views; auto eager_count std::ranges::count_if( v | views::filter([](int x) { return x % 2 0; }) | views::transform([](int x) { return x * x; }) | views::take(3), [](int x) { return x 10; } ); // 求值时机count_if 需要遍历时才触发整个管道链 // 惰性求值的核心filter / transform / take 都不产生新容器 // 只有最终的消费操作count_if / for_each / copy才驱动遍历5.2 遍历过程的微观视角// 考虑偶数 → 平方 → 取前3个 // v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // 遍历过程以 count_if 驱动为例 /* 迭代 1: filter → 跳过 1不满足偶数 迭代 2: filter → 停在 2满足偶数 transform* → 2*2 4 take → 计数1未达上限输出 4 检查 4 10? → false 迭代 3: filter → 跳过 3 迭代 4: filter → 停在 4 transform* → 16 take → 计数2输出 16 检查 16 10? → truecount 迭代 5: filter → 跳过 5 迭代 6: filter → 停在 6 transform* → 36 take → 计数3达到上限输出 36 检查 36 10? → truecount take 计数达到 3停止遍历。 最终 count_if 结果: 2 总计 filter 比较: 6 次检查了 1,2,3,4,5,6 是否偶数 transform 调用: 3 次只处理了 2,4,6 take 计数: 3 次 最终谓词: 3 次 → 没有创建任何临时 vector */5.3 性能对比惰性链 vs 传统写法#include iostream #include vector #include algorithm #include ranges #include chrono void benchmark() { constexpr size_t N 10000000; std::vectorint v(N); std::iota(v.begin(), v.end(), 1); // --- 传统写法三次中间分配 --- auto t1 std::chrono::high_resolution_clock::now(); std::vectorint temp1, temp2, result; std::copy_if(v.begin(), v.end(), std::back_inserter(temp1), [](int x) { return x % 3 0; }); // ~333万个元素 std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2), [](int x) { return x * 2; }); std::copy_n(temp2.begin(), 1000, std::back_inserter(result)); auto t2 std::chrono::high_resolution_clock::now(); // --- Ranges 写法零中间分配 --- auto t3 std::chrono::high_resolution_clock::now(); auto rng_result v | std::views::filter([](int x) { return x % 3 0; }) | std::views::transform([](int x) { return x * 2; }) | std::views::take(1000); std::vectorint result2(rng_result.begin(), rng_result.end()); auto t4 std::chrono::high_resolution_clock::now(); auto traditional_ms std::chrono::duration_caststd::chrono::milliseconds(t2 - t1).count(); auto ranges_ms std::chrono::duration_caststd::chrono::milliseconds(t4 - t3).count(); std::cout 传统写法: traditional_ms ms\n; std::cout Ranges: ranges_ms ms\n; std::cout 加速比: static_castdouble(traditional_ms) / ranges_ms x\n; } // 典型输出MSVC 2022, /O2: // 传统写法: 87ms // Ranges: 3ms // 加速比: 29.0x // 原因Ranges 只遍历了约3000个元素找到1000个3的倍数只需3000次迭代 // 而传统写法要先遍历全部1000万找到所有~333万个再transform它们指标传统 STLRanges内存分配次数3 次temp1/temp2/result1 次result2遍历的源元素数1000 万 333 万 1000~3000惰性终止中间数据量~333 万个 int~13 MB0总耗时近似~87ms~3ms6. 自定义 View 实战6.1 实现 stride_view每隔 N 个取一个元素#include ranges #include iterator templatestd::ranges::input_range V requires std::ranges::viewV class stride_view : public std::ranges::view_interfacestride_viewV { public: stride_view() default; stride_view(V base, std::iter_difference_tstd::ranges::iterator_tV step) : base_(std::move(base)), step_(step) { if (step_ 0) throw std::invalid_argument(step must be positive); } class iterator { public: using iterator_category std::input_iterator_tag; using value_type std::ranges::range_value_tV; using difference_type std::iter_difference_tstd::ranges::iterator_tV; iterator() default; iterator(std::ranges::iterator_tV current, std::ranges::sentinel_tV end, difference_type step) : current_(std::move(current)), end_(std::move(end)), step_(step) {} decltype(auto) operator*() const { return *current_; } iterator operator() { // 前进 step_ 步但不能越过 end_ for (difference_type i 0; i step_ current_ ! end_; i) current_; return *this; } iterator operator(int) { auto tmp *this; *this; return tmp; } bool operator(std::default_sentinel_t) const { return current_ end_; } private: std::ranges::iterator_tV current_; std::ranges::sentinel_tV end_; difference_type step_{1}; }; auto begin() { return iterator(std::ranges::begin(base_), std::ranges::end(base_), step_); } auto end() { return std::default_sentinel; } private: V base_; std::iter_difference_tstd::ranges::iterator_tV step_{1}; }; // Range Adaptor 闭包对象配合管道运算符 struct stride_fn { std::iter_difference_tint step; templatestd::ranges::input_range R constexpr auto operator()(R r) const { return stride_viewstd::views::all_tR( std::views::all(std::forwardR(r)), step); } // 支持闭包组合 templatestd::ranges::range R friend constexpr auto operator|(R r, stride_fn s) { return s(std::forwardR(r)); } }; constexpr auto stride(std::iter_difference_tint step) { return stride_fn{step}; } // 使用示例 int main() { std::vectorint v {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 每隔 3 个取一个0, 3, 6, 9 auto every_third v | stride(3); for (int x : every_third) { std::cout x ; // 输出: 0 3 6 9 } // 管道组合 auto result v | std::views::filter([](int x) { return x % 2 0; }) // 0, 2, 4, 6, 8, 10 | stride(2) // 0, 4, 8 | std::views::transform([](int x) { return x * x; }); // 0, 16, 64 }6.2 实现 sliding_view滑动窗口templatestd::ranges::forward_range V requires std::ranges::viewV class sliding_view : public std::ranges::view_interfacesliding_viewV { public: sliding_view() default; sliding_view(V base, size_t window_size) : base_(std::move(base)), window_size_(window_size) {} class iterator { public: using iterator_category std::input_iterator_tag; using value_type std::vectorstd::ranges::range_value_tV; using difference_type std::ptrdiff_t; iterator() default; iterator(std::ranges::iterator_tV start, std::ranges::sentinel_tV end, size_t window_size) : start_(std::move(start)), end_(std::move(end)), window_size_(window_size) { update_window(); } value_type operator*() const { return window_; } iterator operator() { start_; update_window(); return *this; } iterator operator(int) { auto tmp *this; *this; return tmp; } bool operator(std::default_sentinel_t) const { return window_.size() window_size_; } private: void update_window() { window_.clear(); auto it start_; for (size_t i 0; i window_size_ it ! end_; i, it) window_.push_back(*it); } std::ranges::iterator_tV start_; std::ranges::sentinel_tV end_; size_t window_size_; value_type window_; }; auto begin() { return iterator(std::ranges::begin(base_), std::ranges::end(base_), window_size_); } auto end() { return std::default_sentinel; } private: V base_; size_t window_size_{1}; }; // 使用示例计算移动平均 // auto moving_avg data | sliding(5) | transform(avg);7. 性能考量与优化策略7.1 编译时间影响Ranges 的模板层次深、类型名长对编译时间有显著影响。// 类型长度对比 // filter_viewtransform_viewfilter_viewvectorint, ..., ..., ... // 类型名可长达数千字符 // 优化策略 1限制管道深度 // 不建议 auto r v | f1 | f2 | f3 | f4 | f5 | f6 | f7 | f8 | f9 | f10; // 建议拆分为 2-3 段 auto r1 v | f1 | f2 | f3 | f4; auto r2 r1 | f5 | f6 | f7; auto r3 r2 | f8 | f9 | f10; // 优化策略 2对大型项目启用预编译头 // 在 pch.h 中 #include ranges7.2 避免常见陷阱陷阱原因解决方案filter_view 后调用 .size()filter 不支持 sized_range先 std::ranges::distance() 计数对临时容器创建 view悬垂引用临时对象析构后 view 失效避免 auto v get_vec() | views::filter(...)take_view 后随机访问take 后不满足 random_access_range先物化materialize到容器多次遍历 filter_view谓词被多次调用缓存结果到 std::vector管道链中混用 actionsactions 及早求值破坏惰性链将 actions 放在管道链最末端7.3 borrowed_range悬垂引用检测// borrowed_range 概念标记迭代器不依赖范围对象生命周期的范围 templatetypename T concept borrowed_range std::ranges::rangeT (std::is_lvalue_reference_vT || std::ranges::enable_borrowed_rangestd::remove_cvref_tT); // 安全vector 是 lvalue其迭代器有效 std::vectorint v {1, 2, 3}; auto safe std::ranges::subrange(v.begin(), v.end()); // OK // 危险临时对象 auto dangerous std::ranges::subrange( std::vectorint{1, 2, 3}.begin(), // 临时 vector 已析构 std::vectorint{1, 2, 3}.end() ); // 编译能过运行 UB // C23 防护std::ranges::dangling auto view std::vectorint{1, 2, 3} | std::views::filter([](int x) { return x 0; }); // C20: 返回 filter_view持有悬垂引用 // C23: 返回 std::ranges::dangling编译期标记无效8. Ranges 与经典 STL 算法对比速查表8.1 算法对照操作传统 STLC20 Ranges关键差异全范围排序sort(v.begin(), v.end())ranges::sort(v)无需 begin/end按成员排序sort(v.begin(), v.end(), [](auto a, auto b){ return a.age b.age; })ranges::sort(v, {}, Person::age)Projection 简化查找find(v.begin(), v.end(), val)ranges::find(v, val)返回 iterator条件查找find_if(v.begin(), v.end(), pred)ranges::find_if(v, pred)无需 begin/end条件计数count_if(v.begin(), v.end(), pred)ranges::count_if(v, pred)转换拷贝transform(...)views::transform(f)惰性零拷贝筛选拷贝copy_if(...)views::filter(f)惰性零拷贝取前 N 个copy_n(...)views::take(n)惰性跳过前 N 个next(begin, n)views::drop(n)惰性去重unique(...)views::unique (C23)惰性反转遍历reverse_iteratorviews::reverse获取键/值手动遍历views::keys / views::valuesmap 专用枚举手动索引views::enumerate (C23)带索引遍历合并views::concat (C26)惰性级联生成序列手写循环views::iota(a, b)惰性整数序列重复值fill(...)views::repeat(v, n) (C23)惰性8.2 常用 View 速查ViewC 版本功能views::filterC20条件筛选views::transformC20元素变换views::takeC20取前 N 个views::dropC20跳过前 N 个views::reverseC20反向遍历views::iotaC20整数序列views::allC20范围→视图views::take_whileC20条件取前views::drop_whileC20条件跳过views::joinC20展平嵌套views::splitC20分隔符分割views::lazy_splitC20惰性分割views::commonC20统一 begin/end 类型views::elementsC20取 tuple/pair 的第 N 个元素views::keysC20取 map 的键views::valuesC20取 map 的值views::zipC23并行遍历多个范围views::zip_transformC23带变换的 zipviews::enumerateC23带索引遍历views::chunkC23固定大小分块views::slideC23滑动窗口views::strideC23步长跳跃views::cartesian_productC23笛卡尔积views::as_rvalueC23转为右值引用views::repeatC23重复值生成views::concatC26拼接多个范围views::cache_latestC26缓存最近值views::to (C23 ranges 的 std::ranges::to)C23范围→容器9. 总结C20 Ranges 不是对 STL 的简单封装而是对范围抽象的一次范式升级统一的范围表达std::ranges::range 概念替代迭代器对配合 sentinel 实现非对称范围组合优于继承通过管道运算符和闭包组合无需中间容器即可构建复杂数据流惰性求值View 适配器不产生数据副本遍历由最终消费操作驱动投影简化projection 消除了大量手写比较器的需求是 Ranges 最被低估的特性编译期安全borrowed_range 和 dangling 从类型系统层面防止悬垂引用使用建议优先使用 std::ranges:: 命名空间下的算法它们天然支持投影且更安全管道链深度控制在 5 层以内避免编译时间和调试困难多次遍历场景先用 std::ranges::tostd::vector()C23物化避免对临时对象创建 view用 borrowed_range 概念检查安全性配合 C20 Concepts 约束泛型代码让编译器提供更友好的错误信息从 C20 到 C26Ranges 库仍在快速演化views::concat、views::cache_latest 等新工具将持续拓展惰性组合的边界。