akai
akai

Reputation: 2052

rvalue reference (expression) returned by function is xvalue - but no identity?

According to What are rvalues, lvalues, xvalues, glvalues, and prvalues? and some other explanations, my understanding is that xvalue is the expression which has identity and is safely moved (or is so marked).

Some texts like this and this say that, if a function f()'s return type is rvalue reference, then the expression f() is xvalue. For example:

int&& f() {
  return 1;
}

int main() {
  f(); // xvalue
  2; // prvalue
}

My confusion is that, because the origin of f() is the literal 1, for me f() doesn't seem to have an identity and thus I can't understand how it becomes xvalue. If 1 has identity, why is 2 said to have no identity and is prvalue? Does prvalue suddenly have "identity" if it's returned from a function as an rvalue reference?

EDIT

It's pointed out that f() returns a dangling reference, but I hope my point still makes sense.

EDIT2

Well, after reading the (very helpful) comments, it seems that it probably doesn't make sense?

Upvotes: 1

Views: 273

Answers (3)

HTNW
HTNW

Reputation: 29193

Does prvalue suddenly have "identity" if it's returned from a function as an rvalue reference?

Yes, actually. The standard pretty much says that outright:

[conv.rval]

A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object ([class.temporary]) of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object.

That temporary object, while it exists, most certainly has "identity". Of course, the result of such a conversion is no longer a prvalue, so perhaps we shouldn't say the prvalue "gets an identity." Note that this works, too, also because of temporary materialization:

(int&&)1; // This is different from f(), though, because that reference is dangling but I believe this one isn't (lifetime extension of a temporary by binding to a reference applies here but is suppressed for a return)

Note that the operand of a return statement and the thing that actually gets returned simply don't have to be the same thing. You give an int prvalue, you need an int xvalue, the return makes it work by materializing a temporary. It's not obliged to fail because of the mismatch. Unfortunately, that temporary immediately gets destroyed when the return statement ends, leaving the xvalue dangling, but, for that moment in between the returned reference being bound and the temporary being destroyed, yes, the rvalue reference indeed referred to an object with its own identity.

Other examples of prvalues being materialized so you can bind references to them:

int &&x = 1; // acts just like int x = 1 except for decltype and similar considerations
int const &y = 1; // ditto but const and also available in older C++ versions

// in some imaginary API
void install_controller(std::unique_ptr<controller> &&new_controller) {
     if(can_replace_controller()) current_controller = std::move(new_controller);
}
install_controller(std::make_unique<controller>("My Controller"));
// prvalue returned by std::make_unique materializes a temporary, binds to new_controller
// might be moved from, might not; in latter case new pointer (and thus object)
// is destroyed at full-expression end (at the semicolon after the function call)
// useful to take by reference so you can do something like
auto p = std::make_unique<controller>("Perseverant Controller");
while(p) { wait_for_something(); install_controller(std::move(p)); }

Other examples of return not being trivial:

double d(int x) { return x; }
// int lvalue given to return but double prvalue actually returned! the horror!

struct dangerous {
    dangerous(int x) { launch_missiles(); }
};
dangerous f() { return 1; }
// launch_missiles is called from within the return!

std::vector<std::string> init_data() {
    return {5, "Hello!"};
}
// now the operand of return isn't even a value/expression!
// also, in terms of temporaries, "Hello!" (char const[7] lvalue) decays to a
// char const* prvalue, converts to a std::string prvalue (initializing the
// parameter of std::string's constructor), and then that prvalue materializes
// into a temporary so that it can bind to the std::string const& parameter of
// std::vector<std::string>'s constructor

Upvotes: 2

HolyBlackCat
HolyBlackCat

Reputation: 96053

To be honest, I find the whole concept of "having identity" somewhat moot.

Here's how I tend to think about it:

  • A prvalue is an expression that creates an object.

  • An rvalue is an expression denoting a temporary object (or an object considered to be temporary, e.g. because it was std::moved).

  • An lvalue is an expression denoting a non-temporary object (or an object considered to be non-temporary).

A call to int &&f() {...} doesn't create a new object (at least if we ignore the function body, and only look at the function-calling mechanism itself), so the result is not a prvalue (but it's obviously an rvalue, thus it's also an xvalue).

A call to int f() {...}, on the other hand, unconditionally creates an object (the temporary int; regardless of the function body), so it's a prvalue.

Upvotes: 0

akai
akai

Reputation: 2052

Here I try to summarize my understanding after reading the given comments.

The whole purpose of returning an rvalue reference is to use it in some way, so returning an rvalue reference that points to a function local object, which is already invalid when the function returns, is not considered (well, I'm sure the committee does consider this of course, but not as an intended usage).

As a result, if I have a function T&& f() { /.../ return val; }, val is supposed to locate somewhere with its identity even after f() returns, otherwise it's dangling which is a mere error. Therefore, the intention that f() has an identity, so is an xvalue, is justified.

Upvotes: 0

Related Questions