与(N)RVO一起有效地使用移动语义

时间:2012-03-31 02:56:52

标签: c++ c++11 move-semantics return-value-optimization

假设我想实现一个应该处理对象并返回一个新的(可能已更改的)对象的函数。我想在C + 11中尽可能高效地做到这一点。环境如下:

class Object {
    /* Implementation of Object */
    Object & makeChanges();
};

我想到的替代方案是:

// First alternative:
Object process1(Object arg) { return arg.makeChanges(); }
// Second alternative:
Object process2(Object const & arg) { return Object(arg).makeChanges(); }
Object process2(Object && arg) { return std::move(arg.makeChanges()); }
// Third alternative:
Object process3(Object const & arg) { 
    Object retObj = arg; retObj.makeChanges(); return retObj; 
}
Object process3(Object && arg) { std::move(return arg.makeChanges()); }

注意:我想使用像process()这样的包装函数,因为它会做一些其他工作,我希望尽可能多地重用代码。

更新

我将makeChanges()与给定的签名一起使用,因为我正在处理的对象提供了具有该类型签名的方法。我猜他们用它来进行方法链接。我还修复了提到的两个语法错误。谢谢你指出这些。我还添加了第三种替代方案,我将在下面提出问题。

用clang尝试这些[即Object obj2 = process(obj);]会产生以下结果:

第一个选项对复制构造函数进行两次调用;一个用于传递参数,另一个用于返回。可以改为说return std::move(..)并且一次调用复制构造函数和一次调用移动构造函数。据我所知,RVO无法摆脱其中一个调用,因为我们正在处理函数参数。

在第二个选项中,我们仍然有两次调用复制构造函数。这里我们做一个显式调用,一个是在返回时进行的。我期待RVO能够开始并摆脱后者,因为我们返回的对象是与参数不同的对象。但是,它没有发生。

在第三个选项中,我们只有一个对复制构造函数的调用,这是显式的。 (N)RVO消除了我们为返回而做的复制构造函数调用。

我的问题如下:

  1. (已回答)为什么RVO会选择最后一个选项,而不是第二个?
  2. 有更好的方法吗?
  3. 如果我们传入临时,第二和第三个选项会在返回时调用移动构造函数。是否可以使用(N)RVO消除它?
  4. 谢谢!

3 个答案:

答案 0 :(得分:16)

我想衡量一下,所以我设置了这个Object

#include <iostream>

struct Object
{
    Object() {}
    Object(const Object&) {std::cout << "Object(const Object&)\n";}
    Object(Object&&) {std::cout << "Object(Object&&)\n";}

    Object& makeChanges() {return *this;}
};

我认为某些解决方案可能会为xvalues和prvalues(两者都是rvalues)提供不同的答案。所以我决定测试它们(除了左值):

Object source() {return Object();}

int main()
{
    std::cout << "process lvalue:\n\n";
    Object x;
    Object t = process(x);
    std::cout << "\nprocess xvalue:\n\n";
    Object u = process(std::move(x));
    std::cout << "\nprocess prvalue:\n\n";
    Object v = process(source());
}

现在这是一个简单的事情,尝试所有的可能性,由他人贡献的,我自己扔了一个:

#if PROCESS == 1

Object
process(Object arg)
{
    return arg.makeChanges();
}

#elif PROCESS == 2

Object
process(const Object& arg)
{
    return Object(arg).makeChanges();
}

Object
process(Object&& arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 3

Object
process(const Object& arg)
{
    Object retObj = arg;
    retObj.makeChanges();
    return retObj; 
}

Object
process(Object&& arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 4

Object
process(Object arg)
{
    return std::move(arg.makeChanges());
}

#elif PROCESS == 5

Object
process(Object arg)
{
    arg.makeChanges();
    return arg;
}

#endif

下表总结了我的结果(使用clang -std = c ++ 11)。第一个数字是复制结构的数量,第二个数字是移动结构的数量:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |    legend: copies/moves
+----+--------+--------+---------+
| p1 |  2/0   |  1/1   |   1/0   |
+----+--------+--------+---------+
| p2 |  2/0   |  0/1   |   0/1   |
+----+--------+--------+---------+
| p3 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+
| p4 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p5 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+

process3对我来说似乎是最好的解决方案。但是它确实需要两次重载。一个处理左值,一个处理右值。如果由于某种原因这是有问题的,解决方案4和5只需要一次重载,代价是glvalues(lvalues和xvalues)的1个额外移动构造。这是一个判断调用,是否一个人想要支付额外的移动结构来节省超载(并且没有一个正确的答案)。

  

(已回答)为什么RVO会选择最后一个选项而不是第二个?

要让RVO启动,return语句需要如下所示:

return arg;

如果你把它复杂化:

return std::move(arg);

或:

return arg.makeChanges();

然后RVO被禁止。

  

有更好的方法吗?

我的最爱是p3和p5。我对p5优于p4的偏好仅仅是风格上的。当我知道它将被自动应用以避免意外抑制RVO时,我回避将move放在return声明上。但是在p5中,无论如何RVO都不是一个选项,即使return语句确实得到了隐含的移动。所以p5和p4确实是等价的。选择你的风格。

  

如果我们通过一个临时的,第二和第三选项会召集一个举动   返回时的构造函数。有可能消除使用   (N)RVO?

“prvalue”列与“xvalue”列解决了这个问题。有些解决方案为xvalues添加了额外的移动构造,有些则没有。

答案 1 :(得分:2)

您展示的所有功能都不会对其返回值进行任何重要的返回值优化。

makeChanges会返回Object&。因此,必须将其复制到一个值中,因为您将其返回。所以前两个将始终复制要返回的值。就拷贝数而言,第一个拷贝产生两个拷贝(一个用于参数,一个用于返回值)。第二个副本生成两个副本(一个显式在函数中,一个用于返回值。

第三个甚至不应该编译,因为你不能隐式地将l值引用转换为r值引用。

所以,真的,不要这样做。如果你想传递一个对象,并在原位修改它,那么就这样做:

Object &process1(Object &arg) { return arg.makeChanges(); }

这会修改提供的对象。没有复制或任何东西。当然,有人可能想知道为什么process1不是成员函数或其他东西,但这并不重要。

答案 2 :(得分:0)

最快的方法是 - 如果参数是左值,则复制它并返回该副本 - 如果是右值,则移动它。可以随时移动返回或应用RVO / NRVO。这很容易实现。

Object process1(Object arg) {
    return std::move(arg.makeChanges());
}

这非常类似于多种运算符重载的规范C ++ 11形式。