Почему GCC std:: function не использует ссылки rvalue для аргументов, переданных значением, чтобы передавать их между своими внутренними делегатами?

Сначала рассмотрим следующий код:

#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);
}

и его вывод в разных компиляторах:

Visual С++ (см. прямой эфир)

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

Clang (libС++) (смотреть в прямом эфире)

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

GCC 4.9.0 (см. прямой эфир)

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

То есть GCC выполняет еще одну операцию перемещения/копирования по сравнению с Visual С++ (и Clang + libС++), что, согласитесь, неэффективно во всех случаях (например, для параметра std::array<double, 1000>).

Насколько я понимаю, std::function необходимо сделать виртуальный вызов для некоторой внутренней оболочки, которая содержит фактический объект функции (в моем случае foo). Таким образом, использование ссылок пересылки и безупречной пересылки невозможно (поскольку виртуальные функции-члены не могут быть шаблонами).

Однако я могу представить, что реализация могла std::forward внутренне использовать все аргументы, независимо от того, переданы ли они по значению или по ссылке, как показано ниже:

// 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)...);
    }
};

потому что аргументы, передаваемые по значению, просто превратятся в ссылки rvalue (отлично, почему бы и нет?), ссылки на rvalue будут сбрасываться и оставаться ссылками rvalue, а ссылки lvalue будут сбрасываться и оставаться ссылками lvalue (см. это предложение в прямом эфире). Это позволяет избежать копирования/перемещения между любым количеством внутренних помощников/делегатов.

Итак, мой вопрос, почему GCC выполняет дополнительную операцию копирования/перемещения для аргументов, передаваемых по значению, в то время как Visual С++ (или Clang + libС++) не делает (как кажется ненужным)? Я бы ожидал наилучшей производительности от проектирования/реализации STL.

Обратите внимание, что использование ссылок rvalue в подписи std::function, например std::function<void(Noisy&&)>, для меня не является решением.


Обратите внимание на, что я не прошу об обходном пути. Я не воспринимаю ни одно из возможных обходных решений как правильное.

a) Используйте ссылки const lvalue!

Почему бы и нет? Потому что теперь, когда я вызываю f с rvalue:

std::function<void(const Noisy&)> f = foo;
f(Noisy{});

он блокирует операцию перемещения Noisy временно и принудительно копирует.

b) Затем используйте ссылки не-const rvalue!

Почему бы и нет? Потому что теперь, когда я вызываю f с lvalue:

Noisy n;
std::function<void(Noisy&&)> f = foo;
f(n);

он вообще не компилируется.

Ответы

Ответ 1

В libstdС++ std::function::operator() не вызывает функцию напрямую, она делегирует эту задачу помощнику _M_invoker. Этот дополнительный уровень косвенности объясняет дополнительную копию. Я не изучал код, поэтому не знаю, является ли этот помощник просто удобством или он играет сильную роль. В любом случае, я считаю, что путь к файлу - это усиление PR в gcc bugzilla.

Ответ 2

для clang-3.4.2 с libstdС++ - 4.9.1, это:

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

ссылка на rvalue вообще!

Ответ 3

Это случай плохого QoI. На самом деле нет веской причины.

Существует сложная работа. Передайте тип, который стирает до конструкции T, не делая этого. Затем вызовите конструкцию внутри. С элицией это не все так сложно.

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);
    })
  {}
};

вероятно, есть ошибки выше, но приведенное выше принимает либо T&&, либо T const&, либо ничего, и создает стираемый тип factory для T, либо перемещаясь, либо копируя его.

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

затем избежит дополнительного перемещения. Большинство (но не все) использование этой подписи должно работать (за исключением некоторых случаев вывода типа возврата).