这里应该使用右值引用吗?

Posted

技术标签:

【中文标题】这里应该使用右值引用吗?【英文标题】:Should an rvalue reference be used here? 【发布时间】:2013-10-18 21:47:52 【问题描述】:

我一直在阅读有关右值引用和std::move 的内容,并且我有一个性能问题。对于以下代码:

template<typename T>
void DoSomething()

  T x = foo();
  bar(std::move(x));

从性能的角度来看,像这样使用右值引用重写它会更好吗?

template<typename T>
void DoSomething()

  T&& x = foo();
  bar(x);

如果我正确理解了右值引用,就像评论者指出的那样,这就像bar(foo()) 已被调用一样。但可能需要中间值;这样做有用吗?

【问题讨论】:

可能不会。但是foobar 是什么样的呢? 为什么不直接bar(foo()) @MichaelBurr,我主要想知道它的理论性能。当然,bar(foo()) 会更简洁,除非您需要两行之间的结果。 【参考方案1】:

使用右值引用/移动语义的最佳方式是,在大多数情况下,不要太努力。按值返回函数局部变量。并从这些函数中按值构造它们:

X foo();

template<typename T>
void DoSomething()

  T x = foo();
  ...

如果您是class X 的作者,那么您可能希望确保X 具有高效的移动构造和移动分配。如果X 的复制构造函数和复制赋值已经很有效,那么实际上就没有什么可做的了:

class X

     int data_;
public:
     // copy members just copy data_
;

如果您发现自己在X 的复制构造函数中分配内存,或者复制赋值,那么您应该考虑编写移动构造函数和移动赋值运算符。尽力让他们noexcept

虽然规则总有例外,但总的来说:

    不要通过引用返回,无论是左值还是右值引用,除非您希望客户端能够直接访问非函数局部变量。

    不要通过引用、左值或右值捕获返回。是的,在某些情况下,它可能会为您节省一些东西,而不会给您带来麻烦。甚至不要考虑将其作为一项规则。仅当您的性能测试强烈激励您这样做时,才执行此类操作。

    您(希望)学会的所有与左值引用无关的事情仍然与右值引用一样危险(例如从函数返回悬空引用)。

    第一个程序的正确性。这包括编写广泛且易于运行的单元测试,涵盖 API 的常见和极端情况。 然后开始优化。当您开始尝试编写极其优化的代码(例如,通过引用捕获返回)之前,您还没有一个正确且经过良好测试的解决方案,一个人不可避免地会编写如此脆弱的代码,以至于出现的第一个错误修复只会进一步破坏代码,但通常以非常微妙的方式。这是一个即使是经验丰富的程序员(包括我自己)也必须一遍又一遍地学习的课程。

    尽管我在 (4) 中说了什么,但即使在第一次写入时,也要注意 O(N) 性能。 IE。如果您的第一次尝试由于基本的整体设计缺陷或较差的算法而非常缓慢,以至于无法在合理的时间内执行其基本功能,那么您需要的不仅仅是新代码,您还需要一个新设计。在这个项目符号中,我不是在谈论诸如您是否已通过右值引用捕获返回之类的事情。我说的是你是否创建了 O(N^2) 算法,当 O(N log N) 或 O(N) 可以完成这项工作。当需要完成的工作很重要,或者代码非常复杂以至于你无法判断发生了什么时,这一切都很容易做到。

【讨论】:

【参考方案2】:

这里 std::move 没有帮助。 因为您已经制作了对象的副本(尽管省略可能会有所帮助)。

template<typename T>
void DoSomething()

  T x = foo();
  bar(std::move(x));

这里的右值引用没有帮助 因为变量x 是一个命名对象(因此不再是右值引用)。

template<typename T>
void DoSomething()

  T&& x = foo();
  bar(x);

最好用:

template<typename T>
void DoSomething()

  bar(foo());

但如果你必须在本地使用它:

template<typename T>
void DoSomething()

  T&& x = foo();
  //  Do stuff with x
  bar(std::move(x));

虽然我对上述内容不是 100% 确定,但希望得到一些反馈。

【讨论】:

假设foo() 产生一个prvalue(即不是参考),例如MyClass foo();。然后,在第一个示例中:T x = foo(); 如果可能则不执行任何操作(复制/移动省略),如果可能则移动,否则复制(例如,如果有用户提供的复制,但没有移动 ctor)。如果bar 采用按值,bar(std::move(x)) 可以用移动替换副本。 T&amp;&amp; x = foo(); 可能会阻止复制或移动,尽管这不太可能(从返回值临时复制/移动,可以省略)。如果不允许复制/移动,它也可以工作。 @DyP:所以如果可能的话,第一个版本会移动。这个std::move调用bar其实很有用。 是的,如果bar 按值取值并且T 是可移动的,则std::move(x) 在第一个示例中很有用(另外,如果bar 具有右值引用重载,它可能改变语义)。同样,如果您在第二个示例中添加它可能会有所帮助(在与第一个示例相同的条件和相同的原因下)。 @DyP:好的。同意第一个例子。 @DyP:你确定第二个例子。因为x 是一个命名变量,所以它不再是一个右值引用。 (这是非形成性注释) > n3690 Section 5 Expressions [expr] 第 7 段 一般来说,这条规则的效果是,将命名的右值引用视为左值,将对象的未命名右值引用视为 xvalue;对函数的右值引用被视为左值,无论是否命名。。这是一个非常不规范的注释。但是根据我的阅读,因为它是一个命名变量,所以它会折叠为左值(而不是右值)。

以上是关于这里应该使用右值引用吗?的主要内容,如果未能解决你的问题,请参考以下文章

重新理解C11的右值引用

允许将右值绑定到非常量左值引用吗?

深入思考右值引用

静态转换为右值引用和 std::move 之间有啥区别吗

从4行代码看右值引用

右值引用,移动语义,完美转发