Why doesn't GCC's std::function use rvalue references to arguments passed by value to pass them between its internal delegates?
Asked Answered
A

3

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.

Applesauce answered 24/10, 2014 at 7:21 Comment(10)
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?Natica
@BЈовић It looks like the other answer explains why there are 2 copies, and this one asks why there are 3 with gcc.Bluestone
@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.Melaniamelanic
I made a note at the top explaining, hopefully that will assist in the unduping process.Orphism
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.Bluestone
@MarcGlisse Why comment? That is the answerNatica
@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.Applesauce
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.Gobo
@JonathanWakely thanks! please share your findings once you have themApplesauce
Fixed by gcc.gnu.org/ml/gcc-patches/2014-10/msg03121.htmlGobo
B
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.

Bluestone answered 24/10, 2014 at 8:41 Comment(3)
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.Bluestone
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<Noisy>(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).Applesauce
@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: gcc.gnu.org/wiki/GettingStarted .Bluestone
A
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 !

Aerograph answered 24/10, 2014 at 9:25 Comment(0)
D
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).

Detrusion answered 24/5, 2015 at 4:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.