6

关于通用引用的讲座中,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,它似乎工作正常(输出相同)。

同样,为什么他建议在带有右值的函数内部使用 move ?(答案与 std::forward 相同)

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

我知道两者std::movestd::forward只是转换为适当的类型,但是在上面的示例中真的需要这些转换吗?

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

4

4 回答 4

2

它是必需的,因为bar()可能会为右值和左值分别重载。这意味着它可能会做一些不同的事情,或者完全不允许,这取决于您是否正确地将其描述a为左值或右值,或者只是盲目地将其视为左值。目前,大多数用户不使用此功能,也没有接触过它,因为最流行的编译器不支持它——甚至 GCC 4.8 也不支持 rvalue *this。但它是标准的。

于 2012-10-11T10:32:03.097 回答
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
}

fand内部g,应用于std::forward这样的参数会保留参数的左值或右值,因此通常这是将参数转发给另一个函数的最安全方法。

于 2012-10-11T15:42:25.160 回答
1
void callBar( A && a )
{
  std::move(a).bar();
}

如果您有一个右值引用作为参数,它只能绑定到一个右值,您通常希望使用移动语义从这个右值移动并取出它的胆量。

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

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

如果您要在函数内部从 this 移动,您只想传递一个右值引用,这就是为什么您必须使用std::move.

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

于 2012-10-11T10:32:31.890 回答
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& )使用 。因为aincallBar是一个左值引用。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必须用于右值引用
于 2012-10-12T07:57:31.067 回答