RVO,移动操作和进退两难

时间:2011-10-21 14:38:08

标签: c++ c++11

在过去的一天左右,我一直在学习移动构造函数,试图坚持按照大多数人似乎建议的价值返回的一般规则,并且遇到了一个有趣的(对我来说)困境。

假设我有一个昂贵的构造/复制类'C',它具有正确定义的复制构造函数,赋值运算符,移动构造函数和移动赋值运算符。

首先,这段代码省略了我预期的复制构造函数:

C make_c1() {
    return C();
}

就像这样:

C make_c2() {
    C tmp;
    return tmp;
}

这样做(无论我传入1还是2):

C make_c3(int a) {
    return a == 1 ? make_c1() : make_c2();
}

当我遇到这个问题时,我遇到了一个问题:

C make_c4(int a) {
    C tmp;
    return a == 1 ? make_c1() : tmp;
}

传入1会触发RVO以获取make_c1的结果,但传入2会触发tmp上的复制构造函数。

将函数修改为以下内容会导致为tmp触发移动构造函数:

C make_c5(int a) {
    C tmp;
    return a == 1 ? make_c1() : std::move(tmp);
}

除了......之外,一切都很美好。

在这些简单的例子中,RVO已被触发,就像我希望的那样。

但是,如果我的代码稍微复杂一些,并且某些编译器在最后一个函数中没有唤起RVO怎么办?在这种情况下,我需要在std :: move中包含对make_c1的调用,这将使那些引起RVO的编译器的代码效率降低。

所以我的问题是:

  1. 当我返回本地对象时,为什么make_c4中没有调用move构造函数? (它毕竟将被销毁)。
  2. 在函数make_c5中,我应该按值返回make_c1的结果还是移动它? (为了避免不同编译器/平台的不同版本的代码)。
  3. 有没有更好的方法来编写最终函数,以便它为合理的编译器实现做正确的事情?
  4. 我一直在玩的编译器是关于Cygwin的GCC 4.5.3。

1 个答案:

答案 0 :(得分:7)

隐含的收益回报仅在RVO合法的相同背景下合法。当表达式是一个非易失性自动对象(函数或catch子句参数除外)的名称时,RVO是合法的,它具有与函数返回类型相同的cv-非限定类型([class.copy] / p31 / b1 )。

如果您将make_c4转换为:

C make_c4(int a) {
    C tmp;
    if (a == 1)
        return make_c1();
    return tmp;
}

然后,您获得了make_c4(2)号召唤的预期移动结构。完全由您说明的原因,您的make_c5重写是不可取的。

<强>更新

我还应该包含对[expr.cond] / p6 / b1的引用,它解释了当第二个表达式是prvalue而第三个是lvalue时条件表达式的语义,但它们都具有相同的类型: / p>

  

第二个和第三个操作数具有相同的类型;结果是   那种类型。如果操作数具有类类型,则结果为prvalue   结果类型的临时,从任一个复制初始化   第二个操作数或第三个操作数取决于的值   第一个操作数。

即。此段落指定条件的结果prvalue是 copy-initialized ,来自示例中的第3个参数。 复制初始化在[dcl.init] / p14中定义。当复制初始化的源是类型左值时,这将调用类型的复制构造函数。如果源是rvalue,它将调用移动构造函数(如果存在),否则它将调用复制构造函数。

条件表达式的规范不允许从lvalue参数隐式移动,即使条件表达式是返回表达式的一部分。这种语言有可能被制作成允许这种隐含的举动,但据我所知,它从未提出过。此外,条件表达式的现有规范已经非常复杂,使得对语言的这种改变变得更加困难。