In this example, a foo
instance does nothing but print whether it's copy- or move-constructed.
#include <iostream>
#include <algorithm>
#include <vector>
struct foo {
foo()=default;
foo(foo &&) { std::cout << "move constructed\n"; }
foo(const foo &) { std::cout << "copy constructed\n"; }
};
int main()
{
foo x;
std::vector<int> v; // empty
std::remove_if(v.begin(), v.end(),
[x=std::move(x)](int i){ return false; });
}
This produces the following output:
move constructed
copy constructed
move constructed
move constructed
copy constructed
copy constructed
Questions:
std::remove_if
create so many closures ?Compiler is gcc 8.1.1
If we take a look at the implementation of std::remove_if
in gcc's libstdc++-v3 we notice that the predicate is passed down the call chain (by value, at times) a few steps before reaching the lowermost __find_if
function (used by remove_if
).
Let's count the moves and copies:
move constructed
when the predicate (including the captured x
) is sent by value, but as a non-lvalue, to std::remove_if
entry point
copy constructed
when passed on to the __gnu_cxx::__ops::__pred_iter(...)
function, which in turn:
invokes the _GLIBCXX_MOVE
macro, thus the move constructed
,
which moves the predicate over to the _Iter_pred
ctor which moves it (move constructed
) into the _M_pred
member.
The call from std::remove_if
to std::__remove_if
seems to be optimized, as the _Iter_pred
is not an lvalue, I guess, but __remove_if
in turn pass on the wrapped predicate, by value, to std::__find_if
, for another copy constructed
invocation.
std::__find_if
, in turn, forwards the wrapped predicate, by value, to another __find_if
overload, which is finally the sink of this call chain, and the final copy constructed
.
It can be interesting to compare with e.g. clang's implementation of std::remove_if
, as clang (6.0.1) do not produce this move-copy chain for OP's std::remove_if
example. A quick glance shows that It seems as if clang use traits on the predicates type to make sure to pass the predicate as an lvalue reference.
Both clang and gcc produce the same move
/copy
chains for the contrived example that follows, which shows a similar chain to gcc's implementation:
#include <iostream>
#include <utility>
struct foo {
foo() = default;
foo(foo &&) { std::cout << "move constructed\n"; }
foo(const foo &) { std::cout << "copy constructed\n"; }
};
template <typename Pred>
struct IterPred {
Pred m_pred;
explicit IterPred(Pred pred) : m_pred(std::move(pred)) {}
};
template <typename T>
inline IterPred<T> wrap_in_iterpred (T l) {
return IterPred<T>(std::move(l));
}
template <typename T>
void find_if_overload(T l) {
(void)l;
}
template <typename T>
void find_if_entrypoint(T l) {
find_if_overload(l);
}
template <typename T>
void remove_if_entrypoint(T l) {
find_if_entrypoint(
wrap_in_iterpred(l));
}
int main()
{
foo x;
remove_if_entrypoint([x=std::move(x)](int){ return false; });
}
Where both gcc (8.2.0) and clang (6.0.1) produces the following chain:
move constructed
copy constructed
move constructed
move constructed
copy constructed
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With