Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 872
Marc Andreson Avatar asked Oct 24 '14 07:10

Marc Andreson


1 Answers

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.

like image 143
Marc Glisse Avatar answered Oct 05 '22 11:10

Marc Glisse