为什么需要在使用之前对通用参考的参数进行投射?

时间:2012-10-11 10:18:59

标签: c++ c++11 move-semantics

lecture about universal references中,Scott Meyers(大约第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::movestd::forward都只是强制转换为适当的类型,但在上面的示例中是否真的需要这些强制转换?

Bonus:如何修改示例以生成传递给该函数的对象的副本?

4 个答案:

答案 0 :(得分:2)

这是必要的,因为bar()可能会分别为rvalues和lvalues重载。这意味着它可能会以不同的方式执行某些操作,或者不允许执行某些操作,具体取决于您是否正确地将a描述为左值或右值,或者只是盲目地将其视为左值。目前,大多数用户不使用此功能而且没有接触它,因为最受欢迎的编译器不支持它 - 即使GCC 4.8也不支持右值*this。但它是标准的。

答案 1 :(得分:2)

&&对函数的参数有两种不同的用法。对于普通函数,它意味着参数是一个右值引用;对于模板函数,它意味着它可以 左值引用或左值引用:

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
}

fg内,将std::forward应用于此类参数会保留参数的左值或右值,因此通常这是将参数转发到的最安全的方法另一个功能。

答案 2 :(得分:1)

void callBar( A && a )
{
  std::move(a).bar();
}

如果你有一个rvalue引用作为参数,只能绑定到rvalue ,你通常会想要使用移动语义从这个rvalue移动并把它带出来。< / p>

参数本身是左值,因为它是一个命名的东西。你可以拿它的地址。

因此,为了使其再次成为右值并能够从中移动,您可以将std::move应用于它。如果你只是在传递参数上调用函数,我不明白为什么你有一个参数是一个右值参考。

如果您要在函数内部移动,则只想传递右值引用,这就是您必须使用std::move的原因。

这方面的例子在这方面实际上没有多大意义。

答案 3 :(得分:0)

讲座中的内容是:

void doWork( Widget&& param )
{
  ops and exprs using std::move(param)
}
  SM:这意味着:如果您看到带有右值引用的代码,并且您看到使用该参数而不被move包裹,则非常可疑。

经过一番思考,我意识到这是正确的(如预期的那样)。将原始示例中的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是左值参考。当通用引用是右值引用时,std::forward使其成为右值。

接下来的修改进一步证明了这一点:

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 ),因此它无法进入无限循环。相反,它调用版本采用左值参考。

因此(如讲座中所述):

  • std::forward必须用于通用引用
  • std::move必须用于右值参考