First, consider the following code:
#include <iostream>
#include <functional>
struct Noisy
{
Noisy() { std::cout << "Noisy()" << std::endl; }
Noisy(const Noisy&) { std::cout << "Noisy(const Noisy&)" << std::endl; }
Noisy(Noisy&&) { std::cout << "Noisy(Noisy&&)" << std::endl; }
~Noisy() { std::cout << "~Noisy()" << std::endl; }
};
void foo(Noisy n)
{
std::cout << "foo(Noisy)" << std::endl;
}
int main()
{
Noisy n;
std::function<void(Noisy)> f = foo;
f(n);
}
and its output in different compilers:
Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
~Noisy()
That is, GCC performs one more move/copy operation compared to Visual C++ (and Clang+libc++), that, let's agree, is not efficient in all cases (like for std::array<double, 1000>
parameter).
To my understanding, std::function
needs to make a virtual call to some internal wrapper that holds actual function object (in my case foo
). As such, using forwarding references and perfect forwarding is not possible (since virtual member functions cannot be templated).
However, I can imagine that the implementation could std::forward
internally all arguments, no matter if they are passed by value or by reference, like below:
// interface for callable objects with given signature
template <class Ret, class... Args>
struct function_impl<Ret(Args...)> {
virtual Ret call(Args&&... args) = 0; // rvalues or collaped lvalues
};
// clever function container
template <class Ret, class... Args>
struct function<Ret(Args...)> {
// ...
Ret operator()(Args... args) { // by value, like in the signature
return impl->call(std::forward<Args>(args)...); // but forward them, why not?
}
function_impl<Ret(Args...)>* impl;
};
// wrapper for raw function pointers
template <class Ret, class... Args>
struct function_wrapper<Ret(Args...)> : function_impl<Ret(Args...)> {
// ...
Ret (*f)(Args...);
virtual Ret call(Args&&... args) override { // see && next to Args!
return f(std::forward<Args>(args)...);
}
};
because arguments passed-by-value will just turn into rvalue references (fine, why not?), rvalue references will collapse and remain rvalue references, as well as lvalue references will collapse and remain lvalue references (see this proposal live). This avoids copies/moves between any number of internal helpers/delegates.
So my question is, why does GCC perform additional copy/move operation for arguments passed by value, while Visual C++ (or Clang+libc++) does not (as it seems unnecessary)? I would expect the best possible performance from STL's design/implementation.
Please note that using rvalue references in std::function
signature, like std::function<void(Noisy&&)>
, is not a solution for me.
Please note that I am not asking for a workaround. I perceive neither of the possible workarounds as correct.
Why not? Because now when I invoke f
with rvalue:
std::function<void(const Noisy&)> f = foo;
f(Noisy{});
it inhibits move operation of Noisy
temporary and forces copy.
Why not? Because now when I invoke f
with lvalue:
Noisy n;
std::function<void(Noisy&&)> f = foo;
f(n);
it does not compile at all.
In libstdc++, std::function::operator()
does not call the function directly, it delegates that task to a helper _M_invoker
. This extra level of indirection explains the extra copy. I did not study the code, so I don't know if this helper is mere convenience or if it plays a strong role. In any case, I believe the way to go is to file an enhancement PR in gcc's bugzilla.
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