в лекция об универсальных ссылках, Скотт Мейерс (примерно на 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, и, кажется, работает нормально (вывод тот же).
Точно так же, почему он рекомендует использовать move внутри функции с rvalue? (ответ такой же, как и для std :: forward)
void callBar( A && a )
{
std::move(a).bar();
}
Я понимаю что оба std::move
а также std::forward
Это просто приведение к соответствующим типам, но действительно ли это необходимо в приведенном выше примере?
Бонус: как можно изменить пример, чтобы получить копию объекта, который передается этой функции?
Это нужно, потому что bar()
может быть перегружен отдельно для значений и значений. Это означает, что он может сделать что-то по-другому, или может быть не разрешено, в зависимости от того, правильно ли вы описали a
как lvalue или rvalue, или просто слепо относился к нему как lvalue. В настоящее время большинство пользователей не используют эту функцию и не имеют к ней доступа, потому что большинство популярных компиляторов не поддерживают ее — даже GCC 4.8 не поддерживает rvalue *this
, Но это стандарт.
Есть два разных использования &&
по параметру функции. Для обычной функции это означает, что аргумент является ссылкой на значение; для функции шаблона это означает, что она может быть или ссылка 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-ness-аргумента, поэтому в общем случае это самый безопасный способ пересылки аргумента в другую функцию.
void callBar( A && a )
{
std::move(a).bar();
}
В случае, если у вас есть ссылка на rvalue в качестве параметра, который может привязывать только к значению Обычно вы хотите использовать семантику перемещения, чтобы выйти из этого значения и вывести его из себя.
Сам параметр является именующий, потому что это именованная вещь. Вы можете взять его адрес.
Таким образом, чтобы сделать его снова значимым и уйти от него, необходимо применить std::move
к этому. Если бы вы буквально просто вызывали функцию с переданным параметром, я не понимаю, почему у вас есть параметр, являющийся ссылкой на rvalue.
Вы хотите передать ссылку на rvalue только в том случае, если вы собираетесь отойти от этого внутри своей функции, поэтому вам придется использовать std::move
,
Ваш пример здесь на самом деле не имеет большого смысла в этом отношении.
В лекции сказано следующее:
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
делает его r-значением, когда универсальная ссылка является ссылкой-значением.
Следующая модификация еще раз подтверждает это:
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