What does it mean to have an rvalue reference variable?
Asked Answered
O

2

5

I think I understand functions with signatures like:

void f(std::string&&):

It will be applied to rvalues to reuse their resources. However, I've seen occasionally code like:

std::string t();

std::string&& s = t();

Where a variable is initialised as an rvalue ref. (Here I've written t to return by value but if the behaviour differs when returning by l/rvalue reference be interested to know).

Typically, I see this in posts/puzzles about C++ rather than production code.

I have a few things I'd like to understand about it:

  1. what are the rules for this behaves?
  2. what advantages does this give you over capturing by value or lvalue const-reference? It seems to me that if you capture by value the move/copy will be elided and you'll be able to modify it (unlike const-ref). I may be incorrect about that, but if it is the case I can't see what benefit rvalue reference will have here
  3. does this behave differently when t returns an rvalue-reference? What are good reasons to return an rvalue reference? I guess std::move does it but when else would it be beneficial?
Overmaster answered 30/6, 2023 at 18:10 Comment(2)
This kind of depends on context, and what t is. Is t a function? How is the function declared (more specifically its return value)? Or is t a type?Pyrone
Thanks for replying - I've added some detail to the original post. I had imagined a function but didn't know the return value mattered.Overmaster
T
9

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_iterators 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)?

Theocritus answered 30/6, 2023 at 18:42 Comment(1)
@user17732522 I've clarified that I mean raw arrays, not std::array. std::array doesn't have the rvalue overload on its operator[] function and never returns rvalue references, but this feature might be added in the future. If so, it would work for std::array too.Theocritus
D
2
  1. what are the rules for this behaves?

If t returns by value, you obtain a copy of whatever was returned (though the copy may be elided). The rvalue reference extends the lifetime of that copy, but provides no benefits over a normal variable. See also.

If t returns by lvalue reference, the code does not compile because a rvalue reference cannot bind to it.

If t returns by rvalue reference, you obtain a reference to whatever was returned. If t returns a local variable, then you get a dangling reference, since that variable is gone after the call. Otherwise, the reference you get behaves more or less like a lvalue reference, as if t returned by lvalue reference instead.

  1. what advantages does this give you over capturing by value or lvalue const-reference? It seems to me that if you capture by value the move/copy will be elided and you'll be able to modify it (unlike const-ref). I may be incorrect about that, but if it is the case I can't see what benefit rvalue reference will have here

Don't mistake "capturing", used in the context of lambdas, with the simple process of assignment from a function call. Copy (and move) elision only applies if the function returns a local variable (or a local expression). If t returns something else, for example a value produced by dereferencing something, or a member variable of some other object that lives outside t, then there cannot be any copy elision. In these cases, returning by value produces a copy, while returning by reference does not.

  1. does this behave differently when t returns an rvalue-reference? What are good reasons to return an rvalue reference? I guess std::move does it but when else would it be beneficial?

For a free function, returning by rvalue reference makes little sense. The only use case I can think of is when the function receives an object from a parameter and returns it or something in it, and expects the caller to move from that. For a member function, it can be useful to just move a member variable for example. See also.

Demb answered 30/6, 2023 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.