恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

C++20 std::ranges:用视图重构容器操作

  • 首页
  • 资讯中心
  • /
  • C++20 std::ranges:用视图重构容器操作

相关资讯

airllm:大模型轻量推理调度层,小显存设备高效加载方案 2026/10/18 6:56:33
Redis命令:LPOP 2026/10/18 6:56:33
PyTorch实现Deep Dream:CNN可视化与梯度上升的完整指南 2026/10/18 6:51:33

最新资讯

基于Spring Boot+Vue的社区团购系统设计与毕业设计实践
如何快速上手stable_diffusion_2.1_unclip_repackaged:3步把ComfyUI checkpoint模型装好的新手教程
毕业论文写作系统设计:项目管理、格式引擎与查重预检实践
8个AI论文工具盘点:自考生毕业论文从选题到答辩指南
美妆零售系统怎么算回报?从成本到业绩提升的三维落地指南
高校教师在线学习平台设计与开发:Vue3+Node.js+Element Plus实战

今日推荐

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

C++20 std::ranges:用视图重构容器操作

发布时间:2026/10/18 6:56:33
C++20 std::ranges:用视图重构容器操作 看到“std::ranges”这个标题我第一反应是终于有人愿意聊这个了。作为C20里最“颠覆认知”的库之一ranges绝不是语法糖那么简单。它改写了我们操作数据集合的方式让代码从“告诉机器怎么循环”变成“声明我要什么结果”。这篇文章不讲空洞的大道理直接讲清楚它到底能在哪些场景里帮你扛事又会在哪些场景里埋坑。这适合正在使用C17/20、被传统迭代器算法写法折磨过、想在不重写底层代码的前提下提升代码表达力和健壮性的开发者。如果你还在纠结“这玩意儿学了能用到生产环境吗”看完这篇你心里会有答案。1. 内容整体设计与思路拆解1.1 为什么是ranges它到底改变了什么传统的STL算法长这样std::sort(vec.begin(), vec.end())。你得把一段数据拆成“开头”和“结尾”两个迭代器别人一眼看过去先看到的不是逻辑而是迭代器本身的存在感。更进一步你想对一个vector先过滤、再变换传统写法是多个循环嵌套或者多次中间容器的拷贝。每一次push_back都是一次潜在的性能损耗和代码噪音。ranges从名字上就把“范围”当成一个整体概念它把begin()/end()这对迭代器打包成“一个东西”传进算法里同时引入了“视图”和“管道组合”。这就意味着你想表达“先过滤奇数再每个数乘以2最后取前5个”这种逻辑可以像流水线一样从左到右写出来而中间不产生任何临时容器。用生活类比说清楚这件事传统迭代器写法就像你拿着清单跑五趟仓库每跑一趟取一种零件再自己找个地方堆着ranges的视图写法就像在仓库出口装了一条传送带机器零件依次经过筛选、加工、计数你站在终点直接收货。1.2 什么样的人最应该学它如果你是做数据分析管道、配置解析、日志清洗、算法原型验证这类“大批量容器操作”的方向ranges的收益是立竿见影的。如果你写的是嵌入式底层或者高性能计算看性能章节你会更清楚哪些地方能用、哪些地方别硬上。再说直接一点ranges的适用场景本质上就是“容器操作占比高”的代码。用C写后台服务、写量化策略、写图像处理、写编译器的中间表示变换这些领域全是迭代器和循环的天下所以它们也都是ranges的主场。有位开发者的比喻我印象很深传统的for循环像手工拧螺丝ranges像电动螺丝刀。它不会让螺丝变得更高级但同样的工作量耗费的心力和时间完全不是一个量级。2. 核心细节解析与实操要点2.1 你能用到的ranges组件全梳理std::ranges包含三大块range概念、视图、算法。我按实际使用频率帮你排个序列出最常用的几个。视图是ranges的骨架。std::views::filter按条件筛选元素std::views::transform做元素映射std::views::take取前N个std::views::drop跳过前N个std::views::reverse反向迭代std::views::iota生成连续整数序列。这些视图都是惰性求值的当你写下一条流水线时数据并没有真正被处理只有你最终遍历或者传给一个急切算法时计算才真正发生。与视图配套的是ranges算法。传统算法被重写成了接受“一个范围”的版本比如std::ranges::sort(vec)不再需要begin/end。另一边是to系列操作比如std::ranges::tostd::vector()可以把任意视图物化成真容器。这个组合拳打下来你会发现平时写的数据处理代码至少能压缩掉四成量而且逻辑不是被压缩了是本来就不该有那么多中间变量。2.2 视图的惰性与急切算你需要分清这是ranges里最容易被新手忽略的“暗坑”。视图不会立即执行任何操作它只记录操作的“配方”。只有当你把它扔给需要统一处理数据的算法比如std::ranges::accumulate或者遍历它的时候计算才会触发。这个特性带来两个直接好处第一是你可以在不消耗内存的前提下把流水线搭得特别长十几层变换之间不存在任何中间数组第二是你甚至可以把视图保存在变量里当成一个“待执行的数据处理方案”传来传去。但它的副作用也很明显如果你在视图上做了修改操作后再取引用的值或者把视图传给一个立即执行的函数又立刻离开作用域就容易踩到悬空引用的坑。这个后面在常见问题里细讲。这里先说一条黄金法则视图是“看数据的镜子”不是“存放数据的水缸”。数据活得比视图久视图才安全。2.3 谓词与投影让代码更像业务描述ranges算法里有个特别贴心的设计叫“投影”。以往你要按某个字段排序得写一个比较lambda比较里还得手动取字段。ranges的std::ranges::sort(vec, {}, Person::age)直接在最后一个参数传成员指针它会自动先做“投影”也就是先把元素映射到我们要比较的字段上然后比较字段。同理ranges::count_if、ranges::find_if、ranges::max_element统统支持这个投影参数。这样一来很多表达式会让阅读者几乎感觉不到“迭代”和“比较”的底层细节满眼都是业务按年龄排序、找成绩最高的学生、统计在职员工数。投影为什么值得用因为它把“怎么从完整对象里取得你想要的部分”这件事从lambda里抽离出来变成一个统一、复用、可组合的概念。老实说C20里最能减少噪音的设计我认为投影算头一个。3. 实操过程与核心环节实现这一节我们把袖子撸起来。我会设计一个完整的模拟业务场景把它从“传统循环版”一直改写到“ranges流水线版”全程展示代码和背后的思考过程。模拟场景如下有一批订单记录每个订单包含用户ID、商品名、价格、状态。你需要完成以下操作筛选出状态为“已支付”的订单过滤掉价格低于100元的订单按价格降序排序只保留每个用户最贵的那一单按用户ID去重输出前10个订单的商品名和价格3.1 数据定义与旧写法回顾先定义订单结构体和模拟数据#include vector #include string #include algorithm struct Order { int user_id; std::string product; double price; std::string status; }; std::vectorOrder orders { {1, keyboard, 299.0, paid}, {2, mouse, 89.0, paid}, {1, monitor, 1299.0, paid}, {3, usb-c hub, 159.0, pending}, {2, desk lamp, 199.0, paid}, {1, webcam, 450.0, paid}, {4, chair, 899.0, paid}, {3, headset, 239.0, paid}, {4, microphone, 349.0, paid}, {2, speakers, 549.0, paid}, // ... 假设还有大量数据 };传统写法会是让人皱眉头的一串循环std::vectorOrder paid_orders; for (const auto o : orders) { if (o.status paid o.price 100.0) { paid_orders.push_back(o); } } std::sort(paid_orders.begin(), paid_orders.end(), [](const Order a, const Order b) { return a.price b.price; }); std::vectorOrder deduped; for (const auto o : paid_orders) { bool seen false; for (const auto d : deduped) { if (d.user_id o.user_id) { seen true; break; } } if (!seen) deduped.push_back(o); } const int limit std::min(10, (int)deduped.size()); for (int i 0; i limit; i) { std::cout deduped[i].product : deduped[i].price \n; }看到了吧主逻辑被淹没在“怎么迭代”“怎么查重”这些细节里。用户ID那层查重还得自己开双重循环既不优雅又容易出错要是数据量大这种双重循环就是灾难。3.2 第一次重构ranges算法直接替换第一步是用std::ranges版本的算法和视图把“过滤、排序”这部分换掉#include ranges #include vector #include iostream namespace views std::views; namespace ranges std::ranges; auto paid_orders orders | views::filter([](const Order o) { return o.status paid o.price 100.0; }) | ranges::tostd::vector(); ranges::sort(paid_orders, std::greater{}, Order::price);注意views::filter接收一个谓词只保留满足条件的元素。ranges::tostd::vector把视图“物化”成真正的std::vectorOrder这是C23才进标准的工具如果你还在用C20需要手动写std::vectorOrder(view.begin(), view.end())或者干脆直接用一个vector初始化表达式。排序那里第三个参数Order::price就是2.3里讲的投影。std::greater{}是预定义的比较器它作用在投影后的结果上也就是说比较的是o.price而不是整个Order。这个写法从“代码意图”上看比原来的lambda更直接更加不会出错。3.3 完整流水线视图组合与去重去重那块用视图组合可以写成一行auto result orders | views::filter([](const Order o) { return o.status paid o.price 100.0; }) | ranges::views::transform([](const Order o) { return o; }) | ranges::views::unique;等等unique去重只能去掉连续的重复元素。要想按用户去重且保留价格最高的必须先保证同一个用户的订单在序列里是连续的所以需要先排序。这就是整个流水线的关键顺序过滤 → 排序 → 去重 → 截取。正确的完整版本长这样std::vectorOrder filtered orders | views::filter([](const Order o) { return o.status paid o.price 100.0; }) | ranges::tostd::vector(); ranges::sort(filtered, std::greater{}, Order::price); auto one_per_user filtered | views::unique(std::ranges::equal_to{}, Order::user_id); auto top one_per_user | views::take(10); for (const auto o : top) { std::cout o.product : o.price \n; }注意views::unique的用法它比传统unique更聪明第二个参数传一个二元谓词默认是equal_to第三个参数传投影即按user_id判断是否重复。由于前面已经按价格降序排好了所以当unique遇到重复用户时第一个遇到的就是这个用户最贵的一单后面的重复项被丢弃。这四步加在一起没有用户自定义的去重函数、没有中间变量保存“已见到过的用户”、没有任何双重循环。整个操作“过滤→排序→去重→截取”是数据管线的思路你从左往右读代码读到的正是业务处理步骤。3.4 惰性生成序列iota与transform的妙用还有一个场景我特别想讲因为它在“批量造测试数据”“生成索引序列”时特别实用。比如你要从1到100生成一个包含平方数的序列auto squares views::iota(1, 100) | views::transform([](int x) { return x * x; });iota生成[1, 100)的整数序列整个过程是惰性的你想要多大都行不需要一次性把数据放进容器。再配合views::drop和views::take你可以在不接触原始数据的前提下“切”出任何一个子范围。有人会问这和直接写for循环生成vector有什么区别区别在于iota生成的是“概念上的序列”你可以把它和其他视图任意组合比如挑出平方数里大于50的偶数auto filtered_squares views::iota(1, 100) | views::transform([](int x) { return x * x; }) | views::filter([](int x) { return x 50 x % 2 0; });这在需要生成多种筛选条件的算法实验里特别顺手改条件就像改参数不用反复改循环体和容器累加逻辑。3.5 从C17迁移需要知道的适配写法很多项目还停留在C17或者公司编译器还没完全启用C20/23。这里给你迁移期的实操建议C17环境可以引入range-v3库作为过渡它提供了与std::ranges几乎一致的API命名空间也类似。迁移到C20时只需要把ranges::命名空间从range-v3::切换成std::views或std::ranges大部分代码是原封不动的。另外C20里ranges::to还没有标准实现。你需要自己写一个简单的物化辅助函数或者用std::vectorT(begin, end)的构造方式。这种差别不大只是多几行。我用一个本地包装函数解决了这个问题template typename R, typename T std::ranges::range_value_tR auto to_vector(R r) { return std::vectorT(r.begin(), r.end()); }之后的代码里auto v to_vector(some_view);和C23的ranges::tostd::vector()用法上几乎无缝衔接以后版本升级时替换成本极小。4. 常见问题与排查技巧实录4.1 悬空视图数据死了镜子还亮着这是ranges使用中最容易翻车的大坑。当视图引用的是一个临时容器的数据时容器被销毁视图就成了“悬空引用”。看这个反面案例auto get_view() { return std::vectorint{1, 2, 3} | views::transform([](int x) { return x * 2; }); }函数返回后临时vector被释放了transform视图却还“指向”那段内存。访问它未定义行为可能崩、可能打印垃圾值一切皆有可能。这比迭代器失效还要隐蔽因为视图本身还像模像样你根本不知道它已经坏了。排查经验当你的视图被保存超出某个局部函数范围时先反问一句视图的数据源是以值还是以引用形式活着的如果数据源是函数参数传进来的引用且视图生命周期小于等于参数声明周期安全如果数据源是局部对象或临时对象基本就悬了。4.2 视图不是容器别用下标硬摸视图的接口语义和容器不同。标准容器支持随机访问下标但视图是“按需生成的流”除了views::common这类特殊转换外大部分视图不支持operator[]。比如views::filter的结果你没法直接out[3]取第4个元素。正确做法是先用views::drop跳过前面的元素再用views::take取目标元素或者干脆物化成一个vector再索引。这个毛病大部分是因为以前写惯了v[3]思维没转过来。记住一原则视图流水线适合“从头到尾顺着消费”不适合“随机点菜”。4.3 视图和lifetime修饰符不匹配有一次我在循环里反复用一个管道发现自己用了一个捕获了外部引用的lambda于是视图里的谓词生命周期绑定到了循环内结果循环一迭代、引用失效过滤器开始随机抽风。这种问题很难一眼看出因为错误可能表现为“某些元素偶尔丢失”。排查时你可以把管道里的每个视图单独拉出来运行写一个最小用例逐层添加变换直到哪一步数据不对那一层就是嫌疑最大的。另外你如果用一个非const的lambda捕获了一个局部变量的引用在视图对象超活到下一个作用域时这个引用就跟死了一样。避免方式很简单把谓词设计成纯函数、无状态优先或者用[]值捕获那些小对象。4.4 性能开销疑惑视图到底慢不慢这是ranges被问得最多的问题。性能结论比大多数人想象得好视图的惰性特性决定了只有当整个流水线被完整消费时元素才会被逐个处理这通常比“多次循环 中间容器”更快因为少了多次分配和内存拷贝。你唯一要注意的是别把视图往外传了之后在多个线程里各自消费同一个生成器视图——那会产生竞争或者预料外的副作用。另外过度使用lambda嵌套可能导致编译器无法内联但现代编译器在-O2或者默认优化下这些lambda几乎全被内联进流水线。在绝大多数非极端性能要求场景把“可读性”放在第一位性能不会让你失望。4.5 排查质疑为什么编译错误这么难看模板库里编译错误长是常事ranges尤其夸张。一个普通类型不匹配的报错能长达几KB。我的建议不是去精读那些错误而是用“二分法”把流水线上的视图逐个剥离每次编译一次定位错误具体是从哪层开始爆出来的。另外一个技巧是在比较复杂的管道中显式标注中间变量类型。比如先写auto v1 xxx | views::filter(...);再用std::vectorOrder v2 v1 | ranges::tostd::vector();把类型错误卡在两行之间编译器能给你的信息会清晰一个数量级。4.6 实战速查表常见问题一览症状根因处理返回视图后程序崩溃或输出乱码视图引用临时容器悬空引用确保数据源的生命周期覆盖视图使用范围视图上的元素无法随机访问视图是单向流式结构使用views::droptake后物化或先转成vector某些元素在新环境极慢lambda捕获了外部引用内联失效尽量用无状态lambda或值捕获输出结果是从旧容器拷贝来的修改没生效视图只是映射不是容器修改原容器或物化后再修改C20编译环境没有ranges::to标准版本差异用std::vectorT(begin, end)包装5. 与既有代码的整合与渐进式改造5.1 大项目里的改造节奏如果你是维护一个老代码库别指望一次性把所有for循环改成ranges也不要每周更新几百个调用点。我的建议是从“纯函数类型的局部处理逻辑”开始迁。比如配置解析、日志格式化、数据装载清洗模块这类地方不涉及复杂外部状态改造风险低、收益高。重点提醒一个容易踩的编译坑老代码里如果重载了begin/end函数模板或者某个容器类型不完全满足ranges概念会让整个编译直接失败。这种情况下先使用std::ranges::subrange显式包装把“可变容器”变成明确的“子范围”就能跨过概念校验。5.2 保留传统迭代器的兼容层项目中总有某些核心算法接受迭代器对而不是范围。ranges提供std::ranges::begin和std::ranges::end你可以拿到迭代器继续喂给旧算法。等于说视图这层“新表达能力”和旧接口之间没有潜规则成本。这个兼容性设计是ranges真正落地价值的地方你不需要把整个项目的IT架构推翻重建。一个实用模式先用ranges流水线把数据预处理成理想顺序再用ranges::begin/end取出迭代器传入原有接口auto processed orders | views::filter(...) | views::transform(...) | ranges::tostd::vector(); legacy_function(processed.begin(), processed.end());前提是处理结果物化成容器性能上多了一次拷贝但这一般比手动循环干净太多。在“热点路径”里你可以换成直接返回惰性视图再一次性消费性能与可读性双收。5.3 团队规范与可读性平衡ranges代码写爽了容易走火入魔。有一条团队约定我强烈推荐单条流水线的视图层数不超过6个。超过了就像读一篇没有分段的文章眼睛会迷路。当然如果你用中间变量切分流水线并给每一段起个有业务含义的名字到10层也照样清晰。另一条约定是关于“是不是所有地方都要用ranges”。我的态度是遍历容器的目的如果是“作用在每个元素上做修改”传统for循环反而更直白但如果是“从一堆数据中推导出一个结果”ranges更胜一筹。这两种场景的判断你要靠代码阅读者的感受去做而不是编译器。6. 我的实践心得与写在最后的建议回看我自己写过的项目ranges给我带来的最大改变不是在性能上省了几毫秒而是代码被阅读和理解的成本显著降低了。原来一段三十分钟才能看懂的数据清洗逻辑现在五分钟就能被接手的人复述出业务规则。对于团队成长和项目维护这是比任何微优化都有价值的优势。如果你决定在下一个项目里尝试它我惟一的建议是别把它当“炫技”把它当“业务逻辑的翻译器”。你写下的每一个views::filter、views::transform、ranges::sort本质上是在说“我要保留哪些数据”“我要把数据变成什么样子”“我按什么顺序看这些数据”。当代码的语言和业务的语言对齐时你才会真正开始体会到现代C的爽快。最后讲一个我自己的小习惯在写完一段ranges流水线之后我会强制自己尝试用非ranges的方式再写一遍。如果非ranges版本明显更简单我就留下旧版本如果ranges版本哪怕只清晰了三分我也会留下ranges版本。这个习惯帮我避免了不少为了用而用造成的过度设计推荐你也可以试试。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号