拿这个
#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;
}
相比之下
#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);
}
周围没有模板,只有两个 TU,每个 TU 都提供具有相同签名的纯函数。鉴于它们的实现,从调用者的角度来看,我希望这两个函数能够完成相同的任务。他们似乎就是这么做的。
但是,它们编译成相当不同的代码,以至于 GCC(至少是 trunk)为后者生成更短的代码,而 Clang(trunk)为前者生成更短的代码。
我看不出这两个函数有任何理由编译成不同的代码,除了“编译器很难为这两个函数提供相同的代码”,但是什么让它变得如此困难?或者,如果我错了,为什么这两个函数必须编译为不同的程序集?
除了基准测试之外,还有什么理由让我更喜欢其中一种实现而不是另一种实现?
完整示例。
我不确定理论上两者是否可能生成相同的代码。让我们来看看这两种方法。
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
return std::move(v) | ranges::actions::remove_if(p);
}
对于操作,这是采取
v
,将其就地进行变异以删除满足p
的元素,然后返回相同的v
。这相当于写了:
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
std::erase_if(v, p);
return v;
}
或者,从 C++20 之前开始:
std::vector<int> bar(std::vector<int> v, bool(*p)(int)) {
v.erase(std::remove_if(v.begin(), v.end(), p), v.end());
return v;
}
肯定不会发生分配,我们只是移动一堆int
,然后更改
v.size()
。观点
std::vector<int> foo(std::vector<int> v, bool(*p)(int)) {
return v | ranges::views::remove_if(p) | ranges::to_vector;
}
views::remove_if
是一个惰性过滤器。这让我们可以了解
v
中不满足
p
的元素。然后,
to_vector
将构造一个新的
vector
,需要分配,并将
v
中所有不满足
p
的元素复制到新的
vector
中。返回新向量。它会
v | remove_if(p) | to_vector
分配一个与
vector<int>
不同的新
v
。
v
在此表达式的整个长度内都处于活动状态,因此您无法在此处重用
v
的内存。这里的优化不仅仅是认识到
v
即将被销毁,因此它的分配可以被重用。而且新的
vector
最多与
v
大小相同,因此重用其分配是一个可行的策略。而且这个新
vector
的元素以允许重用该分配的方式填充。再加上分配是可观察的,甚至可能无法从一开始就省略——即使假设编译器可以以某种方式完成所有这些步骤。
从根本上来说,这两种情况只是不同的算法。有时编译器可以弄清楚这一点,但这似乎是一个巨大的延伸。如果存在这样的优化,那么它基本上是针对这种情况手工制作的。
我应该更喜欢哪一个,为什么?一般来说,这个问题的答案是使用最具体的工具来完成工作。如果您有一个
vector<int>
并且您只想要不满足
p
的元素,并且根本不需要原始元素 - 这就是
actions::remove_if
(或者,根据上下文,只需直接调用
std::erase_if
)。这就是
actions::remove_if
旨在解决的工作。