The deal with C++14 xvalues
I recently published a C++14 value category cheat-sheet, which was created to accompany a tech talk I gave at work. For the most part, examples on prvalues and lvalues were understood with minimal confusion. However, xvalues presented the biggest hurdle for existing C++ programmers. I’d like to explore, in greater detail, why C++11 introduced xvalues and how they are essential to safe and efficient programming in modern C++.
Rvalue references
To start, let’s cover a very handy feature, introduced in C++11: the ability to bind rvalues to references-to-non-const. Since this was previously invalid, like:
int &a{ 0 }; // MSVC accepting this doesn't make it valid C++
We needed a new way of representing this sort of reference. The syntax was extended to support this notation:
int &&a{ 0 };
This unique syntax restricts the initialization of the lvalue a
to only accept rvalues. For example, this would be invalid:
int a{};
int &&b{ a }; // compiler error
A new way was introduced, in C++11, to get an rvalue from an lvalue: std::move
. By moving, you can transition a named value (an lvalue) to being an rvalue. More specifically, since it can’t be a prvalue (it has a name!), it ends up being an xvalue.
int a{};
int &&b{ std::move(a) };
Why would this matter though? Getting an rvalue reference to an int is useless! It does indeed matter, since C++11 also introduced move semantics, including move constructors and move assignment operators. So, now consider the following:
std::string s{ "meow" };
std::string v{ s }; // copies s
std::string m{ std::move(s) }; // takes in an rvalue and moves from it!
// s is now in an undefined, but valid, state
// m is now "meow"
These move semantics allow us to rip apart rvalues when constructing other objects. All of the sudden, the ability to tag an lvalue as an rvalue becomes so much more useful. You can add some type information, just by using std::move
, to any lvalue and the compiler may choose a more optimal constructor or assignment for you. Behind the scenes, containers like std::vector
are already using this for relocating their data. How might this look?
struct string
{
string(char const *str) // Typical ctor, ignoring error handling for brevity
{
auto const len(::strlen(str));
data = std::make_unique<char[]>(len);
std::copy_n(str, len, data.get());
}
string(string const &s) // Copy ctor delegates and deep copies
: string(s.data.get())
{ }
string(string &&s) // Move ctor steals the data; no deep copy
: data{ std::move(s.data) }
{ }
std::unique_ptr<char[]> data;
};
string s{ "meow" };
string f{ s }; // deep copy
string m{ std::move(s) }; // moved out of s; no deep copy
std::unique_ptr
, which cannot be copied and can only be moved, relies entirely on the existence of xvalues and std::move
in order for it to transfer ownership. This is both a safety feature and a performance feature. For safety, the compiler ensures that a unique_ptr
only has one owner at a time; transferring that ownership must also be explicitly done. For performance, tagging an lvalue as an xvalue can optimize the data transfer, allowing the compiler to choose the move constructor/assignment operator. These rvalue references can also open up the door of perfect forwarding, to help eliminate needless copying through layers of abstraction (as found in std::bind
).
Forwarding references (AKA universal references)
As a side effect of rvalue references being added, and a more complex subtlety in the nature of the infectious behavior of lvalue references, there exists a phenomenon called forwarding references. Forwarding references rely on the behavior of reference collapsing, and allow a catch-all implementation that works for both lvalues and rvalues, even both const and non-const. Why would you want this? Well, you’re already using it, whenever you std::make_shared
:
// example implementation
template <typename T, typename... Args>
std::shared_ptr<T> make_shared(Args &&...args)
{ return std::shared_ptr<T>{ new T{ std::forward<Args>(args)... } }; }
This make_shared
function has perfect forwarding features. That is, when I give it an rvalue, the constructor of T
gets an rvalue, too. Without forwarding references, this wouldn’t be possible. Instead, an rvalue passed to make_shared
would become an lvalue and stay that way. Without forwarding references, it’d look like this:
template <typename T, typename... Args>
std::shared_ptr<T> make_shared(Args const &...args)
{ return std::shared_ptr<T>{ new T{ args... } }; }
All of the args go in as references-to-const, which means that make_shared
still accepts rvalues, but now the T
constructors is only going to get lvalues. Where might this break? Well, what if something needs to be an rvalue in order to change ownership? Say, std::unique_ptr
has a that quality! What if we call our make_shared
with just a prvalue std::unique_ptr<int>
?
struct foo
{
// Expects a moved ptr, since these guys can't be copied
foo(std::unique_ptr<int> ptr)
{ }
};
auto const u(make_shared<foo>(std::unique_ptr<int>{}));
If we use perfect forwarding, no problem! Our original prvalue std::unique_ptr<int>
will remain a prvalue, all the way until it hits the foo
constructor. However, if we use the second make_shared
implementation, our args will all become lvalues. Unfortunately, this code would no longer compile; there’s no way to copy std::unique_ptr<int>
! If you’re using copyable types, the code may still compile, but it’s needlessly inefficient and will result in extra copies being made.
The details of how the reference collapsing works with forwarding references is worthy of its own post entirely – not surprisingly, Meyers has a great talk on the subject. For now, hopefully this covers why the feature exists, at least, and how you’ve probably already used it.