Jonathan Boccara's blog

Understanding lvalues, rvalues and their references

Published February 6, 2018 - 7 Comments

lvalue rvalue references C++

Daily C++

Even though rvalue references have been around since C++11, I’m regularly asked questions about how they work and how to use them. For this reason I’m going to explain my understanding of them here.

I think this is relevant to the topic of Fluent C++, expressive code in C++, because not understanding them adds a layer of confusion over a piece of code that tries to tell you its meaning.

Why am I writing this here? Indeed, you can read about rvalue references in C++ reference books and even on other blog posts on the Internet, and my purpose is not to duplicate them.

Rather, I will explain what helped me understand them. Indeed, I used to be very confused about them at the beginning, and this is because I was missing just a couple of key pieces of information. In particular one that I detail in the third section of this post.

If you find yourself confused about lvalues, rvalues and their references, this article is for you. And if you master them already, I hope you’ll be kind enough to ring the bell if by chance you were to spot any meestayck.

About that, I’m very grateful to Stephan T. Lavavej for taking the time (once again!) to signal the errors that he saw in the post.

What is an lvalue and what is an rvalue?

In C++, every expression is either an lvalue or an rvalue:

  • an lvalue denotes an object whose resource cannot be reused, which includes most objects that we can think of in code. Lvalues include expressions that designate objects directly by their names (as in int y = f(x)x and y are object names and are lvalues), but not only. For instance, the expression myVector[0] also is an lvalue.
  • an rvalue denotes an object whose resource can be reused, that is to say a disposable object. This typically includes temporary objects as they can’t be manipulated at the place they are created and are soon to be destroyed. In the expression g(MyClass()) for instance, MyClass() designates a temporary object that g can modify without impacting the code surrounding the expression.

Now an lvalue reference is a reference that binds to an lvalue. lvalue references are marked with one ampersand (&).
And an rvalue reference is a reference that binds to an rvalue. rvalue references are marked with two ampersands (&&).

Note that there is one exception: there can be lvalue const reference binding to an rvalue. Anyway, let’s not worry about this case just now, let’s focus on the big picture first.

What is this all for?

rvalue references add the possibility to express a new intention in code: disposable objects. When someone passes it over to you (as a reference), it means they no longer care about it.

For instance, consider the rvalue reference that this function takes:

void f(MyClass&& x)
{
    ...
}

The message of this code to f is this: “The object that x binds to is YOURS. Do whatever you like with it, no one will care anyway.” It’s a bit like giving a copy to f… but without making a copy.

This can be interesting for two purposes: improving performance (see move constructors below) and taking over ownership (since the object the reference binds to has been abandoned by the caller – as in std::unique_ptr)

Note that this could not be achieved with lvalue references. For example this function:

void f(MyClass& x)
{
    ...
}

can modify the value of the object that x binds to, but since it is an lvalue reference, it means that somebody probably cares about it at call site.

I mentioned that lvalue const references could bind to rvalues:

void f(MyClass const& x)
{
    ...
}

but they are const, so even though they can bind to a temporary unnamed object that no one cares about, f can’t modify it.

THE one thing that made it all click for me

Okay, there is one thing that sounds extra weird, but that makes sense given the definitions above: there can be rvalue references that are themselves lvalues.

One more time: there can be rvalue references that are themselves lvalues.

Indeed, a reference is defined in a certain context. Even though the object it refers to may be disposable in the context it has been created, it may not be the case in the context of the reference.

Let’s see this in an example. Consider x in the following code:

void f(MyClass&& x)
{
    ...
}

Within f, the expression “x” is an lvalue, since it designates the name of an object. And indeed, if some code inside of f modifies x, the remaining code of f will certainly notice. In the context of f, x is not a disposable object.

But x refers to an object that is disposable in the context that called f. In that sense, it refers to a disposable object. This is why its type has && and is a rvalue reference.

Here is a possible call site for f:

f(MyClass());

The rvalue expression MyClass() denotes a temporary, disposable object. f takes a reference to that disposable object. So by our definition this is an rvalue reference. However this doesn’t prevent the expression denoting this reference from being an object name, “x”, so the reference expression itself is an lvalue.

Note that we cannot pass an lvalue to f, because an rvalue reference cannot bind to an lvalue. The following code:

MyClass x;
f(x);

triggers this compilation error:

error: cannot bind rvalue reference of type 'MyClass&&' to lvalue of type 'MyClass'
f(x);
   ^

Understanding this made a big difference for me: an lvalue expression can designate an rvalue reference. If this doesn’t sound crystal clear yet, I suggest you read this section over one more time before moving on.

There is a way to call f with our lvalue x: by casting it explicitly into an rvalue reference. This is what std::move does:

MyClass x;
f(std::move(x));

So when you std::move an lvalue, you need to be sure you won’t use it any more, because it will be considered like a disposable object by the rest of the code.

r-value reference

Movable objects

In practice we don’t encounter that many functions accepting rvalue references (except in template code, see below). There is one main case that accepts one though: move constructors:

class MyClass
{
public:
    // ...
    MyClass(MyClass&& other) noexcept;
};

Given what we’ve seen so far, we have all the elements to understand the meaning of this constructor. It builds an object using another one, like the copy constructor but, unlike in the copy constructor, no one cares about the object it is passed.

Using this information can allow the constructor to operate faster. Typically, an std::vector will steal the address of the memory buffer of the passed object, instead of politely allocating a new memory space and copying every elements over to it.

It also allows transferring ownership, like with std::unique_ptr.

Note that objects can also be assigned to from disposable instances, with the move assignment operator:

class MyClass
{
public:
    // ...
    MyClass& operator=(MyClass&& other) noexcept;
};

Even if this looks like the panacea for performance issues, let’s keep in mind the guideline in Effective Modern C++‘s Item 29 which is that when you don’t know a type (like in generic code) assume that move operations are not present, not cheap and not used.

The case of templates

rvalue references have a very special meaning with templates. What made me understand how this works is the various talks and book items of Scott Meyers on this topic. So I will only sum it up, also because if you understood everything until now, there is not that much more here. And for more details I suggest you read Items 24 and 28 of Effective Modern C++.

Consider the following function:

template<typename T>
void f(T&& x)
{
    ...
}

x is an lvalue, nothing to question about that.

But even if it looks like it is an rvalue reference (it has &&), it may not be. In fact, by a tweak in template argument deduction, the following happens:

  • x is an lvalue reference if f received an lvalue, and
  • x is an rvalue reference if f received an rvalue.

This is called a forwarding reference or a universal reference.

For this to work though, it has to be exactly T&&. Not std::vector<T>&&, not const T&&. Just T&& (Well, the template parameter can be called something else than T of course).

Now consider the following code:

template<typename T>
void g(T&& x)
{
    ...
}

template<typename T>
void f(T&& x)
{
    g(x);
}

g also receives a forwarding reference. But it will always be an lvalue reference, regardless of what was passed to f. Indeed, in the call g(x), “x” is an lvalue because it is an object name. So the forwarding reference x in void g(T&& x) is an lvalue reference.

To pass on to g the value with the same reference type as that was passed to f, we need to use std::forward:

template<typename T>
void g(T&& x)
{
    ...
}

template<typename T>
void f(T&& x)
{
    g(std::forward<T>(x));
}

std::forward keeps the reference type of x. So:

  • if x is an rvalue reference then std::forward does the same thing as std::move,
  • and if x is an lvalue reference then std::forward doesn’t do anything.

This way the x in g will have the same reference type as the value initially passed to f.

This technique is called “perfect forwarding”.

An illustrating example: std::make_unique

Let’s see an example, with the implementation of std::make_unique. This helper function from the C++ standard library takes somes arguments and uses them to construct an object on the heap and wrap it into a std::unique_ptr.

Here is its implementation:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

(As observed by /r/Xeverous on Reddit, note this is not the exact official implementation as it doesn’t cover all cases, in particular it should prevent an array with known bounds from compiling).

Note how the arguments args passed on to the constructor of T:

T(std::forward<Args>(args)...)

Indeed, for all we know, T could have several constructors that accept lvalue references or rvalue references. The purpose of make_unique is to hide the call to new but to pass on the arguments just like if we had passed them ourselves to new.

Here std::forward allows to keep the reference type of the arguments.

That’s pretty much it… for an introduction

There is more to the subject, like reference types in methods prototypes, when and how move constructors are generated by the compiler, or how move constructors should avoid throwing exceptions and what implications this has, on std::vector in particular. You could look up a reference (hey what a pun) book for more about this.

But I hope the fundamental concepts are here. Those are the keys that made me understand lvalues, rvalues and their references and I hope that, with these keys, you can understand this topic more quickly than I did. And that it will be one less thing to figure out for you when you read code.

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed