提问者:小点点

我们可以在可能的时候使用返回值优化,并在移动时回退,而不是复制,语义学时不?


是否可以编写C代码,我们在可能的情况下依赖返回值优化(RVO),但在不可能的情况下回退到移动语义学?例如,以下代码由于条件无法使用RVO,因此它将结果复制回来:

#include <iostream>

struct Foo {
    Foo() {
        std::cout << "constructor" << std::endl;
    }
    Foo(Foo && x) {
        std::cout << "move" << std::endl;
    }
    Foo(Foo const & x) {
        std::cout << "copy" << std::endl;
    }
    ~Foo() {
        std::cout << "destructor" << std::endl;
    }
};

Foo f(bool b) {
    Foo x;
    Foo y;
    return b ? x : y;  
}

int main() {
   Foo x(f(true));
   std::cout << "fin" << std::endl;
}

这产生

constructor
constructor
copy
destructor
destructor
fin
destructor

这是有道理的。现在,我可以通过更改行来强制在上面的代码中调用移动构造函数

    return b ? x : y;  

    return std::move(b ? x : y);

这给出了输出

constructor
constructor
move
destructor
destructor
fin
destructor

不过,我真的不喜欢直接叫d::搬家。

真的,问题是我处于这样一种情况,即使构造函数存在,我也绝对不能调用复制构造函数。在我的用例中,有太多的内存需要复制,尽管删除复制构造函数会很好,但由于各种原因,它不是一个选项。同时,我想从函数中返回这些对象,并且更喜欢使用RVO。现在,我真的不想在编码时记住RVO的所有细微差别,当它被应用时,当它没有被应用时。大多数情况下,我希望返回对象,我不希望调用复制构造函数。当然,RVO更好,但是移动语义学很好。有可能的时候有RVO的方法,没有的时候有移动语义学的方法吗?

以下问题帮助我弄清楚了发生了什么。基本上,12.8.32的标准状态:

当满足或将满足复制操作省略的条件(除了源对象是函数参数这一事实),并且要复制的对象由左值指定时,首先执行重载解析以选择复制的构造函数,就好像对象由右值指定一样。如果重载解析失败,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是cv限定的),则再次执行重载解析,将对象视为左值。[注意:无论是否会发生复制省略,都必须执行此两阶段重载解析。如果不执行省略,它确定要调用的构造函数,即使调用被省略,所选构造函数也必须是可访问的。-结束说明]

好的,所以要弄清楚复制elison的标准是什么,我们看12.8.31

在具有类返回类型的函数中的返回语句中,当表达式是与函数返回类型cvun的类型相同的非易失性自动对象(除了函数或catch-子句参数)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作

因此,如果我们将f的代码定义为:

Foo f(bool b) {
    Foo x;
    Foo y;
    if(b) return x;
    return y;
}

然后,我们的每个返回值都是一个自动对象,所以12.8.31说它有资格复制elison。这就开始了12.8.32,它说复制就像它是一个右值一样执行。现在,RVO不会发生,因为我们不知道先验要走哪条路径,但是由于12.8.32中的要求,调用了移动构造函数。从技术上讲,当复制到x时,避免了一个移动构造函数。基本上,在运行时,我们得到:

constructor
constructor
move
destructor
destructor
fin
destructor

关闭构造函数上的elide会生成:

constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor

现在,假设我们回到

Foo f(bool b) {
    Foo x;
    Foo y;
    return b ? x : y;
}

我们必须看看5.16.4中条件运算符的语义学

如果第二个和第三个操作数是相同值类别的glvalue并且具有相同的类型,则结果是该类型和值类别,并且如果第二个或第三个操作数是位字段,或者如果两者都是位字段,则它是位字段。

由于x和y都是左值,条件运算符是左值,但不是自动对象。因此,12.8.32不起作用,我们将返回值视为左值而不是右值。这需要调用复制构造函数。因此,我们得到

constructor
constructor
copy
destructor
destructor
fin
destructor

现在,由于这种情况下的条件运算符基本上是复制出值类别,这意味着代码

Foo f(bool b) {
    return b ? Foo() : Foo();
}

将返回右值,因为条件运算符的两个分支都是右值。我们看到这一点:

constructor
fin
destructor

如果我们在构造函数上关闭elide,我们会看到移动

constructor
move
destructor
move
destructor
fin
destructor

基本上,这个想法是,如果我们返回一个右值,我们将调用移动构造函数。如果我们返回一个左值,我们将调用复制构造函数。当我们返回一个类型与返回类型匹配的非易失性自动对象时,我们将返回一个右值。如果我们有一个不错的编译器,这些副本和移动可能会被RVO省略。然而,至少,我们知道在无法应用RVO的情况下调用什么构造函数。


共2个答案

匿名用户

当返回语句中的表达式是非易失性自动持续时间对象,而不是函数或catch子句参数,并且具有与函数返回类型相同的cv非限定类型时,生成的复制/移动有资格进行复制省略。该标准还继续说,如果禁止复制省略的唯一原因是源对象是函数参数,并且如果编译器无法省略副本,则应该像表达式是右值一样对副本进行重载解析。因此,它更喜欢移动构造函数。

OTOH,由于您使用的是三元表达式,因此没有任何条件成立,并且您只能使用常规副本。将您的代码更改为

if(b)
  return x;
return y;

调用移动构造函数。

请注意,RVO和复制省略之间存在区别——复制省略是标准允许的,而RVO是一种通常用于在标准允许复制省略的情况下省略副本的技术。

匿名用户

是的,有。不要返回三元运算符的结果;而是使用if/else。当您直接返回局部变量时,尽可能使用移动语义学。但是,在您的情况下,您不是直接返回局部-您返回的是表达式的结果。

如果您将函数更改为如下所示:

Foo f(bool b) {
    Foo x;
    Foo y;
    if (b) { return x; }
    return y;
}

然后您应该注意到调用的是您的移动构造函数而不是您的复制构造函数。

如果您坚持每个返回语句返回一个局部值,那么如果类型支持,则将使用移动语义学。

如果你不喜欢这种方法,那么我建议你坚持使用std::移动。你可能不喜欢它,但你必须选择你的毒药——语言就是这样。