Почему параметры универсальной ссылки должны быть отлиты перед использованием?

В лекции о всеобщих ссылках Скотт Мейерс (примерно на 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.

Ваш пример здесь на самом деле не имеет большого смысла в этом отношении.