news 2026/10/20 8:25:13

std::ranges 深度实践:C++20 视图、算法与性能优化指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
std::ranges 深度实践:C++20 视图、算法与性能优化指南

1. 废话少说:为什么std::ranges值得你花一个下午搞明白

先抛个场景。你写了一段代码,要把一个vector<int>里的偶数挑出来、翻倍、再求和。传统写法长这样:

std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8}; std::vector<int> evens; std::copy_if(v.begin(), v.end(), std::back_inserter(evens), [](int n) { return n % 2 == 0; }); std::vector<int> doubled; std::transform(evens.begin(), evens.end(), std::back_inserter(doubled), [](int n) { return n * 2; }); auto sum = std::accumulate(doubled.begin(), doubled.end(), 0);

够直白,但问题也很明显:中途多了两个临时容器,代码被"迭代器对"(begin()和end())撕成了一截一截,读起来得手动把三段拼进脑子里。而用 C++20 的std::ranges写同一个逻辑:

auto sum = std::ranges::fold_left( v | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * 2; }), 0, std::plus<>());

一行管道把"筛选→变换→折叠"串成了自然语言式的链路,没有中间容器,没有迭代器对。这就是std::ranges系统最直观的价值:让算法更贴近人类表达,同时干掉大量手写迭代器代码。

这篇我想聊的不是照抄文档,而是在实际项目里把std::ranges用起来之后,我理解的这整套系统是怎么设计的、有哪些真正好用的点、哪些性能坑要绕开。适合三类人看:刚学完 C++ 基础、听说过 ranges 但不知道从哪下手的;写了好几年 STL 算法、觉得begin/end写烦了的老手;以及负责团队代码规范、想评估要不要引入 ranges 的技术负责人。


2. 从"迭代器对"到"范围":这套系统到底改了什么

2.1 核心不是管道符,是概念上的"范围"

很多人第一次看到std::ranges就盯着|操作符看,觉得这是"新语法"。其实管道符只是语法糖,真正的核心是把"范围"(Range)提升为一等公民。

传统 STL 算法有个根深蒂固的设计:算法操作的是迭代器对,也就是"从哪开始"和"到哪结束"。这带来了一个长期被吐槽的毛病——算法和容器之间永远隔着一层。你要排序一个vector,写的是sort(v.begin(), v.end()),而不是sort(v)。

该怎么解释这个转变呢。可以这样想:传统算法像拆快递,你递过去的是"绳子这头"和"绳子那头",距离多少、中间有没有缠着胶带,算法一概不管;而 ranges 改成了你把"一整团绳子"递过去,它自己知道从哪头开始、到哪头结束、中间经过什么结构。

这个抽象可不是仅仅是少敲几个字母。它意味着:

  • 算法可以直接吃容器:ranges::sort(v)而不是sort(v.begin(), v.end())。
  • 算法可以吃"视图"(view):filter、transform运算得到的东西也是一个"范围",可以继续传给别的算法。
  • 算法可以吃"哨兵"(sentinel):范围的结尾不必非得是"指向最后一个元素之后的迭代器",可以是任何能判断"是否到达末尾"的谓词对象。

这第三点在日常代码里最不起眼,但其实是整套系统里最深刻的设计之一。以前你处理"以 '\0' 结尾的字符串"或者"读文件直到某个标记",得先把它们转成有end()的容器,或者硬写循环判断。现在你只要定义恰当的类型(迭代器 + 哨兵),算法直接就能在"上限未定"的范围上工作。后面实战部分我会给个例子。

2.2 三个支柱:视图、算法、适配器的分工

std::ranges不是一个大一统库,而是三块咬合在一起的部分:

组件作用常见例子
std::ranges::xxx算法操作一个范围对象的算法sort、transform、filter、find_if、fold_left
std::views::xxx视图适配器把范围变换成新范围,延迟计算filter、transform、take、drop、reverse、join
范围概念与工具定义"什么样的类型是范围"、提供辅助工具std::ranges::range、std::views::all、std::ranges::size、std::ranges::subrange

三个东西分工非常明确:视图管"怎么变换数据",算法管"怎么处理数据",概念管"什么样的对象能参与这套体系"。三者独立演进又互相咬合,这是整个设计最讲究的地方。

打个比方:视图是"装修方案变换"(瓷砖换成木地板),算法是"验收标准"(检查地面是否平整),概念是"房屋基本条件"(必须有四面墙和屋顶,否则免谈)。

2.3 懒惰求值:为什么view不会复制数据

视图适配器最核心的行为特征,是懒惰求值——普通算法是"你调用它,它立刻把所有元素处理完",而视图是"你调用它,它只是搭好了一条流水线,真正处理要等你去消费元素的那一刻"。

用filter举例:

auto evens = v | std::views::filter([](int n) { return n % 2 == 0; });

执行这一行的时候,filter并没有遍历v,没有把偶数挑出来存进一个新容器,甚至没有调用一次 lambda。它只是创建了一个"视图对象",这个对象内部保存了底层范围v的引用和筛选谓词。等你什么时候for (int n : evens)或把evens传给某个算法时,它才开始一个接一个地检查元素。

这个设计的好处有两个:

  1. 零拷贝、零分配。你永远不需要把中间结果存下来。对于动辄几十万条记录的日志文件、传感器时间序列,这一点直接决定了内存占用是 O(n) 还是 O(1)。

  2. 可以无限拼接。比如| take(5):从无限序列(如输入流、随机数生成器)里取前 5 个,你不需要先生成完整序列。用传统 STL 你根本没法优雅地表示"无限序列",因为迭代器对要求序列必须有尽头;而 ranges 的视图配上哨兵,可以表达"无穷尽的范围"。

我刚开始也被"懒惰"这个特性绕晕过,总觉得怎么它不干活。后来我用一个类比搞明白了:视图是电视遥控器上的直播画面——你按下"频道+1"按钮,屏幕并不会立刻把未来一天的所有节目都缓存下来;它只是把频道指针调了一下,真正播放是每秒都在发生的。视图就是"调整频道指针"那一下,真正"播放"是你遍历元素那一刻。


3. 先看效果再聊原理:一个真实脚本被 ranges 改写的全过程

3.1 改前的经典写法

某次写一个小工具,要从一堆日志文本里筛出所有"ERROR"级别的行,提取错误码(比如[E1001]这种格式),按出现次数从高到低排序,最后输出 Top 3。

先行代码是这样的,很常见:

std::ifstream file("app.log"); std::vector<std::string> errors; std::string line; while (std::getline(file, line)) { if (line.find("ERROR") != std::string::npos) { errors.push_back(line); } } std::vector<std::string> codes; for (const auto& e : errors) { auto pos = e.find("["); if (pos != std::string::npos) { codes.push_back(e.substr(pos + 1, 5)); } } std::map<std::string, int> counter; for (const auto& c : codes) { counter[c]++; } std::vector<std::pair<std::string, int>> sorted(counter.begin(), counter.end()); std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); for (int i = 0; i < 3 && i < sorted.size(); ++i) { std::cout << sorted[i].first << " " << sorted[i].second << "\n"; }

能跑,但这里有多少次中间拷贝?errors拷贝了所有 ERROR 行,codes拷贝了提取出的错误码,sorted又从 map 拷了一份进 vector……这个脚本处理的日志只有几百行,无所谓;但如果日志有几 GB,这种写法会直接让内存爆掉。还有,读代码的人要把五个循环串起来才能知道整体在做什么,心智负担不轻。

3.2 改后的 ranges 版本

用std::ranges重构后长这样:

std::ifstream file("app.log"); std::string line; std::map<std::string, int> counter; for (auto&& input : std::ranges::istream_view<std::string>(file)) { if (auto pos = input.find("ERROR"); pos != std::string::npos) { if (auto bracket = input.find("["); bracket != std::string::npos) { counter[input.substr(bracket + 1, 5)]++; } } } std::vector<std::pair<std::string, int>> sorted(counter.begin(), counter.end()); std::ranges::sort(sorted, std::greater{}, &std::pair<const std::string, int>::second); for (auto&& [code, count] : sorted | std::views::take(3)) { std::cout << code << " " << count << "\n"; }

改动点逐个说明:

  • std::ranges::istream_view<std::string>(file)把文件流包装成一个范围,每迭代一次读入一行字符串。它依然是"按需读取",不会预读整个文件。
  • 两个if嵌套代替了原来的errors和codes两个中间 vector。注意 C++17 引入的if (auto x = ...; 条件)语法搭配使用,作用域紧绑在一起,变量生命周期更精确。
  • 排序直接用std::ranges::sort,不再需要sorted.begin(), sorted.end(),而是整个sorted。第四参数传了一个"投影"(projection)&std::pair<const std::string, int>::second——这又是一个 ranges 带来的好东西,下面单独说。
  • 输出时用sorted | std::views::take(3)优雅地控制只取前 3 个,不需要手写i < 3 && i < sorted.size()这种边界判断。

提示:&std::pair<const std::string, int>::second这样的写法在部分编译器上需要显式指定成员指针类型,dangling 风险很低。如果嫌难看,也可以写个 lambda:[](const auto& p) { return p.second; }。

3.3 投影(Projection):这玩意儿是隐藏的宝贝

上面解了ranges::sort的第四参数:投影。它允许你在不创造临时对的情况下,告诉算法"请按这个属性的值来操作元素"。

以前你要按学生成绩排序,就得写完整比较 lambda:

std::sort(students.begin(), students.end(), [](const auto& a, const auto& b) { return a.score > b.score; });

用 ranges:

std::ranges::sort(students, std::greater{}, &Student::score);

更妙的是投影不光能用于排序,find_if、count、lower_bound等大量 ranges 算法都支持。比如查找第一个成绩及格的学生:

auto it = std::ranges::find_if(students, [](int s) { return s >= 60; }, &Student::score);

这里的 lambda 拿到的直接就是score的引用,而不是整个Student对象。代码意图瞬间清晰:我要找这么一个人,他有一个 score 字段,这个字段满足某种条件。投影把"取字段"这个动作从比较谓词里剥离出来,让每个关注点各管一摊。

3.4 一个小插曲:编译器的版本要求

入坑 ranges 之前先检查编译器。截至 2024 年初,GCC 12+、Clang 16+、MSVC 19.31+ 对 ranges 的支持都比较完整。GCC 10/11 对一些 fancy 的视图(如views::enumerate、views::chunk)要么缺失要么有 bug。我的建议是直接上 GCC 13 或 Clang 17,省得被"明明代码照着 cppreference 抄的但编译不过"气得摔键盘。std::ranges::fold_left是 C++23 才进的,要用的也得注意标准版本;C++20 时期可以用std::accumulate(r.begin(), r.end(), 0)替代,虽然看着不够帅但不影响逻辑。


4. 容易翻车的几个点:views 的生命周期和 const 问题

4.1 悬垂视图(Dangling View):把临时容器丢了就完蛋

这是 ranges 新手最常踩的坑,没有之一。

看这段:

std::ranges::range auto get_even_view(std::vector<int>& v) { return v | std::views::filter([](int n) { return n % 2 == 0; }); } auto main() -> int { auto evens = get_even_view(std::vector<int>{1, 2, 3, 4}); // 灾难:底层临时 vector 已经被销毁,evens 里保存的是悬空引用 }

视图默认是"引用语义"——它保存底层范围的引用或迭代器,自己不拥有数据。第一行那个临时std::vector<int>在表达式结束后就析构了,而evens还傻乎乎地握着一个指向已灭亡容器的迭代器,一行for (int x : evens)就是 undefined behavior,轻则乱值重则段错误。

这是 C++ 里老生常谈的悬垂引用问题在 ranges 里的形态。解决的思路也一致:

  1. 不要让视图比底层数据活得更久。视图的生命周期必须嵌套在容器的生命周期内。
  2. 如果非要返回一个"拥有数据"的视图,别用 filter/transform 这类引用视图,可以先拷成std::vector等具体容器再返回;或者用自己的 RAII 类包一层。

我之前在日志分析工具里就犯过这个错。过滤器返回了一个局部范围上的视图,函数一结束,原先那个"只读打开的文件内容"缓冲区被回收,整个视图变成一堆野指针。从那以后,凡是视图要跨作用域传递,我都额外加注释提醒自己"此处按引用持有底层对象"。

注意:views::all、views::filter这类视图绝不拥有数据;而ranges::to<std::vector>()(C++23)是把范围急切地物化成容器的正确姿势——它真的把元素拷贝进去,不再引用原范围。

4.2 const 正确性:视图和 const 容器一起用,容易让编译器发飙

简单规则:const容器的视图,只能用const谓词来直接作用在元素上,但很多可变的视图(如reverse、transform)对const容器会抛出编译错误或显著限制。

比如:

const std::vector<int> v{1, 2, 3, 4}; auto rev = v | std::views::reverse; // 在部分标准库实现里,这行会编译失败,因为 reverse_view 的 begin() 非常量方法调用受限

原因很微妙:reverse_view::begin()返回其内部迭代器,该迭代器可能保存一个底层迭代器,而底层迭代器在 const 容器里是 const_iterator,正常应该能通过。但早期实现里reverse_view需要持有非 const 的 begin() 来满足某些概念约束,导致 const 范围适配不佳。

解决办法很土:把容器声明为非 const 但语义上不修改,或者在需要跨函数传递时显式转成std::views::all(v)再迭代。实际项目里最省心的是,尽量保持容器非 const 传给视图,只有在明确不会触发视图写语义的地方才放心用 const。

读者可能会好奇为什么标准不把 const 这事儿完全做对。这里是客观现实:有些视图(比如iota_view、istream_view)天生对"是否可变迭代器"敏感,标准里对应的概念迭代器分类又保守,导致你自查起来非常隐蔽。我的策略很简单:交付前用static_assert(std::ranges::range<decltype(myView)>)检查一遍基础性质,再用编译期的概念断言去捕捉 const 相关错误,剩下就交给 lint 工具了。

4.3 迭代器类型太复杂:auto 是你的朋友,但不是全部

视图链一长,begin()的返回类型就变成了一串吓人的模板嵌套。别看不起编译器输出,也别自己去手写那个类型——一律用auto,必要的时候用decltype或者 CTAD 来拿类型。

auto chain = v | std::views::filter(...) | std::views::transform(...) | std::views::take(10); auto it = chain.begin(); // 别试图手写它的类型 auto first = *it;

一旦你需要用视图的迭代器类型去声明其他东西,立即变成一大坨无法维护的std::ranges::iterator_t<...>。万一标准库更新,类型名字一变,你手写的类型就崩了。所以写 ranges 代码的第一纪律:任何迭代器型和视图型,交给类型推导,绝不手写。


5. 从抄到造:自建视图组件,理解 view_interface 基础设施

5.1 什么时候需要自写视图,什么时候别写

std::views提供了官方适配器全家桶,但我遇到固定模式反复出现在十几个文件中时,还是考虑封装成自定义视图。比如"每 N 个元素归一组取平均值"或者"处理一个 CSV 行,把每列解析成结构体"——这些既不是标准库覆盖的行为,又不想在业务代码里到处摆 for 循环。

但先别急着造轮子。自写视图的代价是:你不仅要实现begin()/end(),还要确保符合一堆概念(view、input_range、forward_range……),还要处理值语义、拷贝语义、迭代器的六大 typedef。标准库里std::ranges::view_interface就是给你提供这些基础设施的——只需要你在派生类里实现begin()和end(),它就可以免费提供size()、empty()、operator[]等一大堆便捷成员函数。

5.2 一个最简单的自造视图:每隔一个元素取一个(stride)

下面这个实现适合拿来当模板,它生成一个"每隔 step 个元素才产出一次"的视图:

template <std::ranges::view V> class stride_view : public std::ranges::view_interface<stride_view<V>> { private: V base_; std::ranges::range_difference_t<V> step_; std::ranges::iterator_t<V> current_; public: stride_view() = default; stride_view(V base, std::ranges::range_difference_t<V> step) : base_(std::move(base)), step_(step) { current_ = base_.begin(); } auto begin() { return current_; } auto end() { return base_.end(); } };

光有begin/end还不够,迭代器的自增得实现"跳步"逻辑。我们可以用一个非常简单的方案:创建这个视图时就把迭代器直接推进到合适的起始位置,而begin()返回的迭代器自增时维护一个内部计数器,遇到step-1个元素就跳过、输出第step个。

完整实现代码量不小,不在这里全部展开。我想强调的是:看官方 adapter 的源码(比如drop_view、take_view),是学习自造视图最快的路径。它们在实现上都利用view_interface提供的脚手架,代码量其实很少。自造视图的时候,先把这些条件想清楚:

设计问题自查项
底层是单向范围还是双向范围?这决定迭代器是输入迭代器还是前向迭代器,能支持哪些算法
step 为 0 或负数的行为?必须定义,别让 UB 溜进来
视图的拷贝是否拷贝底层范围?默认是浅拷贝,符合语义即可
end()返回值类型与begin()一致?不一致就需要哨兵支持,边界处理麻烦
是否满足view概念必须是可拷贝构造和析构的,且拷贝是 O(1)

自造视图最大的价值不是代码复用,而是让你更深地理解"视图"这个抽象的本质——它必须轻、必须是引用语义、必须正确建模范围的迭代能力和结束条件。

5.3 性能面:懒惰不等于免费,但通常比临时容器快

很多人看完上面的内容会有一个错觉:既然是懒惰求值,那性能肯定永远是最好的。真相是:懒惰求值跳过的是"分配内存 + 拷贝元素"的开销,但每次迭代元素时仍然可能多做几次函数调用和分支判断。

比如前面的"筛选偶数再翻倍"例子。v | filter(偶数) | transform(翻倍)对于每个元素,至少要经历 filter 谓词的一次判定、transform 谓词的一次调用。如果这两种操作本身非常廉价(就是几次整数运算),那传统写法(两个临时 vector + 三次循环)相比 ranges 管道通常是差不多的开销,因为传统写法也做了同样多次的谓词调用,而且多了分配和拷贝。

真正让 ranges 管道显得慢的场景是:错误地在一个亿级元素的循环内部每轮重复构造同一串视图链。视图对象自身创建虽然便宜,但在循环里反复创建、每次遍历,效率确实比直接在循环体外建好再复用差一点。好在标准实现通常把这些小对象优化得很极致,只要视图不在最内层循环里反复重新求值,实际跑起来很难感觉到与手写循环有数量级差异。

我在生产环境做过一个简单的 benchmark(别太当真,主要看数量级):对一个 1000 万元素整数数组做"过滤偶数 → 翻倍 → 累加",手写循环 vs ranges 管道,结果是手写循环快 20~30%。理论上差的这些点主要来自函数无法内联(当谓词是无捕获 lambda 时现代编译器都能内联,一旦用std::function或虚函数封装就会暴跌)。所以性能建议很直白:

  • 小数据集或中数据集:放心用 ranges,代码可读性收益远大于那百分之几的性能差。
  • 大数据集 + 热循环:先用 ranges 写清楚,再针对性 benchmark,如果确实慢手写循环也不丢人。
  • 谓词必须避免虚函数调用和std::function开销。

6. 我踩过的坑和团队的磨合实录

前面讲了不少"应该怎么做",这里换个角度讲讲实际项目中踩过的几个真坑,以及我们团队从"禁止用 ranges"到"默认用 ranges"的态度转变。

6.1 模块边界处的类型爆炸

有一阵子我对 ranges 上头,写了一个库内部接口,参数形如std::ranges::input_range auto&& r,声明得特别高级。结果其他同事接手时一脸茫然——这是啥?能用 vector 吗?然后每个调用点都开始出现template <typename R>的模板函数,模块间接口从清晰的具名类型变成了一大堆泛型约束。编译错误提示还特别难看,一个概念没满足就刷出几百行"no matching function for call to ... constraints not satisfied"。

这件事给我的教训是:ranges 内部的算法链可以尽情用,但跨模块公开 API 不要轻易给函数套模板。除非你是写库的,否则公共接口老老实实用具体容器类型(vector、span)反而清晰可维护。泛型接口省下的那点代码量,会在可读性和编译速度上原样还回来。

6.2 藏在迭代器失效里的 bug

你以为用了 ranges 就不会有迭代器失效了吗?天真。视图只是把迭代器封装了一层,底层容器照样可能被修改。

最经典的翻车现场:

std::vector<int> v{1, 2, 3, 4, 5}; auto view = v | std::views::filter([](int n) { return n % 2 == 0; }); v.push_back(6); // 向量的 push_back 可能使所有迭代器失效 for (int n : view) { // 未定义行为 std::cout << n << " "; }

filter_view的迭代器内部存着v的迭代器(也可能只存了一个 begin 迭代器),其实它有个内部缓存的 begin 迭代器,push_back之后缓存里那个迭代器可能就指向悬空位置了。虽然标准并没有规定filter_view一定要缓存 begin 迭代器,但多数实现是缓存的,所以这个坑几乎必然触发。

这条让我养成了习惯:在同一个作用域内,视图一旦创建,底层容器就只读不写。往容器加元素、删元素、扩容的操作,一律放到视图作用域之外,或者干脆先完成容器修改再创建视图。这不是 ranges 的设计缺陷,这是所有基于迭代器的库共有的约束——std::sort之后你还不是得小心迭代器失效。

6.3 团队引入 ranges 的节奏建议

我不建议一步到位把所有代码都改成 ranges 风格。那样做的后果往往是:一半人会用,一半人看不懂,code review 变成两个人互相说服对方的辩论赛。更务实的路径是分三步走:

  1. 先让所有人熟悉ranges::sort、ranges::find_if这类"单算法版",把它们当作 STL 算法的提速版。不必引入管道符,就能消灭海量begin()/end()。
  2. 第二步统一引入views::filter和views::transform,同时规定:视图只在函数内部使用,不得跨函数传递,不得引用临时对象。
  3. 第三步再放开自由组合,让大家用管道组装复杂逻辑,但配 code review 检查清单(生命周期、迭代器失效、const 正确性)。

我见过不少团队上来就铺管道,结果 bug 率上升、新人挫败感强。逐步放开反而更稳。另外,强烈建议把编译器和标准库升级到版本列表里较新的那一档,否则你会踩到很多"标准有但这版本没修"的坑。


7. 值得进一步挖的角落:哨兵、子范围、生成器与 C++23/26 的延续

7.1 哨兵(Sentinel):范围的另一种结束方式

前面提到过,ranges 允许结束标志不是"迭代器"而是"哨兵"。它的威力在实际代码里体现在处理"读输入直到某标记"的场景。

比如处理一个以'-'结尾的字符串数组。传统写法你得先数有多少个元素,或者干脆规定end()是数组末尾。用哨兵,你可以定义一个谓词"是否是终止标志",然后把数组包装成范围:

struct not_dash_sentinel { const char* current; friend constexpr bool operator==(const char* pos, not_dash_sentinel s) { return *pos == '-'; // 到达 '-' 即视为结束 } }; const char content[] = "abc-def"; std::string result(std::ranges::begin(content), not_dash_sentinel{content}); // result == "abc"

这个例子有点简陋,但思路很清楚:定义"范围结束"的标准可以是任何条件。这让 ranges 能统一处理"有限但长度未知"的数据源。配合ranges::subrange使用,可以在保留某段迭代器区间的同时不丢失边界信息,比裸存两个迭代器安全多了。

7.2 生成器视图:views::iota和自定义生成器

std::views::iota(1, 100)生成一个从 1 到 99 的整数序列视图。它没有底层容器,元素是"按需生成"的。更妙的是它没有上界版本:

for (int i : std::views::iota(1) | std::views::take(10)) { std::cout << i << " "; // 1 2 3 4 5 6 7 8 9 10 }

这种"生成器 + 适配器"的组合是函数式风格的基石。让它变成生产力的真正技巧是:把随机数引擎、时间戳、甚至命令行参数序列包装成生成器。

比如生成递增时间戳:

auto current = std::chrono::system_clock::now(); auto timestamps = std::views::iota(0) | std::views::transform( [current](int offset) { return current + std::chrono::seconds(offset); });

然后你可以for (auto t : timestamps | take(10))。这在写仿真、测试数据生成时非常顺手。

7.3 C++23 和 C++26 的延续:单子操作、to<vector>、格式化输出

C++23 为 ranges 补完了一部分旧标准缺失的适配器和工具:

  • std::ranges::to<std::vector>():把视图急切地物化成容器,而且是真正的"拥有数据"。这几年被无数人吐槽"视图不能转容器"的痛点终于解决。用法:auto vec = v | views::filter(...) | std::ranges::to<std::vector>();
  • std::views::enumerate:给元素配上索引输出。以前你得用一个计数器变量在循环里手动++,现在视图自带。
  • std::views::zip:把两个范围打包成"元素对"范围,处理两个平行序列再也不用手写索引对齐。
  • std::views::chunk:按固定大小分块处理序列,一口气处理"每 4 个一组求和"这种逻辑。
  • 单子操作(monadic operations):views::transform配合std::optional可以写类似链式调用的求值流,C++23 逐渐引入transform与and_then的视图封装,代码风格越来越接近函数式语言。

C++26 目前的草案还包含更复杂的视图,以及改进的 format 支持。我个人的感觉是:ranges 的演进方向正在把 C++ 从"必须先构造完整数据结构才能处理"的老套路中彻底解放出来,数据流式的处理方式逐渐成为主流。


8. 给新手的一句实在话

看到这里,可能信息量有点大。我建议直接照着下面的路线动手:

  1. 用std::ranges::sort替换三个日常排序。
  2. 用std::views::transform重写一个"从容器生成新容器"的循环。
  3. 找一个filter加transform加take组合的练习题做掉。
  4. 试着给自己的某个范围写一个最简单的自定义视图。

代码编辑器里敲一遍比读十篇博客都管用。真碰到编译报错别急着放弃,概念约束的报错通常背后都有一个具体的语义问题,仔细看最前面几行错误信息往往就能定位(比如视图生命周期问题、迭代器类型不匹配)。我现在的经验是,ranges 的报错看着吓人,但九成情况下错误信息的第一行就指明了问题所在,别被后面几百行模板垃圾吓退。

这套系统确实有一定的学习曲线,但它换来的是代码表达力的大幅提升,以及整个团队在"处理数据流"这一层抽象上的共同语言。如果你在写需要经常处理集合、序列、数据管道的 C++ 代码,那么花一天时间入坑 ranges,长期来看肯定不亏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:24:39

65W PD快充方案:集成反激控制器与协议栈的单芯片设计解析

一直在做电源适配器和快充方案的人&#xff0c;应该对“协议芯片加反激控制器”这种老配方再熟悉不过了。做一颗65W的C口快充&#xff0c;尤其是带PPS的PD充电器&#xff0c;过去得在板上放一颗高压反激控制器、一颗同步整流控制、一颗独立的PD协议握手芯片&#xff0c;运气不好…

作者头像 李华
网站建设 2026/10/20 8:24:39

Spring Boot多数据源密码安全:dynamic-datasource加密与Vault集成实践

前几年我负责一个订单中台的重构&#xff0c;代码评审时发现一位同事把测试库密码直接写在 application-dev.yml 里&#xff0c;又因为某次误操作把整个仓库推到了公开源上。虽然最后密码改了、仓库也删了&#xff0c;但那天晚上我反复在想&#xff1a;如果当时已经有人扒走了这…

作者头像 李华
网站建设 2026/10/20 8:24:38

任意配对训练:打破严格配对限制的高性能图像融合新范式

做图像融合这一行的&#xff0c;大概都被同一个问题卡过喉咙&#xff1a;训练数据必须严格配对。红外图和可见光图得是同一时刻、同一机位、同一个静态场景&#xff0c;多曝光得固定相机连拍&#xff0c;多聚焦得同一物体不同景深逐张采集&#xff0c;最后还得逐像素对齐。采集…

作者头像 李华
网站建设 2026/10/20 8:24:27

LRU缓存算法:双向链表与哈希表实现O(1)淘汰

链表专题一路写到第九篇&#xff0c;前面聊反转、聊环检测、聊合并&#xff0c;那些题目更像是练手感和培养指针直觉的"基础课"。但链表真正的工业级名场面&#xff0c;还得看 LRU 缓存。LRU 全称 Least Recently Used&#xff0c;中文叫最近最少使用&#xff0c;是缓…

作者头像 李华
网站建设 2026/10/20 8:23:34

Missile DATCOM气动估算方法详解:从部件叠加到工程实践

简介&#xff1a;missile datcom 是面向导弹设计与气动分析人员的图形化气动计算工具&#xff0c;基于美国空军 DATCOM 工程估算程序封装而成。它可在输入弹体长度、直径、翼展、马赫数、攻角等条件后&#xff0c;快速输出升力系数、阻力系数、侧向力系数及滚转力矩系数&#x…

作者头像 李华