我们可以在可能的情况下使用返回值优化,而不是复制,而不是复制,语义不是吗?

wye*_*r33 7 c++ c++11 c++14

是否有可能在可能的情况下编写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;
}
Run Code Online (Sandbox Code Playgroud)

这产生了

constructor
constructor
copy
destructor
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

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

    return b ? x : y;  
Run Code Online (Sandbox Code Playgroud)

    return std::move(b ? x : y);
Run Code Online (Sandbox Code Playgroud)

这给出了输出

constructor
constructor
move
destructor
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

但是,我真的不想直接调用std :: move.

真的,问题在于,即使构造函数存在,我仍然无法正确地调用复制构造函数.在我的用例中,有太多的内存要复制,虽然删除复制构造函数很好,但由于各种原因,它不是一个选项.同时,我想从函数中返回这些对象,并且更喜欢使用RVO.现在,我真的不想在编码时记住RVO的所有细微差别,并且当它未应用时应用它.大多数情况下,我希望返回该对象,我不希望调用复制构造函数.当然,RVO更好,但移动语义很好.有没有办法在可能的情况下使用RVO,何时没有移动语义?


编辑1

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

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

好吧,为了弄清楚复制精灵的标准是什么,我们看12.8.31

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

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

Foo f(bool b) {
    Foo x;
    Foo y;
    if(b) return x;
    return y;
}
Run Code Online (Sandbox Code Playgroud)

然后,我们的每个返回值都是一个自动对象,所以12.8.31表示它有资格获得复制elison.踢到12.8.32,表示副本的执行就好像它是一个右值.现在,RVO没有发生,因为我们不知道先前采用哪条路径,但是由于12.8.32中的要求而调用了移动构造函数.从技术上讲,复制到x时避免使用一个移动构造函数.基本上,在运行时,我们得到:

constructor
constructor
move
destructor
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

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

constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

现在,说我们回去吧

Foo f(bool b) {
    Foo x;
    Foo y;
    return b ? x : y;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

constructor
constructor
copy
destructor
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

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

Foo f(bool b) {
    return b ? Foo() : Foo();
}
Run Code Online (Sandbox Code Playgroud)

将返回一个rvalue,因为条件运算符的两个分支都是rvalues.我们看到这个:

constructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

如果我们关闭构造函数的elide,我们会看到这些动作

constructor
move
destructor
move
destructor
fin
destructor
Run Code Online (Sandbox Code Playgroud)

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

Pra*_*han 10

如果return语句中的表达式是非易失性自动持续时间对象,而不是函数或catch-clause参数,并且具有与函数返回类型相同的cv-unqualified类型,则生成的复制/移动符合复制省略条件.标准还继续说,如果禁止复制省略的唯一原因是源对象是一个函数参数,并且如果编译器无法删除副本,则应该完成复制的重载解析,就好像表达是一个rvalue.因此,它更喜欢移动构造函数.

OTOH,因为你正在使用三元表达式,所以没有任何条件成立,你就会遇到常规副本.将代码更改为

if(b)
  return x;
return y;
Run Code Online (Sandbox Code Playgroud)

调用move构造函数.

请注意,RVO和复制省略之间存在区别 - 复制省略是标准允许的,而RVO是一种常用于在标准允许复制省略的情况的子集中删除副本的技术.


cdh*_*wie 6

就在这里.不要返回三元运算符的结果; 使用if/ else代替.直接返回局部变量时,尽可能使用移动语义.但是,在您的情况下,您没有直接返回本地 - 您将返回表达式的结果.

如果您将功能更改为如下所示:

Foo f(bool b) {
    Foo x;
    Foo y;
    if (b) { return x; }
    return y;
}
Run Code Online (Sandbox Code Playgroud)

那么你应该注意调用你的移动构造函数而不是你的复制构造函数.

如果您坚持每个return语句返回一个本地值,那么如果该类型支持,则将使用移动语义.

如果你不喜欢这种方法,那么我建议你坚持下去std::move.你可能不喜欢它,但你必须选择你的毒药 - 语言就是这样.