Почему 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);
}
и его вывод в разных компиляторах:
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()
То есть 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){};
затем избежит дополнительного перемещения. Большинство (но не все) использование этой подписи должно работать (за исключением некоторых случаев вывода типа возврата).