【问题标题】:Why parameters of universal reference needs to be casted, before used?为什么通用参考的参数需要在使用之前进行转换?
【发布时间】:2012-10-01 23:39:36
【问题描述】:

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,它似乎工作正常(输出相同)。

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

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

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

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

【问题讨论】:

  • 你没问多少同样的问题就打败了我。我也在寻找这方面的解释。
  • @Vikas 那么,这是否意味着我的理解是正确的?您确实需要使用右值的移动函数吗?
  • 我的意思是我看了他的演讲后也有类似的疑惑。我也打算在这里问这个问题:-)。
  • 在 callBar 中移动有什么意义,您不会复制?

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


【解决方案1】:

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

【讨论】:

  • 所以,在这种情况下,这也应该没问题:void callBar( A &amp;&amp; a ) { std::move(a).bar();std::move(a).bar(); },正如视频中所解释的,参数应该在被访问之前被强制转换。对吗?
  • 视情况而定。你对bar() 可能窃取a 的资源感到满意吗?因为它可能会以这种方式实现
【解决方案2】:

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

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 应用于此类参数会保留参数的左值或右值,因此通常这是将参数转发给另一个函数的最安全方法。

【讨论】:

  • f(c); // error: c is not an rvalue 为什么会失败?它不应该接受它吗,因为 T&& 是通用参考(如讲座中所述)?
  • @BЈовић - 不,在模板之外,&amp;&amp; 表示右值引用。仅在模板中表示右值引用或左值引用。
  • @PeteBecker 但 f() 一个模板,所以 f(c) 是正确的。另外,我会将g 的签名更改为g(C&amp;&amp;)g(C&amp;),以使代码和cmets 匹配。 (在目前的形式g(c) 是一个错误)
  • @mitchnull - 哎呀,我把 cmets 弄反了。我会修复它。谢谢。
【解决方案3】:
void callBar( A && a )
{
  std::move(a).bar();
}

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

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

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

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

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

【讨论】:

  • 搬家的决定应该在呼叫现场做出。函数不应该决定移动它通过引用获得的参数:它不知道移动是安全的。
  • @PeteBecker 如果不是为了传递可以安全移动的参数,参数列表中的右值引用有什么意义?
  • @LucDanton - &amp;&amp; 在类型为模板参数的参数上不会告诉您它是右值;这就是“通用参考”一词的意义所在。 template &lt;class T&gt; void f(C&amp;&amp;); void h() { C c; f(c); } 在这里,f 使用左值调用。 void g(C&amp;&amp;); void h() { C c; f(c); } 在这里,对f 的调用是一个错误,因为c 是一个左值,而g 需要一个右值。
  • 啊,我似乎错过了callBar 确实是一个诱导上下文,因此是一个普遍参考的事实。嗯
  • @PeteBecker 虽然我还没有看到演讲,但我认为第二个 callBar(在 OP 中)不是模板。 “类似地”在通用引用+std::forward 和右值引用+std::move 之间引入平行。
【解决方案4】:

讲座中说的是这样的:

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&amp; )。因为callBar 中的a 是左值引用。当通用引用是右值引用时,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&amp;&amp; ra )函数中使用,所以它不会进入死循环。相反,它调用采用左值引用的版本。

因此(如讲座中所述):

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-22
    • 1970-01-01
    • 2011-05-10
    • 1970-01-01
    • 2021-10-12
    • 2020-12-13
    • 1970-01-01
    • 2016-01-02
    相关资源
    最近更新 更多