An rvalue reference can be bound to two things:
- an xvalue, in which case we're simply referring to something, but the type of reference tells us that we can move from it, if we want to
- a prvalue, in which case we're materializing something
In case you're not familiar with value categories or need a quick refresher, here's a quick overview:
|
movable |
immovable |
has identity |
xvalue - std::move(x) , etc. |
lvalue - x , "str" , etc. |
anonymous |
prvalue - 1 + 2 , sqrt(2) , etc. |
|
xvalues and prvalues are collectively called rvalues, and these are the things that an rvalue reference can bind to.
Rvalue references to xvalues
// (a) returning an rvalue reference produces an xvalue
std::string&& t();
std::string&& a = t();
// (b) elements of an rvalue array are xvalues
std::string array_of_strings[N];
std::string&& b = std::move(array_of_strings)[0];
// (c) members of an rvalue struct are xvalues
struct wrapper { std::string str; };
wrapper wrap;
std::string&& c = std::move(wrap).str;
// ...
In such cases, we are just binding a reference to some other object.
If we use a
, b
, or c
, they are lvalues in most contexts, and there is little difference compared to an lvalue reference like std::string&
.
However, the type carries the information that a
, b
, and c
refer to something movable, and we can use std::move
or std::forward
to turn the reference back into an xvalue. This would enable passing references around, and then calling move constructors later.
Returning rvalue references
When an rvalue reference is returned from a function (e.g. std::move
), it turns into an xvalue. That's how rvalue references to xvalues are created most of the time.
Here are a few more examples:
std::forward
sometimes returns rvalue reference if it was called with an rvalue
std::move_iterator
s have a *
operator that returns an rvalue reference
std::get(std::tuple)
returns an rvalue reference to the tuple member when the tuple is an rvalue
See also: Does it make sense for a function to return an rvalue reference?
Rvalue references to prvalues
// returns an object, so t() is prvalue
std::string t();
std::string&& r = t(); // materialize temporary object returned by t()
In this case, we are referring to the temporary object returned by t()
.
This code looks wrong at first glance, but is actually valid, because the lifetime of that temporary object is extended to the scope of r
.
Temporary materialization is done because it makes generic programming easier. We often create rvalue references when using forwarding references (e.g. auto&&
).
// this works regardless of whether the * operator of the iterator returns:
// - prvalue, e.g. std::vector<bool>::reference
// - lvalue, e.g. int&
// - xvalue, e.g. int&& from a std::move_iterator
for (auto&& e : container)
{
use(e); // TODO: perfect forwarding, if necessary
}
Without temporary materialization, this code would have undefined behavior when e
is initialized to a prvalue, because we would immediately create a dangling reference.
Outside of that use case, it makes no sense, because materializing isn't any better than just storing an object.
Materializing is actually worse for performance (see below).
Note: temporary materialization also happens for rvalue reference function parameters, in which case it is helpful.
Pessimization
Note that materializing a prvalue through an rvalue reference can't make our code any faster; it's not an optimization trick. It's always better to just pass through prvalues when possible. For example:
std::string t();
void consume(std::string s);
// BAD, results in one extra move constructor call
std::string&& r = t();
consume(std::move(r));
// GOOD, the result of t() and and the argument to consume() are the same
// object, thanks to mandatory copy elision
consume(t());
Examples like these are why you don't materialize temporary values if you don't have to. Creating rvalue references is done out of necessity, not because it improves performance or code quality.
See also: Is it useless to declare a local variable as rvalue-reference, e.g. T&& r = move(v)?
t
is. Ist
a function? How is the function declared (more specifically its return value)? Or ist
a type? – Pyrone