Почему параметры универсальной ссылки должны быть отлиты перед использованием?
В лекции о всеобщих ссылках Скотт Мейерс (примерно на 40-й минуте) сказал, что объекты, являющиеся универсальными ссылками, должны быть преобразованы в реальный тип, перед использованием. Другими словами, всякий раз, когда есть функция шаблона с универсальным ссылочным типом, перед использованием операторов и выражений следует использовать std::forward
, иначе может быть сделана копия объекта.
Мое понимание этого в следующем примере:
#include <iostream>
struct A
{
A() { std::cout<<"constr"<<std::endl; }
A(const A&) { std::cout<<"copy constr"<<std::endl; }
A(A&&) { std::cout<<"move constr"<<std::endl; }
A& operator=(const A&) { std::cout<<"copy assign"<<std::endl; return *this; }
A& operator=(A&&) { std::cout<<"move assign"<<std::endl; return *this; }
~A() { std::cout<<"destr"<<std::endl; }
void bar()
{
std::cout<<"bar"<<std::endl;
}
};
A getA()
{
A a;
return a;
}
template< typename T >
void callBar( T && a )
{
std::forward< T >( a ).bar();
}
int main()
{
{
std::cout<<"\n1"<<std::endl;
A a;
callBar( a );
}
{
std::cout<<"\n2"<<std::endl;
callBar( getA() );
}
}
Как и ожидалось, выход:
1
constr
bar
destr
2
constr
move constr
destr
bar
destr
Вопрос в том, почему это необходимо?
std::forward< T >( a ).bar();
Я пробовал без std:: forward, и кажется, что он работает нормально (выход такой же).
Точно так же, почему он рекомендует использовать перемещение внутри функции с rvalue? (ответ такой же, как и для std:: forward)
void callBar( A && a )
{
std::move(a).bar();
}
Я понимаю, что как std::move
, так и std::forward
просто причисляются к соответствующим типам, но действительно ли эти броски нужны в приведенном выше примере?
Бонус: как можно изменить пример для создания копии объекта, который передается этой функции?
Ответы
Ответ 1
В лекции говорится следующее:
void doWork( Widget&& param )
{
ops and exprs using std::move(param)
}
SM: Это означает: если вы видите код, который принимает ссылку rvalue, и вы видите использование этого параметра без обмотания движением, это очень подозрительно.
После некоторой мысли я понял, что это правильно (как и ожидалось). Изменение функции callBar
в исходном примере для этого демонстрирует точку:
void reallyCallBar( A& la )
{
std::cout<<"lvalue"<<std::endl;
la.bar();
}
void reallyCallBar( A&& ra )
{
std::cout<<"rvalue"<<std::endl;
ra.bar();
}
template< typename T >
void callBar( T && a )
{
reallyCallBar( std::forward< T >( a ) );
}
Если std::forward
не использовался в callBar
, тогда будет использоваться reallyCallBar( A& )
. Потому что a
в callBar
является ссылкой на lvalue. std::forward
делает это rvalue, когда универсальная ссылка является ссылкой rvalue.
Следующая модификация доказывает еще одну точку:
void reallyCallBar( A& la )
{
std::cout<<"lvalue"<<std::endl;
la.bar();
}
void reallyCallBar( A&& ra )
{
std::cout<<"rvalue"<<std::endl;
reallyCallBar( ra );
}
template< typename T >
void callBar( T && a )
{
reallyCallBar( std::forward< T >( a ) );
}
Так как std::move
не используется в функции reallyCallBar( A&& ra )
, он не вводит бесконечный цикл. Вместо этого он вызывает версию, использующую ссылку lvalue.
Поэтому (как объясняется в лекции):
-
std::forward
должен использоваться для универсальных ссылок
-
std::move
должен использоваться для ссылок rvalue
Ответ 2
Это необходимо, потому что bar()
может быть перегружен отдельно для rvalues и lvalues. Это означает, что он может сделать что-то по-другому или не разрешено, в зависимости от того, правильно ли вы описали a
как lvalue или rvalue, или просто слепо относились к нему как к lvalue. В настоящее время большинство пользователей не используют эту функцию и не подвергаются воздействию, потому что самые популярные компиляторы ее не поддерживают - даже GCC 4.8 не поддерживает rvalue *this
. Но это стандарт.
Ответ 3
Для параметра &&
для параметра используется две разные функции. Для обычной функции это означает, что аргумент является ссылкой rvalue; для функции шаблона это означает, что она может быть или ссылкой на rvalue или ссылкой lvalue:
template <class T> void f(T&&); // rvalue or lvalue
void g(T&&); // rvalue only
void g(T&) // lvalue only
void h() {
C c;
f(c); // okay: calls f(T&)
f(std::move(c)); // okay: calls f(T&&)
g(c); // error: c is not an rvalue
g(std::move(c)); // okay: move turns c into an rvalue
}
Внутри f
и g
, применяя std::forward
к такому аргументу, сохраняет значение lvalue или rvalue аргумента, поэтому, вообще говоря, самый безопасный способ перенаправления аргумента другой функции.
Ответ 4
void callBar( A && a )
{
std::move(a).bar();
}
В случае, когда у вас есть ссылка rvalue как параметр, который может привязываться только к rvalue, вы обычно хотите использовать семантику перемещения, чтобы перейти от этого значения rvalue и отбросить его.
Параметр сам по себе является lvalue, потому что это именованная вещь. Вы можете принять его адрес.
Итак, чтобы снова присвоить значение rvalue и уйти от него, вы примените к нему std::move
. Если вы буквально просто вызывали функцию по переданному параметру, я не понимаю, почему у вас будет параметр, являющийся ссылкой rvalue.
Вам нужно только передать ссылку rvalue, если вы собираетесь перейти от этого внутри своей функции, поэтому вам нужно использовать std::move
.
Ваш пример здесь на самом деле не имеет большого смысла в этом отношении.