是否有可能在可能的情况下编写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,何时没有移动语义?
以下问题帮助我弄清楚发生了什么.基本上,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是一种常用于在标准允许复制省略的情况的子集中删除副本的技术.
就在这里.不要返回三元运算符的结果; 使用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.你可能不喜欢它,但你必须选择你的毒药 - 语言就是这样.