返回由右值引用传递的参数 [英] Returning an argument passed by rvalue reference

查看:153
本文介绍了返回由右值引用传递的参数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如果我有一个类 A 和函数

  A f (A& a)
{
doSomething(a);
return a;
}
A g(A a)
{
doSomething(a);
return a;
}

当返回 code>从 f ,但是从 g 返回时使用move构造函数。然而,从我的理解, f 只能传递一个对象,它是安全移动(临时或对象标记为可移动,例如,使用 std :: move )。有什么例子,当从 f 返回时使用move构造函数不安全吗?为什么我们需要 a 具有自动存储时间?



我阅读答案在这里,但是顶部的答案只显示规范不应该允许移动 a 到函数体中的其他函数;它不解释为什么移动当返回对于 g 是安全的,但不是 f 。一旦我们得到return语句,我们不再需要 f a



更新0



所以我明白临时表达式可以访问,直到完整表达式结束。然而,从 f 返回时的行为似乎违背了语言根深蒂固的语言,它是安全的移动临时或xvalue。例如,如果调用 g(A()),则将临时变量移入 g 有可能引用临时存储在某处。如果我们用xvalue调用 g ,同样会发生。由于只有temporal和xvalues绑定到右值引用,对于从 f a c $ c>,因为我们知道 a 被传递了临时或xvalue。

解决方案

第二次尝试。希望这是更多的简洁和清楚。



我将忽略RVO几乎完全为讨论。它使得它真的令人困惑,因为什么应该发生没有优化 - 这只是关于移动VS副本语义。



为了帮助这个参考将是非常有用的在这里 c ++ 11中的值类型类型



何时移动?



lvalue



它们指的是可能被引用到其他地方的变量或存储位置,因此不应将其内容转移到另一个实例。



prvalue



上面将它们定义为没有标识的表达式。显然,没有其他可以引用无名值,所以这些可以移动。



rvalue



右手的价值,唯一可以肯定的是,他们可以从。



xvalue



p>这些都是混合的 - 他们有身份(是一个参考)他们可以移动。他们不需要有一个命名的变量。原因?他们是令人兴奋的价值,即将被毁灭。考虑他们的最终参考。 xvalues只能从右值生成,这就是为什么/ how std :: move 在将lvalues转换为xvalues(通过函数调用)。



glvalue



另一种具有其右值表达式的突变类型,可以是xvalue或lvalue - 它有身份,但不清楚这是否是变量/存储的最后一个引用,因此不清楚是否可以移动。



分辨率Order



其中存在可以接受 const lvalue ref ,并且传递右值,则右值被绑定,否则使用左值版本。



可能发生的地方



(假设所有类型都是 A 其中没有提及)



它只发生在对象从同一类型的x值初始化。 xvalues绑定到右值,但不像纯表达式那样受限。换句话说,可移动的东西不仅仅是未命名的引用,它们也可以是关于编译器意识的对象的最后引用。



初始化



  A a = std :: move(b); // assign-move 
A a(std :: move(b)); // construct-move



函数参数传递



  void f(A a); 
f(std :: move(b));



函数返回



  A f(){
// A a存在,将很快讨论
return a;
}



为什么不会在 f



考虑f:

上的这个变化

  action1(A& a){
// alter a somehow
}

void action2(A& a){
// alter a somehow
}

A f(A& a){
action1(a);
action2(a);
return a;
}

c $ c>作为 f 中的左值。因为它是一个 lvalue 它必须是一个引用,无论是否显式。每一个普通的变量在技术上都是对自身的参考。



这就是我们去的地方。因为 a 是用于 f 目的的左值,我们实际上返回了一个左值。



要显式生成右值,必须使用 std :: move (或生成 A& result以其他方式)。



为什么会发生在 g



根据我们的腰带,考虑 g

  A g(A a){
action1(a); // as above
action2(a); // as above
return a;
}

是, a 是用于 action1 action2 目的的左值。但是,因为对 a 的所有引用仅存在于 g (它是副本或移入副本)



但是为什么不在 f



&& 没有特定的魔法。真的,你应该把它作为参考首先。我们在 f 中要求一个右值引用,而 > A& 不会改变作为参考的事实,它必须是一个左值,因为 a 存储位置 $ c>在 f 外部,这是任何编译器所关注的。



g 中应用,其中显然 a 的存储是临时的,并且仅当 g 被调用,没有其他时间。在这种情况下,它显然是一个xvalue,可以移动。






rvalue ref vs lvalue ref 和参考传递的安全性



假设我们重载一个函数来接受两种类型参考。会发生什么?

  void v(A& lref); 
void v(A& rref);

唯一的时间 void v(A&&&)将被用于上述(可能发生的地方),否则 void v(A&)。也就是说,在尝试进行lvalue ref重载之前,右值引用将总是尝试绑定到右值引用签名。一个左值引用不应该绑定到右值引用,除非在它可以被当作一个xvalue(保证在当前范围内被销毁,无论我们是否想要它)。



很有趣的是,在右值的情况下,我们知道要传递的对象是临时的。情况并非如此。这是一个签名,用于绑定引用 是临时对象。



喜欢做 int * x = 23; - 它可能是错误的,但是你可以(最终)强制它编译与糟糕的结果,如果你运行它。编译器不能肯定地说,如果你是认真的,或拉他的腿。



关于安全,一个必须考虑的功能,不这样做 - 如果它仍然编译):

  A& make_A(void){
A new_a;
return new_a;
}

虽然语言方面没有明显错误 - 将获得对 返回的引用 - 因为 new_a 的存储位置是回收/无效当函数返回时。因此,使用此函数结果的任何内容都将处理释放的内存。



同样, A f(A& / code>旨在但不限于接受prvalues或xvalue,如果我们真的想强迫别的东西通过。这是 std :: move 进来的地方,让我们这样做。



是因为它相对于 A f(A& a) 仅对于相对于右值过载将优选哪个上下文而不同。在所有其他方面,它是如何 a 由编译器处理相同。



> > A&& 是为移动保留的签名是模拟点;它用于确定我们要绑定到的 A -type参数的哪个版本,我们应该获取所有权的排序rvalue)或排序,我们不应该取得底层数据的所有权(lvalue)(即,将其移动到其他位置并擦除实例/引用我们给出)。在这两种情况下,我们正在使用的是对 f 不受控制的内存的引用。



无论我们做或不是不是编译器可以告诉的东西;它落入编程的常识区域,例如不使用无意义的存储器位置,但是否则是有效的存储器位置。



什么编译器知道 A f(A& a)是不为 a 创建新的存储,因为我们将得到一个地址(参考)来处理。我们可以选择保留源地址不变,但这里的整个想法是通过声明 A&&& 我们告诉编译器嘿,给我引用对象即将消失,所以我可以在这之前做一些事情。这里的关键字是可能,也是一个事实,我们可以明确地定位这个函数签名不正确。



考虑我们是否有一个版本 A 表示,当移动构建时,并没有删除旧实例的数据,并且由于某种原因我们是通过设计实现的(假设我们有自己的内存分配函数,知道我们的内存模型如何保持数据超过对象的生命周期)。



编译器不能知道这一点,因为它需要代码分析来确定对象当它们在右值绑定中处理时 - 这是一个人为判断问题。最好的编译器看到'一个引用,没有分配额外的内存在这里',并遵循引用传递规则。



可以认为编译器思考: a参考,我不需要处理它的内存生命期内 f ,它是一个临时将删除后 f 已完成。



在这种情况下,当临时传递到 f 临时会在我们离开 f 时消失,然后我们可能处于与 A&



语义问题...



std :: move



std :: move 是创建右值引用。大体上它做什么(如果没有别的)是强制结果值绑定到左值而不是左值。其原因是在右值引用可用之前的 A& 的返回签名对于诸如运算符重载(和其他用途确实)之类的内容是不明确的。



运算符 - 示例



 类A {
// ...
public:
A& operator =(A& rhs); // rhs的生命是什么?移动或复制打算?
A&运算符+(A& rhs); // ditto
// ...
};

int main(){
A result = A()+ A(); //不编译!
}

请注意,此不会接受任何运算符的临时对象!在对象复制操作的情况下这样做也没有意义 - 为什么我们需要修改我们复制的原始对象,可能具有复制我们可以稍后修改。这就是为什么我们必须为复制操作符声明 const A& 参数的原因,以及对参考文件进行复制的任何情况,以保证我们不是改变原始对象。



当然,这是一个移动问题,我们必须修改原始对象,以避免新容器的数据被释放过早。 (因此移动操作)。



为了解决这个问题,我们提供了 T&&& 声明替换上面的示例代码,并且具体地指向在上面将不编译的情况下对对象的引用。但是,我们不需要修改 operator + 是一个移动操作,你会很难找到这样做的原因(虽然你可以想) 。再次,因为假定加法不应该修改原始对象,只有表达式中的左操作数对象。所以我们可以这样做:

  class A {
// ...
public:
A& operator =(const A& rhs); // copy-assign
A& operator =(A&& rhs); // move-assign
A& operator +(const A& rhs); //不修改rhs操作数
// ...
};

int main(){
A result = A()+ A(); // const A&此外,A& for assign
A result2 = A()。operator +(A()); //字面上相同的东西
}

em>尽管 A()返回一个临时的事实,它不仅能绑定到 const A& ,但由于预期的加法语义(它不修改其右操作数)而应该。第二个版本的赋值是清楚的,为什么只有一个参数应该被修改。



这也很清楚,移动将发生在赋值,没有将在运算符中 rhs 发生移动。



的返回值语义和参数绑定语义



从函数(well,operator)定义中可以清楚地看出上面只有一个移动的原因。重要的是,我们确实绑定了明显是xvalue / rvalue,什么是运算符+ 中的一个左右。



我必须强调这一点:在这个例子中没有有效的区别在于 operator + operator = 参考他们的参数。就编译器而言,在任一函数体内,参数 const A& 对于 + A& = 。区别仅在于 const ness。 A& A&& 不同的唯一方法是区分签名,而不是类型。



不同的签名有不同的语义,它是编译器的工具包,用于区分某些情况,否则没有明确的区别。



这个函数的另一个例子是 operator ++( void) vs operator ++(int)。前者期望在增量操作之前返回其底层值,后者返回。没有传递 int ,它只是因为编译器有两个签名来工作 - 没有其他方法来指定两个相同的函数具有相同的名称,你可能知道也可能不知道,只是返回类型的一个函数重载是不合法的。



rvalue变量和其他奇怪的情况 - 详尽的测试



要明确地了解 f 中发生的情况我已经将一个smorgasbord的事情但看起来像他们会工作,迫使编译器的手在这一问题上几乎穷尽:

  void bad(int&& ; x,int& y){
x + = y;
}
int&差(int&& z){
return z ++,z + 1,1 + z;
}
int&&& justno(int& no){
return better(no);
}
int num(){
return 1;
}
int main(){
int&& a = num();
++ a = 0;
a ++ = 0;
bad(a,a);
int&&& b =差(a);
int&&& c = justno(b);
++ c =(int)'y';
c ++ =(int)'y';
return 0;
}

g ++ -std = gnu ++ 11 -O0 -Wall -c -fmessage-length = 0 -osrc\\basictest.o..\\src\\basictest.cpp

  .. \src\basictest.cpp:在函数'int&更糟的(int&&)':
..\src\basictest.cpp:5:17:警告:逗号操作符的右操作符没有效果[-Wunused-value]
return z ++ ,z + 1,1 + z;
^
..\src\basictest.cpp:5:26:error:从类型'int'的右值初始化类型为'int&'的非常量引用
return z ++,z + 1,1 + z;
^
..\src\basictest.cpp:在函数'int&& justno(int&)':
..\src\basictest.cpp:8:20:错误:不能绑定'int'lvalue到'int&&'
返回;
^
.. \src\basictest.cpp:4:7:error:初始化'int&差(int&&&)'
int& bad(int& z){
^
.. \src\basictest.cpp:在函数'int main()'中:
..\src\ basictest.cpp:16:13:错误:不能绑定'int'lvalue到'int&&'
bad(a,a);
^
..\src\basictest.cpp:1:6:错误:初始化'void bad(int&&&&&&)'
的参数1坏的(int&& x,int& y){
^
..\src\basictest.cpp:17:23:error:can not bind'int'lvalue to 'int&&'
int&& b =差(a);
^
.. \src\basictest.cpp:4:7:error:初始化'int&差(int&&&)'
int& bad(int& z){
^
.. \src\basictest.cpp:21:7:错误:作为赋值的左操作数需要lvalue
c ++ = int)'y';
^
.. \src\basictest.cpp:在函数'int& bad(int&& amp;)':
.. \src\basictest.cpp:6:1:warning:控制到达非空函数的结束[-Wreturn-type]
}
^
.. \src\basictest.cpp:在函数'int&& justno(int&)':
.. \src\basictest.cpp:9:1:warning:控制到达非空函数的结束[-Wreturn-type]
}
^

01:31:46 Build Finished(took 72ms)

这是未经改变的输出sans构建头,你不需要看到:)我将把它作为一个练习,以理解发现的错误,但重新阅读我自己的解释(特别是在下面),应该是明显的每个错误



结论 - 我们可以从中学到什么?



请注意,编译器将函数体视为单个代码单元。这基本上是关键在这里。无论编译器对函数体做了什么,它都不能对函数的行为做出假设,这需要改变函数体。为了处理那些情况,有模板,但是这超出了讨论的范围 - 只是注意模板生成多个函数体来处理不同的情况,否则相同的函数体必须可重复使用在每个



其次,右值类型主要用于移动操作 - 这是一种非常特殊的情况,预期会在对象的赋值和构造中发生。使用右值引用绑定的其他语义超出了任何编译器的范围。换句话说,最好将右值引用视为语法糖,而不是实际代码。签名在 A&& A& 中有所不同,但是用于函数体的参数类型不,它总是被视为 A& 表示要以某种方式修改 const A& ,虽然句法正确,但不允许所需的行为。



当我说编译器将生成 f 的代码体,如同它被声明为 f(A&) 。以上, A&&& 帮助编译器选择何时允许将可变引用绑定到 f ,否则编译器不考虑 f(A&) f(A&&& amp;) f 返回有所不同。



一个很长的说法: f 的返回方法不取决于它接收的参数类型。



混乱是精确的。实际上,在返回值时有两个副本。首先创建一个副本作为临时,然后这个临时被分配给某些东西(或者它不是并且仍然是纯临时的)。第二个 副本很可能通过返回优化消除。可以在 g 中移动第一个副本,但不能在 f 中。我期望在 f 不能被省略的情况下,将有一个副本,然后从原始 f 代码。



要覆盖此临时必须使用 std :: move 显式构造, f 中的返回语句。然而,在 g 中,我们返回一个已知对于 g 的函数体是暂时的,因此它是



我建议编译原始代码,禁用所有优化,并在诊断消息中添加复制和移动构造函数以保持制表符何时和何处在删除之前移动或复制值成为一个因素。即使我错了,一个未优化的构造函数/操作的跟踪将绘制一个明确的图片编译器做了什么,希望很明显为什么它做它做了什么...


If I have a class A and functions

A f(A &&a)
{
  doSomething(a);
  return a;
}
A g(A a)
{
  doSomething(a);
  return a;
}

the copy constructor is called when returning a from f, but the move constructor is used when returning from g. However, from what I understand, f can only be passed an object that it is safe to move (either a temporary or an object marked as moveable, e.g., using std::move). Is there any example when it would not be safe to use the move constructor when returning from f? Why do we require a to have automatic storage duration?

I read the answers here, but the top answer only shows that the spec should not allow moving when passing a to other functions in the function body; it does not explain why moving when returning is safe for g but not for f. Once we get to the return statement, we will not need a anymore inside f.

Update 0

So I understand that temporaries are accessible until the end of the full expression. However, the behavior when returning from f still seems to go against the semantics ingrained into the language that it is safe to move a temporary or an xvalue. For example, if you call g(A()), the temporary is moved into the argument for g even though there could be references to the temporary stored somewhere. The same happens if we call g with an xvalue. Since only temporaries and xvalues bind to rvalue references, it seems like to be consistent about the semantics we should still move a when returning from f, since we know a was passed either a temporary or an xvalue.

解决方案

Second attempt. Hopefully this is more succinct and clear.

I am going to ignore RVO almost entirely for this discussion. It makes it really confusing as to what should happen sans optimizations - this is just about move vs copy semantics.

To assist this a reference is going to be very helpful here on the sorts of value types in c++11.

When to move?

lvalue

These are never moved. They refer to variables or storage locations that are potentially being referred to elsewhere, and as such should not have their contents transferred to another instance.

prvalue

The above defines them as "expressions that do not have identity". Clearly nothing else can refer to a nameless value so these can be moved.

rvalue

The general case of "right-hand" value, and the only thing that's certain is they can be moved from. They may or may not have a named reference, but if they do it is the last such usage.

xvalue

These are sort of a mix of both - they have identity (are a reference) and they can be moved from. They need not have a named variable. The reason? They are eXpiring values, about to be destroyed. Consider them the 'final reference'. xvalues can only be generated from rvalues which is why/how std::move works in converting lvalues to xvalues (through the result of a function call).

glvalue

Another mutant type with its rvalue cousin, it can be either an xvalue or an lvalue - it has identity but it's unclear if this is the last reference to the variable / storage or not, hence it is unclear if it can or cannot be moved from.

Resolution Order

Where an overload exists that can accept either a const lvalue ref or rvalue ref, and an rvalue is passed, the rvalue is bound otherwise the lvalue version is used. (move for rvalues, copy otherwise).

Where it potentially happens

(assume all types are A where not mentioned)

It only occurs where an object is "initialized from an xvalue of the same type". xvalues bind to rvalues but are not as restricted as pure expressions. In other words, movable things are more than unnamed references, they can also be the 'last' reference to an object with respect to the compiler's awareness.

initialization

A a = std::move(b); // assign-move
A a( std::move(b) ); // construct-move

function argument passing

void f( A a );
f( std::move(b) );

function return

A f() {
    // A a exists, will discuss shortly
    return a;
}

Why it will not happen in f

Consider this variation on f:

void action1(A & a) {
    // alter a somehow
}

void action2(A & a) {
    // alter a somehow
}

A f(A && a) {
    action1( a );
    action2( a );
    return a;
}

It is not illegal to treat a as an lvalue within f. Because it is an lvalue it must be a reference, whether explicit or not. Every plain-old variable is technically a reference to itself.

That's where we trip up. Because a is an lvalue for the purposes of f, we are in fact returning an lvalue.

To explicitly generate an rvalue, we must use std::move (or generate an A&& result some other way).

Why it will happen in g

With that under our belts, consider g

A g(A a) {
    action1( a ); // as above
    action2( a ); // as above
    return a;
}

Yes, a is an lvalue for the purposes of action1 and action2. However, because all references to a only exist within g (it's a copy or moved-into copy), it can be considered an xvalue in the return.

But why not in f?

There is no specific magic to &&. Really, you should think of it as a reference first and foremost. The fact that we are demanding an rvalue reference in f as opposed to an lvalue reference with A& does not alter the fact that, being a reference, it must be an lvalue, because the storage location of a is external to f and that's as far as any compiler will be concerned.

The same does not apply in g, where it's clear that a's storage is temporary and exists only when g is called and at no other time. In this case it is clearly an xvalue and can be moved.


rvalue ref vs lvalue ref and safety of reference passing

Suppose we overload a function to accept both types of references. What would happen?

void v( A  & lref );
void v( A && rref );

The only time void v( A&& ) will be used per the above ("Where it potentially happens"), otherwise void v( A& ). That is, an rvalue ref will always attempt to bind to an rvalue ref signature before an lvalue ref overload is attempted. An lvalue ref should not ever bind to the rvalue ref except in the case where it can be treated as an xvalue (guaranteed to be destroyed in the current scope whether we want it to or not).

It is tempting to say that in the rvalue case we know for sure that the object being passed is temporary. That is not the case. It is a signature intended for binding references to what appears to be a temporary object.

For analogy, it's like doing int * x = 23; - it may be wrong, but you could (eventually) force it to compile with bad results if you run it. The compiler can't say for sure if you're being serious about that or pulling its leg.

With respect to safety one must consider functions that do this (and why not to do this - if it still compiles at all):

A & make_A(void) {
    A new_a;
    return new_a;
}

While there is nothing ostensibly wrong with the language aspect - the types work and we will get a reference to somewhere back - because new_a's storage location is inside a function, the memory will be reclaimed / invalid when the function returns. Therefore anything that uses the result of this function will be dealing with freed memory.

Similarly, A f( A && a ) is intended to but is not limited to accepting prvalues or xvalues if we really want to force something else through. That's where std::move comes in, and let's us do just that.

The reason this is the case is because it differs from A f( A & a ) only with respect to which contexts it will be preferred, over the rvalue overload. In all other respects it is identical in how a is treated by the compiler.

The fact that we know that A&& is a signature reserved for moves is a moot point; it is used to determine which version of "reference to A -type parameter" we want to bind to, the sort where we should take ownership (rvalue) or the sort where we should not take ownership (lvalue) of the underlying data (that is, move it elsewhere and wipe the instance / reference we're given). In both cases, what we are working with is a reference to memory that is not controlled by f.

Whether we do or not is not something the compiler can tell; it falls into the 'common sense' area of programming, such as not to use memory locations that don't make sense to use but are otherwise valid memory locations.

What the compiler knows about A f( A && a ) is to not create new storage for a, since we're going to be given an address (reference) to work with. We can choose to leave the source address untouched, but the whole idea here is that by declaring A&& we're telling the compiler "hey! give me references to objects that are about to disappear so I might be able to do something with it before that happens". The key word here is might, and again also the fact that we can explicitly target this function signature incorrectly.

Consider if we had a version of A that, when move-constructing, did not erase the old instance's data, and for some reason we did this by design (let's say we had our own memory allocation functions and knew exactly how our memory model would keep data beyond the lifetime of objects).

The compiler cannot know this, because it would take code analysis to determine what happens to the objects when they're handled in rvalue bindings - it's a human judgement issue at that point. At best the compiler sees 'a reference, yay, no allocating extra memory here' and follows rules of reference passing.

It's safe to assume the compiler is thinking: "it's a reference, I don't need to deal with its memory lifetime inside f, it being a temporary will be removed after f is finished".

In that case, when a temporary is passed to f, the storage of that temporary will disappear as soon as we leave f, and then we're potentially in the same situation as A & make_A(void) - a very bad one.

An issue of semantics...

std::move

The very purpose of std::move is to create rvalue references. By and large what it does (if nothing else) is force the resulting value to bind to rvalues as opposed to lvalues. The reason for this is a return signature of A& prior to rvalue references being available, was ambiguous for things like operator overloads (and other uses surely).

Operators - an example

class A {
    // ...
  public:
    A & operator= (A & rhs); // what is the lifetime of rhs? move or copy intended?
    A & operator+ (A & rhs); // ditto
    // ...
};

int main() {
    A result = A() + A(); // wont compile!
}

Note that this will not accept temporary objects for either operator! Nor does it make sense to do this in the case of object copy operations - why do we need to modify an original object that we are copying, probably in order to have a copy we can modify later. This is the reason we have to declare const A & parameters for copy operators and any situation where a copy is to be taken of the reference, as a guarantee that we are not altering the original object.

Naturally this is an issue with moves, where we must modify the original object to avoid the new container's data being freed prematurely. (hence "move" operation).

To solve this mess along comes T&& declarations, which are a replacement to the above example code, and specifically target references to objects in the situations where the above won't compile. But, we wouldn't need to modify operator+ to be a move operation, and you'd be hard pressed to find a reason for doing so (though you could I think). Again, because of the assumption that addition should not modify the original object, only the left-operand object in the expression. So we can do this:

class A {
    // ...
  public:
    A & operator= (const A & rhs); // copy-assign
    A & operator= (A && rhs); // move-assign
    A & operator+ (const A & rhs); // don't modify rhs operand
    // ...
};

int main() {
    A result = A() + A(); // const A& in addition, and A&& for assign
    A result2 = A().operator+(A()); // literally the same thing
}

What you should take note of here is that despite the fact that A() returns a temporary, it not only is able to bind to const A& but it should because of the expected semantics of addition (that it does not modify its right operand). The second version of the assignment is clearer why only one of the arguments should be expected to be modified.

It's also clear that a move will occur on the assignment, and no move will occur with rhs in operator+.

Separation of return value semantics and argument binding semantics

The reason that there is only one move above is clear from the function (well, operator) definitions. What's important is we are indeed binding what is clearly an xvalue / rvalue, to what is unmistakably an lvalue in operator+.

I have to stress this point: there is no effective difference in this example in the way that operator+ and operator= refer to their argument. As far as the compiler is concerned, within either's function body the argument is effectively const A& for + and A& for =. The difference is purely in constness. The only way in which A& and A&& differ is to distinguish signatures, not types.

With different signatures come different semantics, it's the compiler's toolkit for distinguishing certain cases where there otherwise is no clear distinction from the code. The behavior of the functions themselves - the code body - may not be able to tell the cases apart either!

Another example of this is operator++(void) vs operator++(int). The former expects to return its underlying value before an increment operation and the latter afterwards. There is no int being passed, it's just so the compiler has two signatures to work with - there is just no other way to specify two identical functions with the same name, and as you may or may not know, it is illegal to overload a function on just the return type for similar reasons of ambiguity.

rvalue variables and other odd situations - an exhaustive test

To understand unambiguously what is happening in f I've put together a smorgasbord of things one "should not attempt but look like they'd work" that forces the compiler's hand on the matter almost exhaustively:

void bad (int && x, int && y) {
  x += y;
}
int & worse (int && z) {
  return z++, z + 1, 1 + z;
}
int && justno (int & no) {
  return worse( no );
}
int num () {
  return 1;
}
int main () {
  int && a = num();
  ++a = 0;
  a++ = 0;
  bad( a, a );
  int && b = worse( a );
  int && c = justno( b );
  ++c = (int) 'y';
  c++ = (int) 'y';
  return 0;
}

g++ -std=gnu++11 -O0 -Wall -c -fmessage-length=0 -o "src\\basictest.o" "..\\src\\basictest.cpp"

..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:5:17: warning: right operand of comma operator has no effect [-Wunused-value]
   return z++, z + 1, 1 + z;
                 ^
..\src\basictest.cpp:5:26: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
   return z++, z + 1, 1 + z;
                          ^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:8:20: error: cannot bind 'int' lvalue to 'int&&'
   return worse( no );
                    ^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
 int & worse (int && z) {
       ^
..\src\basictest.cpp: In function 'int main()':
..\src\basictest.cpp:16:13: error: cannot bind 'int' lvalue to 'int&&'
   bad( a, a );
             ^
..\src\basictest.cpp:1:6: error:   initializing argument 1 of 'void bad(int&&, int&&)'
 void bad (int && x, int && y) {
      ^
..\src\basictest.cpp:17:23: error: cannot bind 'int' lvalue to 'int&&'
   int && b = worse( a );
                       ^
..\src\basictest.cpp:4:7: error:   initializing argument 1 of 'int& worse(int&&)'
 int & worse (int && z) {
       ^
..\src\basictest.cpp:21:7: error: lvalue required as left operand of assignment
   c++ = (int) 'y';
       ^
..\src\basictest.cpp: In function 'int& worse(int&&)':
..\src\basictest.cpp:6:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^
..\src\basictest.cpp: In function 'int&& justno(int&)':
..\src\basictest.cpp:9:1: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^

01:31:46 Build Finished (took 72ms)

This is the unaltered output sans build header which you don't need to see :) I will leave it as an exercise to understand the errors found but re-reading my own explanations (particularly in what follows) it should be apparent what each error was caused by and why, imo anyway.

Conclusion - What can we learn from this?

First, note that the compiler treats function bodies as individual code units. This is basically the key here. Whatever the compiler does with a function body, it cannot make assumptions about the behavior of the function that would require the function body to be altered. To deal with those cases there are templates but that's beyond the scope of this discussion - just note that templates generate multiple function bodies to handle different cases, while otherwise the same function body must be re-usable in every case the function could be used.

Second, rvalue types were predominantly envisioned for move operations - a very specific circumstance that was expected to occur in assignment and construction of objects. Other semantics using rvalue reference bindings are beyond the scope of any compiler to deal with. In other words, it's better to think of rvalue references as syntax sugar than actual code. The signature differs in A&& vs A& but the argument type for the purposes of the function body does not, it is always treated as A& with the intention that the object being passed should be modified in some way because const A&, while correct syntactically, would not allow the desired behavior.

I can be very sure at this point when I say that the compiler will generate the code body for f as if it were declared f(A&). Per above, A&& assists the compiler in choosing when to allow binding a mutable reference to f but otherwise the compiler doesn't consider the semantics of f(A&) and f(A&&) to be different with respect to what f returns.

It's a long way of saying: the return method of f does not depend on the type of argument it receives.

The confusion is elision. In reality there are two copies in the returning of a value. First a copy is created as a temporary, then this temporary is assigned to something (or it isn't and remains purely temporary). The second copy is very likely elided via return optimization. The first copy can be moved in g and cannot in f. I expect in a situation where f cannot be elided, there will be a copy then a move from f in the original code.

To override this the temporary must be explicitly constructed using std::move, that is, in the return statement in f. However in g we're returning something that is known to be temporary to the function body of g, hence it is either moved twice, or moved once then elided.

I would suggest compiling the original code with all optimizations disabled and adding in diagnostic messages to copy and move constructors to keep tabs on when and where the values are moved or copied before elision becomes a factor. Even if I'm mistaken, an un-optimized trace of the constructors / operations used would paint an unambiguous picture of what the compiler has done, hopefully it will be apparent why it did what it did as well...

这篇关于返回由右值引用传递的参数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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