c++ - 与 (N)RVO 一起有效使用 move 语义

标签 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(..) 并调用一次复制构造函数和一次调用 move 构造函数。我知道 RVO 无法摆脱这些调用之一,因为我们正在处理函数参数。

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

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

我的问题如下:

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

谢谢!

最佳答案

我喜欢测量,所以我设置了这个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(两者都是右值)给出不同的答案。所以我决定测试它们(除了左值):

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)。第一个数字是复制结构的数量,第二个数字是 move 结构的数量:

+----+--------+--------+---------+
|    | 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(左值和 xvalues)的 1 次额外 move 构造。这是一个关于是否要支付额外的 move 构造以节省过载的判断调用(并且没有一个正确的答案)。

(answered) Why does RVO kick in the last option and not the second?

要启动 RVO,返回语句需要如下所示:

return arg;

如果您将其复杂化:

return std::move(arg);

或:

return arg.makeChanges();

然后 RVO 被抑制。

Is there a better way to do this?

我最喜欢的是 p3 和 p5。我对 p5 而不是 p4 的偏好仅仅是风格上的。我避免将 move 放在 return 语句上,因为我知道它会自动应用,因为担心会意外抑制 RVO。但是在 p5 中 RVO 无论如何都不是一个选项,即使 return 语句确实得到了隐式 move 。所以 p5 和 p4 真的是等价的。选择您的风格。

Had we passed in a temporary, 2nd and 3rd options would call a move constructor while returning. Is is possible to eliminate that using (N)RVO?

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

关于c++ - 与 (N)RVO 一起有效使用 move 语义,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9952622/

相关文章:

c++ - std::copy 用于多维数组

c++ - 链式转换构造函数

c++ - 在 C++ 中将信息写入文件的函数

c++ - 抽象类和 move 语义

c++ - 移出的 xvalue 究竟需要什么?

C++: std::move with rvalue reference 不 move 内容

c++ - 为什么boost在函数中实现BOOST_CURRENT_FUNCTION

c++ - 通过 Windows 终端访问 C++ ASCII 代码

c++ - 如何将 std::vector<uint8_t> 转换为 QByteArray?

c++ - 如何在构造函数中初始化数组?