【问题标题】:Efficient use of move semantics together with (N)RVO将移动语义与 (N)RVO 一起有效使用
【发布时间】:2012-04-14 16:59:40
【问题描述】:

假设我想实现一个函数,该函数应该处理一个对象并返回一个新的(可能已更改的)对象。我想在 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 试试这些 [i.e. Object obj2 = process(obj);] 结果如下:

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

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

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

我的问题如下:

  1. (已回答)为什么 RVO 使用最后一个选项而不是第二个选项?
  2. 有没有更好的方法来做到这一点?
  3. 如果我们传入一个临时的,第二个和第三个选项将在返回时调用一个移动构造函数。是否可以使用 (N)RVO 消除这种情况?

谢谢!

【问题讨论】:

  • 为什么makeChanges 会返回Object&?它应该不返回任何内容并且是一个变异函数,或者它应该是 const 并按值返回一个新对象。目前,因为它是 not const,所以您列出的第一个和第二个选项甚至无法编译,因为您正在调用 const 对象上的非 const 成员函数。实际上,如果没有 makeChanges' 当前签名的理由,这会使您的问题变得非常荒谬。
  • @ildjarn:感谢 cmets。我进行了更改并提出了问题。我想我仍然不清楚 RVO 如何/何时启动。我很想听听您的想法和建议。

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


【解决方案1】:

我喜欢测量,所以我设置了这个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(左值和 xvalues)额外移动 1 个移动构造。这是一个判断一个人是否愿意支付额外的移动构造来避免超载(并且没有一个正确的答案)。

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

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

return arg;

如果您将其复杂化:

return std::move(arg);

或:

return arg.makeChanges();

然后 RVO 被禁止。

有没有更好的方法来做到这一点?

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

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

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

【讨论】:

    【解决方案2】:

    您展示的所有函数都不会对其返回值进行任何重大的返回值优化。

    makeChanges 返回一个Object&amp;。因此,它必须被复制到一个值中,因为您要返回它。所以前两个总是会复制要返回的值。在副本数量上,第一个复制了两份(一份用于参数,一份用于返回值)。第二个复制两份(一份在函数中显式复制,一份用于返回值。

    第三个甚至不应该编译,因为您不能将左值引用隐式转换为右值引用。

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

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

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

    【讨论】:

    • 感谢您的回答。我明白为什么第二种选择现在仍然需要副本。但是我不明白您提出的解决方案如何适合。我想要的功能需要生成一个新的Object
    【解决方案3】:

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

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

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

    【讨论】:

    • 当我尝试时,我看到总是调用移动构造函数。 RVO 没有启动。根据第一个答案,我认为这是因为std::move 给了我们一个(r 值)引用但我们按值返回。您是说 RVO 应该(或可以?)参与进来吗?如果没有,process3 似乎效率更高一些(不调用移动构造函数)。
    • @iheap:RVO/NRVO 绝对可以解决这种情况。当然,它可以应用的确切情况充其量是依赖于实现的。您所能做的就是允许它 - 由编译器实际执行。
    • @DeadMG:这里没有 RVO,因为返回表达式是一个右值引用。由于返回类型为Object,因此返回值必须从右值引用构造。 RVO 与临时搬家不同。这意味着完全消除了复制/移出功能。你不能用你在这里给出的代码来做到这一点。
    猜你喜欢
    • 2014-07-13
    • 2011-05-05
    • 1970-01-01
    • 2014-01-05
    • 2019-09-14
    • 2016-11-18
    • 1970-01-01
    • 2016-08-17
    • 2011-06-29
    相关资源
    最近更新 更多