15

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:

Visual C++ (see live)

Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()

Clang (libc++) (see live)

Noisy()
Noisy(const Noisy&)
Noisy(Noisy&&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()

GCC 4.9.0 (see live)

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.

a) Use const lvalue references !

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.

b) Then use non-const rvalue references !

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.

Marc Andreson
  • 3,405
  • 5
  • 35
  • 51
  • You question is : "why does GCC perform additional copy/move operation for arguments passed by value?", and that is answered there. Or am I missing something? – BЈовић Oct 24 '14 at 08:04
  • @BЈовић It looks like the other answer explains why there are 2 copies, and this one asks why there are 3 with gcc. – Marc Glisse Oct 24 '14 at 08:06
  • @BЈовић: This question explicitly aims at why GCC does an **additional** copy that is avoided by MSVC; in the "duplicate" this is only addressed (and not really answered) in comments. Voting to reopen. – DevSolar Oct 24 '14 at 08:09
  • I made a note at the top explaining, hopefully that will assist in the unduping process. – Mark Harrison Oct 24 '14 at 08:10
  • 4
    I believe that filing an enhancement PR in gcc's bugzilla is the way to go. They may chose not to change it though. operator() doesn't directly call the function, it delegates that to a helper `_M_invoker`, hence the extra copy. – Marc Glisse Oct 24 '14 at 08:21
  • @MarcGlisse Why comment? That is the answer – BЈовић Oct 24 '14 at 08:28
  • @BЈовић I'm aware of the fact the call gets delegated (maybe multiple times), that is why I posted a code proposal that I think VC++ uses. with this trick you can add any number of indirections, and end up with only two copies (as they operate on rvalue references, not arguments passed by value), that is why the question asks "Why doesn't ... use **internally**", internally means between helper functions/objects. Or maybe I'm wrong and my code is invalid, I would be grateful to hear that this approach is incorrect for some reason I don't know about. – Marc Andreson Oct 24 '14 at 08:59
  • 7
    I think the reason is probably that GCC's `std::function` is based on our `std::tr1::function` which was written many years before rvalue-references existed. I've optimised other call wrappers such as `std::bind`, `std::thread` and `std::async` to use perfect forwarding but `function` might still be suboptimal. I'll take a look. – Jonathan Wakely Oct 24 '14 at 11:56
  • @JonathanWakely thanks! please share your findings once you have them – Marc Andreson Oct 24 '14 at 11:58
  • 4
    Fixed by https://gcc.gnu.org/ml/gcc-patches/2014-10/msg03121.html – Jonathan Wakely Oct 29 '14 at 18:41

3 Answers3

5

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.

Marc Glisse
  • 7,550
  • 2
  • 30
  • 53
  • I am not sure that's really an answer, but BЈовић asked me to post it, and there may not be any true answer to "why" except that the code was written like that to begin with and noone complained before. – Marc Glisse Oct 24 '14 at 08:46
  • All in all thank you for the answer. However to me it looks that any additional level of indirection could **also** operate on rvalue references, just like in my snippet. When an argument gets passed by value: `void operator()(Noisy n)`, and one calls `std::forward(n)` then it is turned into rvalue reference, so that additional helper can declare its `operator()` as `operator()(Args&&...)` so that `Noisy` parameter turns into `operator()(Noisy&&)`, and then additional forward to another level of indirection could also use above trick. This minimizes the number of copies (I think so). – Marc Andreson Oct 24 '14 at 08:49
  • 1
    @MarcAndreson Maybe it could, indeed. Then there is a choice of whether it is simpler to remove the indirection or make the indirection more clever. That's all information you can provide in your bug report. Unless you want to contribute a patch yourself? That would be welcome: https://gcc.gnu.org/wiki/GettingStarted . – Marc Glisse Oct 24 '14 at 08:53
0

for clang-3.4.2 with libstdc++-4.9.1,it is:

Noisy()
Noisy(const Noisy&)
Noisy(const Noisy&)
Noisy(const Noisy&)
foo(Noisy)
~Noisy()
~Noisy()
~Noisy()
~Noisy()

no rvalue reference at all !

Peixu Zhu
  • 2,111
  • 1
  • 15
  • 13
0

It is a case of poor QoI. There is no good reason, really.

There is a convoluted work around. Pass a type that type erases down to construction of T without doing it. Then invoke the construction inside. With elision, it isn't all that tricky.

template<class T>
struct ctor_view {
  ctor_view(T&&t):ctor_view(tag{},std::move(t)){}
  ctor_view(T const&t):ctor_view(tag{},t){}
  ctor_view():ptr(nullptr),
    f(+[](void*)->T{return{};})
  {}
  T operator()()const&&{
    return f(ptr);
  };
  operator T()const&&{
    return std::move(*this)();
  }
private:
  void* ptr;
  T(*f)(void*);
  struct tag {};
  template<class U,class pU=std::decay_t<U>*>
  ctor_view(tag, U&&t):ptr(const_cast<pU>(std::addressof(t))),
    f(+[](void* p){
      U&& t = static_cast<U&&>(*(pU)(p));
      return std::forward<U>(t);
    })
  {}
};

there are probably errors above, but the above takes either a T&& or T const& or nothing, and produces a type-erased factory for a T, either moving or copy constructing it.

std::function< void(ctor_view<X>) > = [](X x){};

will then avoid the extra move. Most (but not all) uses of that signature should work (except some cases of return type deduction).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524