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.
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:
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) {
x += 6;
}
}
// usage:
vector<int> v = {2, 5, 3};
addSixToAll(v);
// 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.
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:
But you can’t write
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,
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:
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:
behaves just like
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);
printf("%d %d\n", y, x);
}
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:
T variable = /* some expression */;
void f(T parameter) {}
f(/* some expression */);
T f() { return /* some expression */; }
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
ReturnType f(ParamType param) {
/* code here */
return /* return expr */;
}
VarType v = f(/* arg expr */);
to
ParamType param = arg;
/* code here */
ReturnType ret = /* return expr */;
VarType v = ret; // we will see later that this line is fake
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.
The same rule applies when you’re calling a function that has a parameter with an rvalue reference:
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,
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
:
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:
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:
whereas the below does not:
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):
We just apply std::move
to the expression we’re initializing the variable with:
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.
It’s almost exactly as if you had defined it as an lvalue reference instead:
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.
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:
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:
vector<int> x = {3, 1, 4, 1, 5, 9};
// shouldn't modify x, so it has to allocate a new vector
vector<int> y = sorted(x);
// 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,
vector<int> generateTestVector() { /* ... */ }
// `sorted` shouldn't allocate an additional vector
vector<int> y = sorted(generateTestVector());
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:
vector<int> sorted(const vector<int>& vec) { /* ... */ }
vector<int> sorted(vector<int>&& vec) { /* ... */ }
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.
vector<int> x = {3, 1, 4, 1, 5, 9};
// `sorted` can only tell that you passed an rvalue, but in fact its
// parameter will be an rvalue reference to `x`.
vector<int> y = sorted(std::move(x));
// 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;
}
Thing thing = make_thing();
results in this code:
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:
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
f(x); // very imperfect forwarding
}
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:
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:
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,
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,
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:
It may help to imagine the int
specializations of g
as well. They simplify down to:
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:
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
should be equivalent to this:
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:
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.
It does work if you explicitly specify T
, but of course, that defeats the purpose of type inference entirely:
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:
It even seems useful if we could find a way to make this work with non-type-variables, something like:
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.
It may be more likely than you think. You can do it in Python, you can do it in Java…↩
Although this is a bit out of the way, one place this does make a visible difference in C++ is with
decltype
. Ifx
is a variable or has a similarly simple form (I won’t list this out; refer to cppreference.com on decltype),decltype(x)
gives the type of the variable, reference-ness and all. But on any more complicated expression, even(x)
,decltype
gives a type derived from the value category of the expression: lvalue reference for lvalue, rvalue reference for xvalue, and non-reference for prvalue. Here’s a program to show that:#include <utility> // Undefined class template to elicit error with type (Item 4 of // Effective Modern C++) template<typename T> class Elicit; int main() { int x; // x is int, decltype(x) = int Elicit<decltype(x)> t1; // (x) is lvalue, decltype((x)) = int& (!) Elicit<decltype((x))> t2; // std::move(x) is xvalue, decltype((x)) = int&& Elicit<decltype(std::move(x))> t3; // 3 is prvalue, decltype((x)) = int Elicit<decltype(3)> t4; // rvalue reference we'll learn about later: int&& y = std::move(x); // y is int&&, decltype(x) = int&& Elicit<decltype(y)> t5; // (y) is lvalue, decltype((y)) = int& (!) Elicit<decltype((y))> t6; }
This might not seem too bad since, whenever you see
decltype
, you can immediately tell what variable or expression it’s being applied to. But it can get into much spookier action-at-a-distance when applied toauto
. To steal another example from Effective Modern C++, Item 3:↩decltype(auto) f1() { // deduced as returning int int x; // imagine lots of other code here return x; } decltype(auto) f2() { // deduced as returning int& (!) int x; // imagine lots of other code here return (x); // dangling reference (!!) }
Well, you could shoehorn it in with code like
but you really shouldn’t. Don’t take my word for it, the C++ FAQ is perfectly adamant about it.↩
Although the full example is contrived, note that if you pass a lvalue to
silly
, it just returns a reference to the same lvalue; there’s no undefined behavior, and a function with similar parameter and return types could be useful (e.g. producing a const reference to a field in a struct that it also takes by const reference). And, you can write a function that takes a const reference, does computations with it, and returns a non-reference, which also wouldn’t cause any undefined behavior:So
silly
’s type and the action of passing an rvalue as an argument to a function withsilly
’s parameter’s type could both individually make sense (so it’s plausible that compilers don’t warn about the above code), but when combined as in the contrived example above, they don’t.↩See also this table by Nicolai Josuttis for the plethora of syntaxes C++ has for initialization.↩
Section 5 of Thomas Becker’s explainer is dedicated to the question: “Is an Rvalue Reference an Rvalue?”. Jonathan Boccara bolds it twice in “Understanding lvalues, rvalues and their references”. It gets stated explicitly in literally page 3 of Effective Modern C++ in the first code snippet in the introduction.↩
If you try, you can at least do it with
decltype
and type support utilities. There may be much easier ways; I’m not good enough at C++ to know. But note that, as mentioned in an earlier footnote, this hinges crucially on the fact thatdecltype
may treat the thing you apply it to as a variable rather than an expression. Anything that can’t do that is doomed.↩#include <iostream> int main() { // print booleans as "true" or "false" instead of 1 or 0 std::cout << std::boolalpha; int t = 1; const int& x = t; int&& y = 2; std::cout << "Is x an lvalue reference? "; std::cout << std::is_lvalue_reference<decltype(x)>::value << std::endl; std::cout << "Is x an rvalue reference? "; std::cout << std::is_rvalue_reference<decltype(x)>::value << std::endl; std::cout << "Is y an lvalue reference? "; std::cout << std::is_lvalue_reference<decltype(y)>::value << std::endl; std::cout << "Is y an rvalue reference? "; std::cout << std::is_rvalue_reference<decltype(y)>::value << std::endl; }
Examples include in A Brief Introduction to Rvalue References and in Item 23 of Effective Modern C++.↩
The constructor with this exact signature, one parameter of type const lvalue reference to the class the constructor is defined on, is one of a few special methods in that it has a default implementation where it just copies all the fields with each field’s copy constructors. If the class satisfies certain constraints and you don’t opt-out explicitly, the compiler will generate such a constructor automatically. You can also explicitly request this default implementation with
= default
.In addition to this constructor, called the copy constructor, the other constructors and methods with default implementations are the default (parameterless) constructor, the move constructor (taking one
Thing&&
parameter), the copy assignment operator (taking oneconst Thing&
parameter), the move assignment operator (taking oneThing&&
parameter), and the destructor. For more details, cppreference.com’s classes page links to each of these.↩However, if you actually look it up,
vector
’s move-constructor — constructor overload (8) on cppreference.com, as of time of writing — actually explicitly states that the moved-from vector will beempty()
, so this program is guaranteed to print6 0
.vector
’s move-assignment-operator overload (2) might have been a better example: the moved-from vector is “in a valid but unspecified state afterwards.” But the intuitive role thatstd::move
plays is the same.↩Well, there’s no guarantee this copy will happen — it’s possible the compiler will optimize it away.↩
Okay, fine, this is another lie: if
ReturnType
is a non-reference, thenstd::forward<ReturnType>(ret)
will be an xvalue, but the call expression will be a prvalue. But they’re either both lvalues or both rvalues, which is good enough.↩But one reason to have hope anyway is that, as of C++14,
auto
can also introduce templating in lambdas (“generic lambdas”). Declaring a lambda like this:roughly declares an implicit class with a templated
operator()
function likeand then initializes
f
to an instance of this class. This also works if you replaceauto
withauto&&
:T
becomesT&&
, a universal reference. And then, you can in fact usedecltype(x)
to dig out the deduced reference-ness of the parameter, which tells you whether the argument is an lvalue or rvalue.Admittedly,
auto
already shared a lot of the same type deduction machinery as templates in C++11, so perhaps this is natural. And this doesn’t actually makef
’s type itself templated: it’s an instance of a concrete class, just one with a multitude of instantiations of one method. We’re trying to come up with syntax that turns a method declaration into a templated one with many instantiations.↩