ranges::views::remove_if与actions编译差异原因及选型建议
Range-v3中remove_if视图与动作的代码差异及选择依据
先看两段功能等价的代码:
实现1:使用视图(view)
#include <range/v3/view/remove_if.hpp> #include <range/v3/range/conversion.hpp> #include <vector> std::vector<int> foo(std::vector<int> v, bool(*p)(int)) { return v | ranges::views::remove_if(p) | ranges::to_vector; }
实现2:使用动作(action)
#include <range/v3/action/remove_if.hpp> #include <vector> std::vector<int> bar(std::vector<int> v, bool(*p)(int)) { return std::move(v) | ranges::actions::remove_if(p); }
这两个是无模板纯函数,签名完全一致,从调用者角度看功能相同、运行结果一致,但不同编译器生成的汇编代码差异明显:GCC主干版本为后者生成更短的代码,Clang主干版本则为前者生成更短的代码。
一、代码生成差异的核心原因
除了编译器优化能力的差异外,两者底层实现逻辑的本质不同,是必须生成不同汇编的根本原因:
views::remove_if是惰性视图,它不会修改传入的容器,只是生成一个过滤元素的“虚拟范围”。后续的to_vector需要遍历这个虚拟范围,把符合条件的元素移动/拷贝到新创建的vector中。整个流程是:原容器传值拷贝 → 创建惰性视图 → 遍历视图构造新容器。actions::remove_if是原地修改动作,通过std::move接管传入容器的所有权后,直接在原容器内存上执行类似std::remove_if的逻辑——把不符合过滤条件的元素移到容器前端,然后截断容器大小。整个流程是:接管原容器所有权 → 原地调整元素 → 返回修改后的容器。
两种实现的内存操作、元素转移路径完全不同,不同编译器的优化策略侧重点又不一样:
- GCC对原地修改的路径优化更高效,因为可以直接复用原容器内存,减少额外内存分配和拷贝开销,生成的代码更紧凑;
- Clang对惰性视图转容器的路径优化更到位,能抵消新容器创建带来的额外成本,所以生成的代码更短。
二、选择实现的依据(除基准测试外)
- 内存开销:要避免额外内存分配的话,优先选
actions::remove_if——它直接复用传入容器的内存,不需要为新容器分配空间;而视图+to_vector的组合一定会创建新容器,带来额外内存开销。 - 异常安全:
actions::remove_if在移动元素时如果抛出异常,可能导致容器处于未定义状态;而视图+to_vector是构造新容器,若构造失败,原容器的拷贝不会被修改,异常安全性更高。 - 代码风格适配:如果项目偏好函数式惰性求值的风格,选
views系列;习惯命令式原地修改风格的话,选actions系列。 - 扩展性:如果后续还要对过滤后的范围做其他链式操作,惰性视图的组合性更强;如果只是单纯过滤后返回容器,动作的实现更直接。
内容的提问来源于stack exchange,提问作者Enlico
相关产品推荐
相关产品推荐

