Сначала рассмотрим следующий код:
#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);
он вообще не компилируется.