Is an xvalue's lifetime extended when it is bound to a const lvalue reference?
Asked Answered
E

2

8

If I write the following code:

#include <iostream>

using namespace std;

int main()
{
  cout << &(int &&)123 << endl;
  return 0;
}

Then g++ complains:

foo.cc: In function ‘int main()’:
foo.cc:7:20: error: taking address of xvalue (rvalue reference)

Ok, thanks to What are rvalues, lvalues, xvalues, glvalues, and prvalues? I get that an xvalue means that it's about to "expire", which makes sense. But now if I do this:

#include <iostream>

using namespace std;

int main()
{
  const int &x = (int &&)123;
  cout << &x << endl;
  return 0;
}

This "works" just fine and will print an address. So, I have a few questions:

  1. If the value is about to expire, why can I make a reference to it? The reference won't keep the original object alive (right?).
  2. Does such a reference result in undefined behavior? E.g. because we're referencing an object that may have been destroyed?

In general is there a way to know the lifetime of an rvalue reference?

Eula answered 27/2, 2012 at 15:47 Comment(7)
Binding to a const reference will keep the value alive.Adduction
Yes. you can only have a const ref and a ordinary ref. I think this is allowed for backward compatibility with c++03 where you could bind the const ref to a object returned by value.Casares
@BoP I think you have it wrong.Georgianngeorgianna
@Johannes - In that case, I'm not sure. Otherwise I would be. :-)Adduction
Literals, except for string literals, are prvalues, not xvalues. String literals are lvalues.Parlormaid
Perhaps 'xvalue' isn't the right term to use in the question. For example, the result of std::move will not have its lifetime extended, but apparently "std::move obtains an rvalue reference to its argument and converts it to an xvalue. ". Are you asking about all xvalues, or just prvalues directly casted to &&?Nonparous
@AaronMcDaid, aren't literals already prvalues? So casting to && should be a no-op, shouldn't it?Parlormaid
A
2

Clause 12.2, paragraphs 4-5, says that the lifetime is extended in the second example

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. ...

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:
(none of the exceptions apply here)

Adduction answered 27/2, 2012 at 16:21 Comment(1)
I don't think "when a reference is bound to a temporary" is supposed to cover binding a reference to an xvalue which is bound to a temporaryUpgrowth
G
10

Perhaps I'm alone but I think that this is not safe:

const int &x = (int &&)123;

The lifetime-lengthening rules apply only when the reference is initialized by an expression that directly refers to a temporary object. In other words, the expression has to carry the "temporary" attribute. If you omit the (int&&), then for binding to the reference, the compiler will implicitly create a prvalue expression that refers to a temporary object which is initialized by the value 123 and lifetime lengthening then applies.

But if you mix rvalue references in between, the compiler has no way to know at compile time whether or not to lengthen the lifetime of the referred-to object.

int a = 0;
const int &x = ((rand() == 42) ? (int&&)123 : (int&&)a);

I therefor think that your code has undefined behavior, because you evaluate a dangling reference (even if only for taking its address).

Georgianngeorgianna answered 27/2, 2012 at 22:9 Comment(6)
Interesting -- I tried the code snipped you suggested in g++ and it had no complaints. Can the compiler just decide to extend the lifetime of both? Or is there a requirement that the one is destroyed after evaluation?Eula
@fatal the compiler is not required to complain on undefined behavior. if you try your code with a class that has a destructuor you will observe that gcc destroys the temporary early .Georgianngeorgianna
Wouldn't the compiler have the same difficulty with the apparently legal const int &x = ((rand() == 42) ? 123 : a);?Lyrism
@DanNissenbaum That case is fundamentally different. With the ternary operator, if 2nd and 3rd operand have the same type and value category then the result has that same type and value category. In the original example , both are xvalues so the ternary operator evaluates to an xvalue. But in your case the operands are different category (123 is a prvalue, a is an lvalue). In the case of built-in types, the result is a prvalue, so a temporary (of type const int) is always created from this prvalue and bound to x.Upgrowth
Is there any difference between (int &&) 123 and std::move(123)? In the latter case, the 123 is clearly going to be discarded quite soon.Nonparous
Well, I did try this on a struct R with a destructor and got interesting results. R const & r = (R&&) R(); compiles, runs and the temporary's destructor is not called until r goes out of scope. However, in the case of R const & r = std::move(R()); the destructor is called at the end of the statement. This in g++ 4.9.Parlormaid
A
2

Clause 12.2, paragraphs 4-5, says that the lifetime is extended in the second example

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. ...

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:
(none of the exceptions apply here)

Adduction answered 27/2, 2012 at 16:21 Comment(1)
I don't think "when a reference is bound to a temporary" is supposed to cover binding a reference to an xvalue which is bound to a temporaryUpgrowth

© 2022 - 2024 — McMap. All rights reserved.