我们可以使用返回值优化,当可能时,回退移动,不复制,语义不是? [英] Can we use the return value optimization when possible and fall back on move, not copy, semantics when not?

查看:111
本文介绍了我们可以使用返回值优化,当可能时,回退移动,不复制,语义不是?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

是否有可能编写C ++代码,尽可能依赖于返回值优化(RVO),但是如果没有,则可以使用move语义?例如,下面的代码不能使用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;
}

这会产生

 构造函数
构造函数
copy
destructor
destructor
fin
destructor

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

  return b? x:y; 

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

这给出了输出

 构造函数
构造函数
move
destructor
destructor
fin
destructor

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



问题是,我在一个情况下,我绝对,积极,不能调用复制构造函数,即使构造函数存在。在我的用例中,有太多的内存要复制,虽然它是很好的只是删除复制构造函数,它不是一个选择的各种原因。同时,我想从函数返回这些对象,并且更喜欢使用RVO。现在,我真的不想记住RVO在编码时的所有细微差别,以及何时不应用RVO。大多数情况下,我想要返回的对象,我不想要调用的复制构造函数。当然,RVO是更好的,但移动语义是好的。在可能的情况下是否有RVO的方法和不是的移动语义?






编辑1



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


当满足复制操作的删除标准或将
met保存的事实,源对象是一个函数参数
和要复制的对象由一个左值指定,重载
分辨率选择的副本的构造函数首先执行
好像对象是由右值指定的。如果过载分辨率
失败,或者如果所选
构造函数的第一个参数的类型不是对象类型(可能是
cv-qualified)的右值引用,则再次执行重载分辨率,考虑
对象作为左值。 [注意:无论是否发生复制精度,都必须执行此两阶段重载分辨率
。如果不执行elision,
决定要调用的构造函数,
,并且即使调用
时,所选的构造函数也必须可访问。 - end note]


好的,为了找出复制elison的标准,我们来看一下12.8.31 <在
表达式是非易失性自动对象的名称时,在带有类返回类型的函数中的return语句中,p>


(除
a函数或catch子句参数之外)具有与函数返回类型相同的cvunqualified类型

可以省略复制/移动操作,直接将自动对象构建到函数的return
value


因此,如果我们定义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中的要求调用move构造函数。从技术上讲,在复制到x时,避免了一个移动构造函数。基本上,当运行时,我们得到:

 构造函数
构造函数
move
destructor
destructor
fin
destructor

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

 构造函数
构造函数
move
destructor
destructor
move
destructor
fin
destructor

现在,

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



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


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




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

 构造函数
构造函数
copy
destructor
destructor
fin
destructor

现在,由于这种情况下的条件运算符基本上是复制,表示代码

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

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

 构造函数
fin
destructor



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

  constructor 
move
destructor
move
destructor
fin
destructor

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

解决方案

return语句中的表达式是非易失性自动持续时间对象,而不是函数或catch子句参数,具有与函数返回类型相同的cv无限制类型,因此生成的副本/移动符合复制elision的条件。标准还继续说,如果唯一的原因是复制删除被禁止是源对象是一个函数参数,并且如果编译器不能删除一个副本,该复制的重载分辨率应该做,就像表达式是一个右值。因此,它更喜欢移动构造函数。



OTOH,由于您使用的是三元表达式,所有条件都不成立,并且您仍然使用常规副本。将您的代码更改为

  if(b)
return x;
return y;

调用move构造函数。



注RVO和复制精度之间存在区别 - 复制精度是标准允许的,而RVO是通常用于在标准允许复制精度的情况下删除复制品的技术。


Is it possible to write C++ code where we rely on the return value optimization (RVO) when possible, but fall back on move semantics when not? For example, the following code can not use the RVO due to the conditional, so it copies the result back:

#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;
}

This yields

constructor
constructor
copy
destructor
destructor
fin
destructor

which makes sense. Now, I could force the move constructor to be called in the above code by changing the line

    return b ? x : y;  

to

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

This gives the output

constructor
constructor
move
destructor
destructor
fin
destructor

However, I don't really like to call std::move directly.

Really, the issue is that I'm in a situation where I absolutely, positively, can not call the copy constructor even when the constructor exists. In my use case, there's too much memory to copy and although it'd be nice to just delete the copy constructor, it's not an option for a variety of reasons. At the same time, I'd like to return these objects from a function and would prefer to use the RVO. Now, I don't really want to have to remember all of the nuances of the RVO when coding and when it's applied an when it's not applied. Mostly, I want the object to be returned and I don't want the copy constructor called. Certainly, the RVO is better, but the move semantics are fine. Is there a way to the RVO when possible and the move semantics when not?


Edit 1

The following question helped me figure out what's going on. Basically, 12.8.32 of the standard states:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]

Alright, so to figure out what the criteria for a copy elison are, we look at 12.8.31

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

As such, if we define the code for f as:

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

Then, each of our return values is an automatic object, so 12.8.31 says that it qualifies for copy elison. That kicks over to 12.8.32 which says that the copy is performed as if it were an rvalue. Now, the RVO doesn't happen because we don't know a priori which path to take, but the move constructor is called due to the requirements in 12.8.32. Technically, one move constructor is avoided when copying into x. Basically, when running, we get:

constructor
constructor
move
destructor
destructor
fin
destructor

Turning off elide on constructors generates:

constructor
constructor
move
destructor
destructor
move
destructor
fin
destructor

Now, say we go back to

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

We have to look at the semantics for the conditional operator in 5.16.4

If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.

Since both x and y are lvalues, the conditional operator is an lvalue, but not an automatic object. Therefore, 12.8.32 doesn't kick in and we treat the return value as an lvalue and not an rvalue. This requires that the copy constructor be called. Hence, we get

constructor
constructor
copy
destructor
destructor
fin
destructor

Now, since the conditional operator in this case is basically copying out the value category, that means that the code

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

will return an rvalue because both branches of the conditional operator are rvalues. We see this with:

constructor
fin
destructor

If we turning off elide on constructors, we see the moves

constructor
move
destructor
move
destructor
fin
destructor

Basically, the idea is that if we return an rvalue we'll call the move constructor. If we return an lvalue, we'll call the copy constructor. When we return a non-volatile automatic object whose type matches that of the return type, we return an rvalue. If we have a decent compiler, these copies and moves may be elided with the RVO. However, at the very least, we know what constructor is called in case the RVO can't be applied.

解决方案

When the expression in the return statement is a non-volatile automatic duration object, and not a function or catch-clause parameter, with the same cv-unqualified type as the function return type, the resulting copy/move is eligible for copy elision. The standard also goes on to say that, if the only reason copy elision was forbidden was that the source object was a function parameter, and if the compiler is unable to elide a copy, the overload resolution for the copy should be done as if the expression was an rvalue. Thus, it would prefer the move constructor.

OTOH, since you are using the ternary expression, none of the conditions hold and you are stuck with a regular copy. Changing your code to

if(b)
  return x;
return y;

calls the move constructor.

Note that there is a distinction between RVO and copy elision - copy elision is what the standard allows, while RVO is a technique commonly used to elide copies in a subset of the cases where the standard allows copy elision.

这篇关于我们可以使用返回值优化,当可能时,回退移动,不复制,语义不是?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆