By a strange quirk of fate, I have started writing C++ for a living.
Learning C++ was about as complicated as I think I expected it to be.
By line count, I’ve written a lot of C++ for programming competitions,
but I knew that I had only ever used a small cross-section of the
language: basic control flow and variables, STL containers and
algorithms, structs on which you mechanically define
bool operator<(const T& other) const
so STL
algorithms can order them, and the very occasional macro or templated
helper function. There were many features I wasn’t even aware
existed.
In the process of learning C++ professionally, one rabbit hole I fell into quickly was C++11’s defining feature, the rvalue reference, and how it can be used to implement move semantics and perfect forwarding. By poring over a copy of the widely recommended book Effective Modern C++, by Scott Meyers, and a few dozen StackOverflow answers and blog posts, I roughly understood it after a few days, but still had a sort of blind-men-feeling-the-elephant feeling. I was confused about what lay under some of the abstractions I had been using, unsure of the full shape of the pitfalls that some of the guides had pointed out to me, and generally uncomfortable that there were still many small variations of the code I had seen that I couldn’t predict the behavior of. It took many more days to work myself out of there, and I wished I had had a guide that explained rvalue references and their applications to a bit more depth than what might be necessary for day-to-day use. So here’s my attempt to explain rvalue references in my own fundamental I-want-to-know-how-things-work-no-really style.
(If this vision doesn’t resonate with you, there are many other posts explaining rvalue references out there that you might prefer. Feel free to just skim the executive summary and/or check out some of the linked articles in the Background section.)
Executive Summary
I got… pretty carried away when writing this post, and a lot of it is just for my own understanding, which may or may not be useful to readers. Here’s a much more concise rundown (assuming you know basic C++ already):
Every C++ expression is either an lvalue or rvalue. Roughly, it’s an lvalue if you can “take its address”, and an rvalue otherwise. For example, if you’ve declared a variable
int x;
, thenx
is an lvalue, but253
andx + 6
are rvalues. If you can assign to it, it’s definitely an lvalue.Rvalue references are a new kind of reference in C++11, declared with two
&&
s instead of one, e.g.int&& x;
The old single-&
kind are now called lvalue references. Lvalue references and rvalue references differ only in the rules surrounding their initialization, which includes when a function has a reference parameter and the compiler determines whether a certain argument is acceptable for that parameter. In particular, they do not differ when you use them in an expression: both kinds of references will be lvalues!The rules for initializing a reference are: You can only initialize a non-const lvalue reference to an lvalue, which makes sense since the reference has to refer to something. However, you can initialize a const lvalue reference to either an lvalue or an rvalue; if you provide an rvalue, it will implicitly declare a new variable, initialize it to that rvalue, and produce a const reference to that variable instead. And you can only initialize an rvalue reference to an rvalue, which will do the same thing. Also, you must be careful of the lifetime of the implicitly declared variable; if it expires too quickly, you end up with a dangling reference.
However, you can call
std::move
on an lvalue to produce an rvalue that can be “smuggled” into an rvalue reference. If you do so, no new implicit variable will be created; the rvalue reference will actually refer to the lvalue passed intostd::move
.Sometimes, you want to write a function taking an argument that can be implemented in one of two ways: a slow way that treats its argument as read-only, perhaps making a new copy, and returns something new, or an efficient way that clobbers its argument and reuses its resources to produce a return value. Loosely speaking, the latter type of behavior or the ability to offer it as an option is referred to as “moving” or move semantics. A popular way to support move semantics that automatically works with many types of client code is to overload the function as follows:
One overload will have an lvalue reference parameter and do the slow, argument-preserving thing; it’ll be called if the argument is an lvalue.
The other overload will have an rvalue reference parameter and do the efficient, argument-clobbering thing; it’ll be called if the argument is an rvalue.
This automatically does the right thing for most clients, but the standard library function
std::move
gives you an escape hatch whereby a caller can deliberately invoke the efficient, argument-clobbering overload on an lvalue that it’s OK with being clobbered. But if there aren’t function overloads that are “paying attention”,std::move
doesn’t do anything by itself.Although you can’t write such a type directly, the new reference collapsing rules in C++11 states that a reference to a reference is just a reference. In particular,
T& && = T&
. So ifT
is a type variable,T&&
can be either an lvalue reference or an rvalue reference, and if a templated function has a parameter typeT&&
, a natural value forT
can be inferred for any argument depending on if it’s an lvalue or an rvalue. This enables you to write a templated function that can be called with any arguments, is aware of whether its arguments are lvalues and rvalues, and can forward those arguments to another function while preserving both their type and their lvalue/rvalue-ness. However, you will need to callstd::forward
to reconstruct the lvalue/rvalue-ness. This is called perfect forwarding and typically looks like this:
template<typename... Ts> void g(Ts&&... v) {
(std::forward<Ts>(v)...);
f}
Read on for the long, detailed version with (way) more examples and links.
Background
I assume you know simple C++, understand and are comfortable with pointers and in particular how pointers can dangle, and understand references, templates, classes, and constructors on at least a basic level. If you understand why the below function doesn’t work, how to fix it, and how to change it so it also applies to vectors that hold any numeric type, you should be good.
void addSixToAll(vector<int> vec) {
for (int x : vec) {
+= 6;
x }
}
// usage:
<int> v = {2, 5, 3};
vector(v);
addSixToAll// v should now contain {8, 11, 9}
In addition, this post will make the most sense if you already understand, on a high level, why move semantics and perfect forwarding are nice features to have in C++; I will discuss them briefly but not try particularly hard to motivate them. Some other posts that cover overlapping material and do motivate them:
For motivating move semantics:
- Triangles, of InternalPointers, C++
rvalue references and move semantics for beginners, motivates move
semantics with a standard managed-
int[]
class. The predecessor Understanding the meaning of lvalues and rvalues in C++ also covers some of the same material. - Eli Bendersky, Understanding lvalues and rvalues in C and C++, motivates move semantics with the standard int vector example.
- Triangles, of InternalPointers, C++
rvalue references and move semantics for beginners, motivates move
semantics with a standard managed-
For motivating perfect forwarding:
- Eli Bendersky, Perfect forwarding and universal references, motivates perfect forwarding through vector “emplacement” and variants.
- oopscene, C++11: Perfect forwarding, motivates perfect forwarding through a higher-order function and variants.
For both:
- Thomas Becker, C++ Rvalue References Explained, motivates move semantics with a generic resource-holding class and perfect forwarding with a generic factory function.
We’re also going to do the pretentious language-lawyer thing where we
differentiate parameters from arguments, because the difference will
matter. Parameters are the variables that a function is
declared as taking and that it can use in its function body;
arguments are the expressions that you actually pass
into a function to call it. Below, param1
and
param2
are the parameters to f
, and
arg1
and arg2_1 + arg2_2
are the arguments
it’s called with. Note already from this example that that parameters
are variables, but arguments are expressions that can be variables or
can be more complicated.
int f(int param1, int param2) {
return param1 + param2;
}
// ...
(arg1, arg2_1 + arg2_2); f
Finally, we’ll be using C++11. If you choose to compile along at
home, make sure to pass -std=c++11
or otherwise specify the
C++ edition to your compiler! I didn’t and was really confused when
certain code snippets didn’t do what I expected them to do. Shows how
narrow my C++ knowledge was until now.
Without further ado:
Lvalues and Rvalues
The names “lvalues” and “rvalues” come from early C and are named after the following rather poor approximation to what they are now, which I mention mostly just to help you remember which one is which:
- Lvalues are expressions that can be assigned to, i.e. they can be on
the left side of an
=
sign in an assignment; - Rvalues are all other expressions, which you will typically find on the right side of an assigmnent.
For example, if x
is an int
variable, the
statement x = 6;
makes sense, so the expression
x
is an lvalue. But 4 = x;
doesn’t make sense
— you can’t assign to 4
; what would that do, change the
meaning of 4
everywhere else it appears in the program?1 — so the expression 4
is an rvalue, as are all other numeric literals. Some other familiar
examples of lvalues include a[i]
if a
is an
array variable and s.f
if s
is a variable
holding a struct with a field called f
. Some other familiar
examples of rvalues are arithmetic expressions between primitive numeric
types, for example something like x + 4
.
While easy to remember, this breaks down quickly in modern C++ (and
modern C too): x
is still an lvalue even if it’s
const
, but const
variables can no longer be
assigned to. Also, surprisingly, string
literals are lvalues. A better rule of thumb is that lvalues
are expressions you can take the address of. And since lvalues
can actually also go on the right side of an assignment, the name is
sometimes retconned to be short for “locator value”. As far as I’m
aware, though, nobody has come up with a good retcon for “rvalue”. And
it is still the case that lvalues and rvalues form a perfect dichotomy
of all expressions: every expression is exactly one of the two. So
rvalues are expressions you can’t take the address
of.
In C++11, the category of “rvalues” was further subdivided into “xvalues” (sometimes “eXpiring values”, though this is also a retcon) and “prvalues” (“pure rvalues”). These categories are called value categories, by the way, and they still form a perfect trichotomy of all expressions: every expression is exactly one of an lvalue, an xvalue, and a prvalue. The term “glvalue” (“generalized l-value”) refers to simply “either an lvalue or an xvalue”. cppreference.com has a very detailed explanation of value categories, and there’s a classic StackOverflow question What are rvalues, lvalues, xvalues, glvalues, and prvalues with many good answers that are worth reading. But at a high level, I think the difference between xvalues and prvalues is less important to know than the difference between lvalues and rvalues. xvalues are pretty rare and you have to write somewhat tricky code to produce an xvalue. On the other hand, the innovative bits of C++11 that we’re here to discuss are exactly those that enable that tricky code. On the gripping hand, the goal of that tricky code is often simply to produce any kind of rvalue rather than specifically an xvalue.
One thing I want to make sure gets across is that a value category is a property of an expression, and not of a variable. This is confusing because expressions can consist of a single variable and will thus have a value category, but later we’ll see why we want to distinguish variables from expressions consisting of a single variable.2 To be clear when this comes up, I’ll call such an expression that just consists of a single variable a “variable expression”, although I don’t think this is established terminology (cppreference calls them “id-expressions”).
References
As I mentioned in the introduction, I’m assuming you understand pointers, so, well, a reference is like a pointer. The C++ FAQ says not to think of a reference as a funny pointer, but I think the comparison is useful in the sense that given a pointer to some data and a reference to some data, the things you can learn about that data and the ways you can modify it are basically the same. You can assign to it and modify it directly; you’ll be affecting exactly the same data, not a copy of it. You can get the address. You can convert between a pointer and a reference easily.
One way I think about references is that they’re pointers where when
you first initialize them, there’s an implicit &
(reference operator) applied to the expression you use to initialize
them with, and whenever you use them in an expression (no matter if
they’re on the left or right side of an assignment!), there’s an
implicit *
(dereference operator) applied to them. These
implicit operators cannot be circumvented, which limits some of the ways
you can manipulate references compared to pointers. Unlike pointers,
references can’t be null3, because you have to initialize each
reference to the &
of some expression; also, pointers
can change to point at something else, but references can’t change to
refer to something else, because to change where a pointer
p
points (as opposed to changing the data at the location
where it points), you have to directly assign to it without
dereferencing it: p = ...;
. So a reference is basically
another name for a variable that exists somewhere else. Finally,
although you can have pointers to pointers (and pointers to pointers to
pointers, and so on), and you can have references to pointers, you can’t
have a reference to a reference or a pointer to a reference. There can
only be one level of “reference-of-ness” in a type and (ignoring
templated types) it can only be at the outermost edge of the type.
The presence of the implicit &
when you initialize a
reference also immediately implies that you must initialize a
(non-const) reference to a (non-const) lvalue. (We’ll see how
that’s not true for const references later. Also, variables and
references can also be volatile, which often affects types in a way
similar to but orthogonal to const-ness; but for simplicity, I’m not
even going to touch that for the rest of the post.) That’s why these
references are more precisely called “lvalue
references”, to differentiate them from the rvalue references
that are the main target of this post, and which we’ll see soon. So, you
can write the following, because x
is an lvalue
expression:
int x;
int& y = x;
But you can’t write
int& y = 253; // doesn't compile
because that would involve taking the address of “253”, which doesn’t make sense; it’s a constant that could be produced by hardcoded assembly instructions and isn’t necessarily ever stored anywhere. You also can’t write, for example,
int& y = x + 6; // doesn't compile
because x + 6
is an intermediate expression. It isn’t
necessarily stored anywhere, certainly not in a way that is guaranteed
to persist after the statement, and so likely doesn’t have an
address.
However, in a sense, this isn’t fundamentally impossible. You could imagine that C++ might have been designed to accept the code above and just treat it as syntax sugar for code like the following, which declares a plain non-reference-type variable in the same scope and takes a reference to it:
int implicitly_created_temp_y = 253;
int& y = implicitly_created_temp_y;
There wouldn’t be any way to access the variable
implicitly_created_temp_y
other than through
y
, but this code could still make sense and y
might still behave the way you’d expect it to behave. And in fact,
you can initialize a const lvalue reference to an rvalue
expression (or an lvalue expression), which produces
code that works basically exactly as I described:
const int& t = 253;
behaves just like
int implicitly_created_temp_t = 253;
const int& t = implicitly_created_temp_t;
The lifetime of this new temporary value is the same as the lifetime of the reference.
Similar things happen with calling a function with a parameter that’s
a reference type. If you have a function f
declared as
f(int& arg)
, you can call it with the expression
f(x)
, but not f(253)
. On the other hand, if
you have a function f(const int& arg)
, you can
call it with f(253)
; this implicitly creates a variable
initialized to 253, takes a const reference to it, and calls the
function with that. As a result, even before C++11, it was quite
idiomatic for functions that only needed read-only versions of their
arguments to be declared with const
reference parameters,
as those would be more efficient than non-reference parameters on lvalue
arguments by avoiding needing to copy them, but would still work on
rvalue arguments. However, note that the lifetime of any such implicitly
created variable only lasts as long as the “full expression” containing
the function call, so it’ll be gone by the following statement and you
need to make sure you don’t still have dangling references to it. A
contrived example to illustrate this:4
const int& silly(const int& x) {
return x;
}
int main() {
const int& x = silly(253);
const int& y = silly(492);
("%d %d\n", y, x);
printf}
This compiles with no warnings, but when I run it, it prints
492 492
. The issue is that x
is a reference to
a temporary variable initialized to 253
that only lives as
long as the expression it’s initialized it, which is
silly(253)
(and in particular, not as long as
x
itself). So, whatever memory location x
refers to, it’s freely overwriteable by the time we finish its
definition and get to the definition of y
, and certainly by
the time we printf
it. It’s undefined behavior (and would
be even if we deleted the definition of y
). The term “full
expression” is a formal one but it roughly means “an expression that’s
not part of another expression”. If you see a semicolon, that’s almost
certainly the end of a full expression.
Finally, if you’re implementing a function whose return type is a
const
reference, you can also write a return
statement that returns an rvalue… but you should never do this because
this particular case never extends the lifetime of the temporary
variable. You are guaranteed to produce a dangling reference. Consistent
with this observation, the C++ compilers I tested actually warn if you
try to return a const reference to a local variable from a function,
whereas they didn’t warn about silly
above.
For the interested, cppreference.com documents the nitty-gritty
details of extending
the lifetime of a temporary. In general, given a fixed reference
type T
(which might or might not be const, and might be an
lvalue reference or an rvalue reference as we’re about to see) and an
expression /* some expression */
with a fixed type and
value category, the rules for whether these three snippets will
typecheck and compile are the same:
= /* some expression */;
T variable
void f(T parameter) {}
(/* some expression */);
f
() { return /* some expression */; } T f
These rules are documented in the full page on reference initialization. In fact, I find it kind of useful to try imagining manually inlining function calls, that is, temporarily ignore scoping issues and mentally translate function calls like this
(ParamType param) {
ReturnType f/* code here */
return /* return expr */;
}
= f(/* arg expr */); VarType v
to
= arg;
ParamType param /* code here */
= /* return expr */;
ReturnType ret = ret; // we will see later that this line is fake VarType v
Note the imaginary variable ret
with type
ReturnType
, which we never named and can be a reference. I
think this may be a mental model you build early on when learning
programming and then stop thinking about because it’s too obvious, but
when there are references involved, the exact semantics can become
nonobvious.
Although I think the above mental inlining helps you reason about
whether some expressions that replace /* return expr */
or
/* arg expr */
will compile, it doesn’t necessarily
represent the operations that actually happen, because of return
value optimization, or RVO. The above code suggests that if
ReturnType
and VarType
are classes with a
nontrivial constructor, the constructor will be called twice, once to
initialize ret
and once to initialize v
. (In
case you haven’t encountered this before: yes, despite appearances,
T x = y;
calls a single-argument constructor of
T
because it’s a variable definition; it has nothing to do
with operator=
.5 But after that
declaration, x = y;
would call operator=
.)
Even worse, /* return expr */
might just be an expression
that calls the constructor — that’s the natural way to write it, since
there’s no syntax to directly construct into ret
— and then
we’d be calling the constructor three times. However, if
ReturnType
and VarType
are the same class and
you actually compile and run such code, you will likely find that the
constructor is only called once, simply because the compiler can tell
where the constructed object will end up. This optimization is so common
that it’s named. There are other ways constructors can be elided from
the above inlined version; Shahar Mike’s article on Return
Value Optimization goes into more depth on this phenomenon.
Here I will put another important note to mirror the one I concluded the last section with: Variables (including function parameters) can be either references or not, as can function return types; but expressions (including function arguments) can never be reference types! Whether you end up taking a reference to any given expression or not depends on how the expression is used. (Don’t be confused by the many ways the word “reference” has popped up in this post. You can apply the reference operator to some expressions to get new expressions, whose types are pointer types; and you can apply the dereference operator to an expression if its type is a pointer type to get another expression. None of these expressions are necessarily reference types.)
Rvalue References
So what is an rvalue reference? It’s just a slightly different kind of reference introduced in C++11. The differences are actually smaller than I expected when I started learning about them. An rvalue reference still has to refer to something you can take the address of, and every time you use it in an expression, it still gets implicitly dereferenced in an uncircumventable way.
You declare a variable, parameter, or function return type with an
rvalue reference type just like you would for an lvalue reference type,
except with two &
s instead of one:
int&& y;
Note that the two ampersands
&&
are a single syntactic unit. It does not mean,
and there is no confusion with, a “reference to a reference to”
something.
The key difference lies in how you initialize rvalue
references: you can only initialize an rvalue reference with an
rvalue. You cannot initialize it to an lvalue! This might seem
bizarre because, of course, you can’t take the address of an rvalue,
which is what we need to produce a reference. But as I described
earlier, you could imagine treating such an initialization as syntax
sugar that implicitly defines a variable that is initialized to the
rvalue and then takes a reference to that, and similar syntax sugar
already exists and has well-defined semantics for const lvalue
references. That sort of implicit
variable-declaration-and-reference-taking is often, but not necessarily,
what happens. The two lines that do compile below are simple examples of
where it does happen: they behave as if they create variables that last
as long as t
and z
and then take reference to
those variables.
int x;
int&& y = x; // won't compile
int&& t = 253; // OK
int&& z = x + 6; // OK
The same rule applies when you’re calling a function that has a parameter with an rvalue reference:
void f(int&& p) {}
You can call this function as f(253)
. If x
is an int
variable, you cannot call
f(x)
because the expression x
is an lvalue,
but you can call f(x + 6)
. The rules defining the lifetime
of the implicitly defined variable are the same as before: it lasts
until the end of the full expression with the function call. (And for
completeness, in a function int&& f() { ... }
, you
could return
an rvalue and it would compile, but just as it
was with const lvalue references, doing this would always produce a
dangling reference, so you shouldn’t.)
Given that the above works, it may be a little surprising that
defining int&& y = x;
or calling f(x)
doesn’t work, because it’s even easier to imagine the syntax sugar that
it could expand to — you just initialize an implicit variable in the
same way, but with the expression x
. Sure, it’s an lvalue,
but lvalues can be on the right side of an assignment too. However, it
doesn’t work because making it hard for yourself to do that is sort of
the point of having rvalue references. We’ll see how you could
nevertheless force it to happen soon.
Another thing I should mention is that, like lvalue references, rvalue references can be const, and a non-const rvalue reference can only be initialized to a non-const rvalue. None of the rvalues we’ve seen so far have been non-const, and the idea of a non-const rvalue might even seem paradoxical — the point of an rvalue that it doesn’t have an address, so doesn’t that mean nobody else has a way to access it, so nobody will care or even notice if we modify an rvalue we got a reference to? We’ll see later how that’s false, so just keep this at the back of your mind for now.
But perhaps the most important thing to understand is that these are
the rules for initializing an rvalue reference, not for
using an rvalue reference in expressions. Even if the variable
y
is an rvalue reference to an int
, the
variable expression y
will still be an lvalue — it has an
address, which is the same as the address of the int
it
refers to. This is the number one confusing thing that every tutorial
about rvalue references will invariably point out specifically,6 and I still had to read like five of
these tutorials to really understand why, so let me try to spell it out
as explicitly as possible.
If we have this variable declaration,
int&& v;
the variable v
has type rvalue reference
int&&
, but you cannot describe it as an
lvalue or rvalue. The variable expression v
is an
lvalue of type int
. And in general, every
variable expression — that’s every expression that consists
solely of a single identifier of a variable — is always an
lvalue, no matter whether that variable’s type is
non-reference, lvalue reference, or rvalue reference. You should think
of “lvalue reference” and “rvalue reference” as compound words that
cannot be naively analyzed as the combination of the two words inside
them, like how the compound word “hot dog” refers to something that is
neither necessarily “hot” nor a “dog”. The first parts of those compound
words refer to the rules surrounding their initialization, but have
nothing to do with how they get used in expressions. After you’ve
initialized a reference, it’s actually quite hard to tell whether it’s
an lvalue reference or rvalue reference — every time you use it, you’ll
just get an lvalue.7
Some concrete consequences are that you cannot initialize another
rvalue reference to v
:
int&& vv = v; // won't compile
Even though the variables v
and vv
have the
same type, the variable vv
can’t be initialized with the
variable expression v
because vv
’s reference
type doesn’t match v
’s value category. For the exact same
reason, if you have a function f
that has an rvalue
reference as a parameter, like the one defined above, you still can’t
call f(v)
.
More interesting than the value categories of variable expressions are the value categories of function call expressions. Here, the rules are as follows. If you write a function call expression that calls a function, and the function has a return type that is…
- a non-reference type
T
, then the function call expression is an rvalue, specifically a prvalue. - an lvalue reference type
T&
, then the function call expression is an lvalue. - an rvalue reference type
T&&
, then the function call expression is an rvalue, specifically an xvalue. (These functions are quite rare, but the fact that they can now exist in C++11 is the entire reason this post exists and has, like, 11,000 words.)
C++ operators are kind of like function calls — on instances of
classes, they literally are calls to the special operator+
functions and company, but even on primitives, you can basically think
of arithmetic operators as like functions that return non-reference
types, and assignment operators as like functions that return lvalue
reference types. So if you are comfortable with the above list of
understanding functions, you should be comfortable with determining the
value category of quite a lot of expressions. However, you should be
aware of implicit conversions secretly turning lvalues into rvalues and
making them assignable to rvalue references. For example:
int x = 6;
float&& y = x; // this compiles! you're really assigning "x, but cast to a float"
By the way, the value category of the function call expression is
where the “mental inlining” I proposed earlier fails: if
ReturnType
and VarType
are both rvalue
references, the below compiles:
() { return /* return expr */; }
ReturnType f
= f(); VarType v
whereas the below does not:
= /* return expr */;
ReturnType ret = ret; // the expression `ret` is always an lvalue :( VarType v
We’ll see how to patch this mental inlining in a few sections.
std::move
We can now understand the standard library function std::move
and resolve some earlier questions with it. The second most popular
thing for rvalue reference tutorials to say is that
std::move
is kind of a misnomer. It doesn’t “move”
anything. (“move” is not an idea with a strict technical definition
anyway — it just loosely describes destructively operating on an object
to move its data to another object.) All std::move
does is
cast its argument to an rvalue reference type and return it. It’s not a
complicated function — you can find simple definitions of it all over
the place, with varying degrees of pedagogical simplification8 — but when first trying to really
understand it, I thought even the few lines of templating were kind of
gross. What I found really illuminating was trying to write out the
specializations of std::move
that would work with a
specific non-reference type, say, only int
s. They’re very
short.
int&& move(int&& x) { return static_cast<int&&>(x); }
int&& move(int& x) { return static_cast<int&&>(x); }
move
is nothing more than a punchily-named function that
performs a typecast. (static_cast
isn’t in the list of
things I assume you understand, but it’s just the modern C++ way to cast
expressions to types. And even in this case, appearances
notwithstanding, the expression resulting from the
static_cast
isn’t a reference type; the reference-ness of
the type just affects the value category of the resulting expression.
Here, static_cast
turns an lvalue into an rvalue, so that
an rvalue reference can be initialized to it.) In the first
instantiation, it doesn’t even do anything (but you would still need the
static_cast
to compile, because again, the variable
expression x
is an lvalue and return x;
wouldn’t work in a function whose return type is an rvalue reference).
But in the second instantiation, it does change the value category,
which is exactly what we need. By calling it on an lvalue, you get an
expression that’s an rvalue but refers to the same data.
Now, we know how to fix our code, where we tried to initialize an rvalue reference to an lvalue, that wouldn’t compile earlier (although whether we should is of course another question):
int x;
int&& y = x; // won't compile
We just apply std::move
to the expression we’re
initializing the variable with:
int x;
int&& y = std::move(x);
Note that this doesn’t implicitly declare a new variable and take a
reference to it. y
is actually truly a reference to
x
, so assigning to y
will assign to
x
and vice versa.
int&& y = std::move(x);
= 6; // also sets x to 6 y
It’s almost exactly as if you had defined it as an lvalue reference instead:
int& y = x;
We’ll see why this behavior is desirable in the next section. By
contrast, if you had defined y
with even a trivial
expression that’s equal to x
, you would have gotten a
reference to something else, an implicit temporary variable. With the
following definition of y
, the variables x
and
y
now refer to distinct int
variables and can
be assigned to without affecting each other.
int&& y = x + 0;
= 6; // doesn't affect x y
There’s one more thing I haven’t mentioned: std::move
preserves const-ness from its input type to its output type, so there
are two more instantiations that are useful to know about:
const int&& move(const int&& x) { return static_cast<const int&&>(x); }
const int&& move(const int& x) { return static_cast<const int&&>(x); }
So calling move
on a const lvalue will produce a const
rvalue, which is something that you wouldn’t be able to initialize a
non-const rvalue reference with:
const int x = 253;
int&& y = std::move(x); // won't compile
You should rarely need to write code that uses such an instantiation, but their existence will have consequences later.
Move Semantics
We can finally fully understand move semantics and how they’re implemented and used. As a reminder, I won’t spend much time motivating why move semantics are desirable; I linked some posts in the introduction of this post that do that instead. But the one-sentence goal of move semantics is that if you’re writing a function that does something with an object and might benefit from modifying it in-place or stealing its resources to use elsewhere (i.e. moving it), you’d want to know whether you’re allowed to do that.
To make things fully concrete, suppose you’re working with
vector<int>
s and you want to write a function
sorted
that takes a vector<int>
and
returns a sorted version of that vector. It would be nice to distinguish
callers that don’t want their original vector to be modified from
callers that don’t care, because in the latter case, you can sort the
vector you received in-place for more efficiency and less memory
allocation and return the same vector, and the caller won’t notice. That
is, you’d want to write a function that works correctly for this
caller:
<int> x = {3, 1, 4, 1, 5, 9};
vector
// shouldn't modify x, so it has to allocate a new vector
<int> y = sorted(x);
vector
// x should still be {3, 1, 4, 1, 5, 9} here
but is still efficient when called like this:
// assuming this allocates a massive vector,
<int> generateTestVector() { /* ... */ }
vector
// `sorted` shouldn't allocate an additional vector
<int> y = sorted(generateTestVector()); vector
Okay, so you still can’t write a single (non-overloaded, non-templated) function that does this. But what you can do, as of C++11, is write two overloads of the same function that do accomplish this. The two overloads have parameters that are a const lvalue reference and a non-const rvalue reference, respectively:
<int> sorted(const vector<int>& vec) { /* ... */ }
vector<int> sorted(vector<int>&& vec) { /* ... */ } vector
The first overload should copy the vector and allocate a new one; the
second overload can modify the one it received. The first client above
would call the first overload, because x
is an lvalue; the
second client above would call the second overload, because
generateTestVector()
is an rvalue. More often, this kind of
overloading is used to write constructors and assignment operators
(operator=
), since you can’t rename those and there are
many syntaxes that use them. All in all, this is a big improvement: you
make a copy when you need to and don’t when you don’t.
However, if you’re a client of this function, you might sometimes
find that you want to call it with an lvalue, say the variable
expression x
, as an argument, but you don’t want to
preserve x
. That is, you want the more efficient
implementation of the function that won’t make a copy of x
,
at the cost of it potentially clobbering the contents of x
.
So you’d want a way to deliberately invoke the second overloading.
Furthermore, note that you do not in fact want to do this by declaring a
new variable vector<int> xx = x;
and then somehow
getting sorted
’s parameter to be an rvalue reference to
xx
; nor do you want syntax that implicitly translates to
code like that, because then xx
would have to be a copy of
x
, and copying x
is precisely the inefficiency
you want to avoid. No, you want to convince sorted
to have
its rvalue reference refer to your variable x
, even though
the expression x
is an lvalue.
That is precisely the setting for which std::move
is
designed. If you call std::move
on the lvalue that you want
to pass in as an argument, it makes the argument an rvalue, causing the
second overload of sorted
to be called instead of the
first. However, crucially, when the second overload initializes its
rvalue reference to that rvalue, it will refer to the same lvalue you
supplied — no copy will occur.
<int> x = {3, 1, 4, 1, 5, 9};
vector
// `sorted` can only tell that you passed an rvalue, but in fact its
// parameter will be an rvalue reference to `x`.
<int> y = sorted(std::move(x));
vector
// Depending on how `sorted` is implemented, `x` may be destroyed here;
// it likely will be if `sorted` is implemented efficiently.
If you’d like to see this in a full program, we can use the
vector<int>
constructor, which is overloaded just
like this:
#include <iostream>
#include <vector>
int main() {
std::vector<int> a = {3, 1, 4, 1, 5, 9};
std::vector<int> b(a); // copy b from a
std::cout << a.size() << std::endl;
std::vector<int> c(std::move(a)); // move c from a
std::cout << a.size() << std::endl;
}
When I run this, it prints 6 0
. The first number must be
6
because we called the vector<int>
constructor with an lvalue, so we would have called the overload with a
const lvalue reference parameter, which is called the “copy
constructor”9. But intuitively, there’s no
guarantee what the second number printed will be at all, because by
passing std::move(a)
into the
vector<int>
constructor when initializing
c
, we’re deliberately passing an rvalue to invoke the
overload with a rvalue reference parameter, which is called the “move
constructor”. Intuitively, that’s a way of saying, “do whatever you want
with a
, I don’t care about it any more.”10
So in terms of its relation to move semantics, std::move
just sort of adds a flag to your lvalue expression saying, “Hey, I’m
okay with being clobbered or otherwise moved out of”. (If you call
std::move
with an rvalue for an argument, it doesn’t really
do anything.) But it’s up to the function receiving such an argument to
notice that type-level flag and handle it by actually performing
efficient move-semantics actions. If you didn’t overload
sorted
and only defined the version with the
const vector<int>&
parameter, client code could
still call it by passing an rvalue, possibly produced by calling
std::move
on an lvalue, and your code would work, but it
would copy the vector once unnecessarily11
and no moving would occur. So std::move
doesn’t necessarily
imply moving at all; it’s just a suggestion to the function that it can
be moved for efficiency, a suggestion that could be heeded, ignored, or
even willfully misinterpreted.
One way it could be ignored is if you try to std::move
a
const lvalue. Consider this slight modification of our above
program:
#include <iostream>
#include <vector>
int main() {
const std::vector<int> a = {3, 1, 4, 1, 5, 9};
std::vector<int> b(a); // copy b from a
std::cout << a.size() << std::endl;
std::vector<int> c(std::move(a)); // still copy c from a!
std::cout << a.size() << std::endl;
}
This compiles fine, but it prints 6 6
: c
was not able to move the data out of a
. That’s because even
though std::move(a)
is still an rvalue, this time it’s a
const rvalue, and can’t be used to initialize a non-const rvalue
reference. But it can be used to initialize a const lvalue reference, so
the compiler silently picks the overload of the constructor with that as
its parameter, i.e. the copy constructor, instead. The overload with a
non-const rvalue reference parameter will only be called on non-const
rvalues, not all rvalues.
Even more dramatically: if you wanted, you could overload
sorted
, or any other constructor or assignment method, in a
way such that it treats lvalues and rvalues exactly oppositely for move
semantics! That is, you could write overloads of sorted
that steal the resources from its argument if you pass in an lvalue
argument via an lvalue reference parameter (although it would have to be
non-const), but perform a shallow copy and allocate a new vector if you
pass in an rvalue argument via an rvalue reference parameter (which
could be const). The first behavior would likely break your clients’
code and the second behavior would be obtusely inefficient in most
cases, but there’s no technical reason you couldn’t write this code. And
if you did, then whenever one of your clients tries to pass an lvalue to
your function as an argument, they would have to call
std::move
on it only if they didn’t want it to be
moved. Hopefully that thought experiment really drives home how
std::move
, on its own, doesn’t do any moving at all.
Still, if you ever find yourself passing an lvalue into a function
supporting move semantics and you don’t care about the lvalue any more,
std::move
may save you a copy. However, I must caution here
that there’s one place you might think of using it immediately, in the
return
statement of a function returning an object it
constructed, that you almost always shouldn’t.
The logic is compelling, to be sure. As we’ve discussed before, mentally inlining this
() {
Thing make_thing;
Thing inner_thing// maybe do stuff with inner_thing
return inner_thing;
}
= make_thing(); Thing thing
results in this code:
;
Thing inner_thing// maybe do stuff with inner_thing
= inner_thing;
Thing ret = ret; Thing thing
That’s one constructor call and two copy constructor calls, because
inner_t
and ret
are both lvalues. If we
replaced return Thing();
with
return std::move(Thing());
, then in the inlined version,
we’d be able to invoke the move constructor rather than the copy
constructor for ret
. Assuming Thing
implements
move semantics sensibly, isn’t that better?
Actually, like I mentioned earlier, without the
std::move
, most compilers will already elide both copy
constructor calls and directly construct the Thing
into
t
because of return value optimization, or RVO. There’s a
section in the RVO article I linked earlier on why
returning by std::move() is an anti-pattern and can even actively
make things worse. Part of Effective Modern C++’s Item 25
also discusses this. I won’t belabor the details.
To sum up:
- Lvalue and rvalue references enable you to write functions or overloads of functions that can only be called with lvalues or rvalues as arguments.
- This is useful because many functions can be implemented more efficiently if they know they can clobber or steal their argument’s resources (“moving”), which is strongly but not perfectly correlated with the argument being an rvalue.
- If a caller has a variable that they are OK with being clobbered or
stolen from, they might want to deliberately break the correlation above
by casting their variable to an rvalue and passing that into a function.
They can do that casting by calling
std::move
. However, whether the called function will recognize this and do any moving depends on its implementation and is purely a matter of convention, albeit a very strong one.
Aside: Reference Qualifiers
Incidentally, class methods can also have reference
qualifiers constraining whether they can be called on lvalues or
rvalues, which look like &
or &&
at the end of the signature:
class Thing {
void method() &; // only callable on lvalues
void method() &&; // only callable on rvalues
};
These notations turn the “implicit object parameter” into an lvalue
reference or an rvalue reference, sort of as if method
was
declared as below, and if it were an operator or if C++ supported Uniform
Function Call Syntax like D or Nim:
void method(Thing& thing);
void method(Thing&& thing);
You could use this feature to implement move semantics in methods in terms of the expression they’re invoked on. However, the convention for doing so is not as strong and this isn’t used in the standard library much (if at all?).
Reference Collapsing, Universal References, and Perfect Forwarding
Now that we understand move semantics, we turn to the second
application of rvalue references in C++11: allowing perfect
forwarding. Again, I won’t try to motivate this very hard, but
it might be useful to understand the problem of perfect forwarding with
the precise terminology we’ve worked out in this post so far. Let’s keep
our setup simple: say you have a function f
, which might
have overloads and which you can’t change, and you want to write a
function g
so that calling g
with some
arguments behaves exactly like calling f
with the same
arguments. (Defining such a g
could be useful if
g
also does something else additionally or postprocesses
the return value of f
.) So g
would be
“forwarding” calls it received to f
instead. Let’s even
make things easy and say we know f
has a single parameter
and we know its return type is void
. Then a first attempt
at implementing g
would be:
template<typename T> void g(T x) {
// maybe do other stuff here
(x); // very imperfect forwarding
f}
This seems okay because T
can be deduced to any type,
including a reference type, so the parameter of g
should be
inferred to be the same type as the parameter of f
. But, by
applying what we’ve learned so far, we can see that that actually isn’t
true because we lose the information of the value category of the
argument. The argument g
was called with might have been an
lvalue or an rvalue, but the argument that f
was called
with, which is the variable expression x
, is always an
lvalue.
Concretely, if f
’s parameter’s type is an rvalue
reference, then we could have called f
with an rvalue as an
argument; but trying to call g
with an rvalue as an
argument will cause it to try to call f
with an lvalue as
an argument, which won’t work. Even worse, if f
has two
overloads that have an lvalue reference parameter and an rvalue
reference parameter, respectively, our attempt at forwarding will
silently always call the former overload even if passed an rvalue, and
then g
will not behave like f
even though
replacing a call to f
with a call to g
still
resulted in code that compiles.
Before we get to how “universal references” resolve this issue, we
need to talk about reference collapsing. Much earlier,
I said that “you can’t have a reference of a reference”. This is sort of
a half-truth. You can’t write int& & y;
— it won’t
compile, and you have no reason to, as we’ll see very soon. But you can
get into a situation where you write something equivalent, with things
like typedefs and template expansion. This code compiles:
typedef int& int_ref;
void f(int_ref& p) {}
What is the type of the parameter p
? Is it a reference
to a reference to an int
? Well, it turns out that taking a
reference to a reference to a type collapses to just taking a
reference to that type directly. The resulting type is an lvalue
reference if either level of reference was an lvalue-reference; it’s an
rvalue reference if both levels of reference were rvalue references. As
a list:
& &
=&
& &&
=&
&& &
=&
&& &&
=&&
It’s binary AND where &&
is true. You can read
more about reference
collapse on cppreference.com. But, in any case, the trichotomy that
every type is exactly one of a non-reference, lvalue reference, and
rvalue reference remains complete. You can’t write
int& & y;
not because there’s no sensible
definition, but because, in a rare instance of C++ preventing yourself
from shooting yourself in the foot, you wouldn’t gain anything — that
would be exactly equivalent to int& y;
.
With that in mind, a prototypical universal reference, useful for
forwarding, is the type T&&
of v
in
this templated function:
template<typename T> void g(T&& v) {
}
As we learned about when we first met rvalue references, if you have
a function f(int&& x)
that has a parameter of type
int&&
, you can only call it with an argument that’s
an rvalue. And in fact, if you had a function g2
that was
declared to take T&
(an lvalue reference to
T
) like so,
template<typename T> void g2(T& v) {
}
you could only call g2
with an argument that’s an
lvalue. However, because of reference collapsing, you can pass either an
lvalue or an rvalue as an argument to g
!
- If you pass an lvalue of, say, type
int
, thenT
can be inferred to beint&
, so that the parameter is of typeint& && = int&
(by reference collapsing). - If you pass an rvalue of type
int
, thenT
can be inferred to beint
so that the parameter is of typeint&&
. (Passing anint
rvalue would also work ifT
were inferred to beint&&
and the parameter’s type would beint&& && = int&&
, and you could explicitly specify thatT
beint&&
if you wanted, but that just turns out to not be how the template type inference rules are written.)
This is why T&&
is called a universal reference:
it’s a reference, but it can be initialized to any argument, lvalue or
rvalue, and for that matter, const or non-const.
Does that mean we’re done? Not at all: if we wrote,
template<typename T> void g(T&& v) {
(v); // just as imperfect
f}
we would still always be passing an lvalue as an argument to
f
, and no amount of fiddling with the type of the parameter
v
or other aspects of the templating will fix this, because
the variable expression v
we’re passing as an argument is
always an lvalue. To have a chance of passing an rvalue to
f
, we must pass it some other kind of expression. The best
candidate (the only one we’ve really looked at in this post) would be a
function call expression. And one function that will solve our problem
neatly is the function std::forward
.
Here’s how std::forward
, which is a templated function
taking one type variable T
, works:
- If
T
is a non-reference (or rvalue reference, but this case won’t really be relevant),std::forward<T>
’s return type is the rvalue referenceT&&
and its parameter type is the lvalue referenceT&
. So the argument tostd::forward<T>
must be an lvalue and the result of callingstd::forward<T>(...)
will be an rvalue (specifically an xvalue). - If
T
is an lvalue reference,std::forward<T>
’s return type isT
itself, which is an lvalue reference, and its parameter type is alsoT
itself, which is an lvalue reference. So the argument tostd::forward<T>
must be an lvalue and the result of callingstd::forward<T>(...)
will also be an lvalue.
More briefly, the int
specializations of
std::forward
are:
int&& forward(int& v) {} // forward<int> (and forward<int&&>)
int& forward(int& v) {} // forward<int&>
In particular, all specializations of forward
have a
parameter of lvalue reference type, so you can’t expect the desired
reference-ness of T
to be inferred solely based on the
argument passed to std::forward
. You will need to specify
T
yourself in order to get forward
to do
anything interesting. And that is exactly what we do to accomplish
perfect forwarding:
template<typename T> void g(T&& v) {
return f(std::forward<T>(v)); // "perfect" forwarding
}
It may help to imagine the int
specializations of
g
as well. They simplify down to:
void g(int& v) { f(v); }
void g(int&& v) { f(std::move(v)); }
The argument we supply to forward
in g
is
always an lvalue, which tracks with the fact that forward
’s
parameter is always an lvalue reference. But you can work out how the
forwarding occurs now:
- if
g
is called with an lvalue argument of typeA
(which must be non-reference — expressions aren’t reference types), thenT
will be inferred to beA&
, sostd::forward<A&>
will have return typeA&
. Thus, calling it will give an lvalue, sof
will be called with an lvalue argument; - if
g
is called with an rvalue argument of typeA
(which, again, must be non-reference), thenT
will be inferred to beA
, sostd::forward<A>
will have return typeA&&
and thus calling it will give an rvalue, sof
will be called with an rvalue argument. What’s more, the argument is always passed by reference, sof
’s parameter will be an rvalue reference to the exact same thing thatg
’s parameter references.
That was a mouthful, but the result is perfect forwarding:
g
will call f
with the same arguments it
receives as the same value categories, so it behaves partially just as
if you had called f
.
For completeness, I’ll quickly mention that you can do perfect forwarding even without knowing how many arguments you’re trying to forward, by using a template parameter pack. But in terms of the types and value categories involved, nothing fundamentally different is going on here. It would look like this, and is likely how you’ll actually see perfect forwarding in the wild or implement it in practice:
template<typename... Ts> void g(Ts&&... v) {
(std::forward<Ts>(v)...);
f}
Perfect forwarding appears in the standard library in places such as the data structure “emplace” functions (e.g. vector::emplace_back and map::emplace), smart pointer construction functions (e.g. std::make_unique and std::make_shared), std::forward_as_tuple, and probably others.
(Unsurprisingly, there are actually quite a few ways in which
“perfect forwarding” is still imperfect: for some suitably crafted
functions f
and some arguments, the above g
will not behave like f
. If you want to learn about them,
you may actually want to buy Effective Modern C++ and read
Item 30, because wowzers, there are some crazy corner cases and there’s
no way I know enough C++ to cover them more effectively.)
Incidentally, std::forward
also lets us patch the
“manual inlining” model of understanding how functions return values.
Ignoring scoping issues and lifetimes, this
() { return /* return expr */; }
ReturnType f
= f(); VarType v
should be equivalent to this:
= /* return expr */;
ReturnType ret = std::forward<ReturnType>(ret); VarType v
The expression std::forward<ReturnType>(ret)
has
the same value category as a call expression to a function with return
type ReturnType
.12 Admittedly, we
introduced another (templated!) function call with this patch, so if
we’re trying to strictly simplify the rules we have to remember, we
didn’t gain any ground, but I mentioned it in case it helps with
intuition anyway.
If perfect forwarding is so good why isn’t there a perfect forwarding 2
A question to think about: Can you write a function that perfectly forwards two disjoint argument lists to two different functions?
That is, if you have two functions f1
and
f2
, and you don’t know how many parameters either takes or
what types they are, can you write a templated function g
such that any caller of g
can specify two lists of
arguments, and g
will behave just as if f1
were called with the first list and f2
were called with the
second list?
It’s not easy, but std::pair has
a constructor overload that does it, which you have to invoke by
prepending a piecewise_constructor
argument and then
packing things into a tuple. Honestly, though, I don’t understand this
deeply and this post is already far too long, so I’ll leave it at
that.
Some unanswered questions
We now understand deeply how C++11 uses rvalue references to achieve move semantics and perfect forwarding, but I don’t know if you have this mathematician’s unease that we made some arbitrary choices along the way about exactly how rvalue references work. In particular, are the reference collapse rules really “canonical”?
It seems that the most direct impetus for the choice of reference
collapse rules is just to allow perfect forwarding by allowing universal
references to exist — specifically to make it so that, under
template<typename T>
, the type
T&&
can be either an lvalue reference or an rvalue
reference, but not a non-reference. What’s more, note that you do want
the expression-in-terms-of-T to be simple and probably have direct
syntax support, because you want to be able to infer T
from
the type and value category of your argument by following canonical,
unsurprising rules when possible. It’s not sufficient to just say your
parameter’s type is an unadorned type variable T
and
require that it be a reference through type utilities through other
parts of the templating. That’s already possible with enable_if:
template<typename T>
typename std::enable_if<std::is_reference<T>, void> f(T t) {
}
This does produce a function where T
must be either an
lvalue reference type or an rvalue reference type, and, depending on
what T
is, can either only be called with lvalues or only
be called with rvalues. Unfortunately, T
isn’t correctly
inferred in calls. Given an argument, the compiler infers its
non-reference type for T
, which would have worked and would
be the most sensible choice without the enable_if
, but then
finds that it fails to substitute because of the enable_if
and doesn’t try to backtrack.
int x = 6;
(x); // doesn't compile, infers T = int
f(6); // doesn't compile, infers T = int f
It does work if you explicitly specify T
, but of course,
that defeats the purpose of type inference entirely:
<int&>(x);
f<int&&>(6); f
So not only do we want a simple type-level expression in terms of
T
that can be either an lvalue reference or an rvalue
reference but not a non-reference, the expression has to be “canonical”
enough that we can standardize how to infer T
given what
kind of reference the expression is. And although we could make
T&
that expression if we made references collapse the
other way (so a reference of a reference would be an lvalue reference
only if both original reference operations were lvalue references), we
probably wouldn’t want to for backwards-compatibility. So choosing
T&&
and adopting the direction of reference
collapsing to make it universal is a plausible choice.
Still, to the best of my knowledge, I suspect a version of C++ where
references didn’t collapse (i.e. taking a reference of a reference would
just fail to compile, whether or not there was a typedef
or
using
in the way) and we found a different way to represent
universal references would still hold up. One thing to observe is that,
even if reference collapse were gone, it’s sufficient to implement
perfect forwarding for a single argument with two overloads that take
T&
and T&&
. But we really want
something more uniform so we can handle varargs (and so that, even for a
finite, known number of arguments n, we don’t need
2n overloads). It’s possible to try to make the
enable_if<is_reference>
mess earlier work, because it
sort of already means the right thing, but I don’t see a compelling
way.
- One strategy to make it work is giving the compiler the intelligence to change its inference rules after seeing such a template expression, but that seems too much of a brittle special case.
- Or we could make the compiler backtrack, trying both the
non-reference type and the reference type for
T
, but that threatens exponential blow-up in compilation time with many parameters.
Perhaps we could have chosen some brand new syntax that forces
T
to be a reference without affecting it, and causes
T
to be inferred to be either an lvalue reference or an
rvalue reference. For example, the natural syntax extension I’m the most
confident wouldn’t affect any other part of the syntax would be:
template<typename T> void f(T&&& p) {} // made-up syntax
It even seems useful if we could find a way to make this work with non-type-variables, something like:
void f(int&&& p) {} // made-up syntax
This would be a function that can take any int
argument
(in particular, causes arguments to be implicitly converted to
int
s), but takes it by reference and knows whether the
argument was an lvalue or an rvalue. But there’s no obvious way or
syntax for the function to access that knowledge. Perhaps we could make
it so, if a variable’s type is an rvalue reference, its variable
expressions is an rvalue? And to allow us to do everything with rvalue
references we could do before, we might have an additional
std
function that casts rvalue references to lvalue
references. But this is also doomed because you can’t wait until
f
is actually called to know if p
is an lvalue
or an rvalue — if you turn around and call another function with it, you
might be calling different overloads with radically different behavior —
so then you’d have to template the function twice, which is extremely
suspicious if our made-up syntax doesn’t have any template variables in
it.13 Attempting to retrofit other type
machinery to dig the information out of p
, for example with
decltype
, seems doomed to failure for the same reasons. Oh
well.
Still, there are other ways reference collapse feels like a somewhat
arbitrary consequence of trying to achieve a goal while maintaining
backwards compatibility. For example, it’s quite annoying to write a
templated function that can take arguments of any type, but only if
they’re rvalues. That is, you’d want the function’s parameter type to be
any rvalue reference, but not an lvalue reference or a non-reference. StackOverflow
shows it’s possible, but it’s tough. Compare to how easy it is to
write a templated function that can take arguments of any type, but only
if they’re lvalues:
template<typename T> void f(T& x)
, end of story.
No matter whether T
is an lvalue reference, an rvalue
reference, or a non-reference, T&
will be an lvalue
reference type (possibly via reference collapse), and given the type
that T&
is equal to, the type that T
should be inferred to be is obvious.
Conclusion
Rvalue references are a new type of variable or parameter in C++11
that can only be initialized to rvalues. Firstly, this lets you write
functions or overloads of functions that only accept rvalues as
arguments, which turns out to usually be a great way to detect whether
you have permission to clobber your argument or take resources from it.
In addition, std::move
lets you explicitly give permission
to such a function or overload of a function in a call where it would
normally not detect it. Secondly, in conjunction with reference
collapsing, rvalue references let you write a templated function that
can tell whether its argument was an lvalue or an rvalue and make
templating decisions accordingly. This enables you to write functions
that preserve the value category of their arguments when calling other
functions with them, an ability called perfect forwarding. These
abilities make C++11 a much more powerful language than its predecessor,
just like your C++ skills are probably much more powerful than when
you started reading this post which I guess justifies why they
incremented the version number so much but Rust’s lifetimes are
actually both better and simpler and nobody can convince me
otherwise why am I even bothering to write a conclusion, this isn’t
an AP exam.
Changelog
- New footnote about
decltype
and its distinguishing variables from expressions. Edited other footnotes to refer to it. - New footnote on constructors.
- New section on reference qualifiers.
- New footnote on generic lambdas and potential implications.