Introduction
In addition to pointers inherited from C, C++ has references that serve a similar purpose:
int i;
int *p = &i; // p points to i
*p = 42; // i = 42
int &r = i; // r refers to i
++r; // ++i
Like a pointer, a reference is a type of variable that refers (is bound) to some other object. However, unlike a pointer, a reference:
- Must be initialized (bound) to an object when defined.
- Can not be rebound to a different object. (There’s no syntax to do it.)
- Can never be null.
But if C++ already has pointers, why were references added to C++?
Cheaply-Copied Types
Before we get to why references were added to C++, we need to define the term cheaply-copied type:
A C++ type T is considered “cheaply copied” when the cost of making a copy is less than or on par with the cost of indirecting through a pointer or reference.
What constitutes “cheaply copied” varies by machine architecture, but generally includes any type T where sizeof(T)
≤ 16, so:
- Any built-in type (e.g.,
unsigned
). - Any
enum
. - Any pointer.
- Any small
struct
,union
, orclass
. - Any
typedef
thereof.
As an exception, a class that manage resources (for example, an object T that contains a pointer to another object U where copying T includes copying U) is not cheaply copied even if sizeof(T)
≤ 16.
Motivation for References
According to Bjarne, references were added to C++ primarily to support operator overloading. The goal is to be able to pass arguments to overloaded operators efficiently even when the argument types are not cheaply-copied. The only way to do that without references is with pointers, for example:
T operator+( T const *lhs, T const *rhs );
void f() {
T x, y;
// ...
T z = &x + &y; // ugly
}
That’s ugly. With references, it gets better:
T operator+( T const &lhs, T const &rhs );
void f() {
T x, y;
// ...
T z = x + y; // better
}
References are also useful when passing non-cheaply-copied types to or returning such types from functions in general:
std::string const* get_string();
void print_string( std::string const *s );
void f() {
std::string s{ *get_string() }; // ugly
// ...
print_string( &s ); // also ugly
}
Not for Output Parameters
References can also be used instead of pointers for “output parameters” of functions to receive “return values”:
bool parse_T( std::string const &s, T &t_out );
void f( std::string const &s ) {
T t;
if ( !parse_T( s, t ) ) // Is 't' modified?
// complain
}
The problem with this you can’t tell by looking at uses of parse_T()
whether t
is modified or not because there’s no visible difference between passing t
by value versus by non-const
reference. To find out, you have to find the declaration of parse_T()
to see whether the reference is const
or not.
Even though there’s no visible difference between passing
t
by value versus byconst&
either, you don’t care becauset
can’t be modified. From your point of you, it would make no semantic difference whethert
were passed byconst&
or by value. The use ofconst&
is an optimization to pass a non-cheaply-copied type efficiently.
However, if you use a pointer instead:
bool parse_T( std::string const &s, T *t_out );
void f( std::string const &s ) {
T t;
if ( !parse_T( s, &t ) ) // '&' hints 't' is modified.
// complain
}
then the presence of &
is a visual hint that t
is likely modified.
Consequently, I personally recommend that non-const
references not be used for output parameters and that pointers be used instead. To add weight to this recommendation, it happens that Bjarne agrees:
“My personal style is to use a pointer when I want to modify an object because in some contexts that makes it easier to spot that a modification is possible.” — Bjarne Stroustrup
Not for Data Members Either
References can be used as data members of classes:
class C {
public:
C( T &t ) : _t{ t } { }
// ...
private:
// ...
T &_t;
};
One advantage of using references instead of pointers for data members is that you can’t forget to initialize references in every constructor. However, there are disadvantages:
The existence of at least one reference data member will suppress the implicitly defined assignment operators because references can not be reassigned (rebound).
The existence of at least one reference data member means you can not copy (rebind) references in your own assignment operators either.
-
Have a problem similar to the use of non-
const
references for output parameters in that you can’t tell by looking at uses that they’re not part of the class itself:
_t.f(); // Is '_t' part of C?
However, if you use a pointer instead:
_t->f(); // Says '_t' points elsewhere.
Lvalues & Rvalues
Before continuing, we need to digress a bit to define the terms lvalue and rvalue that are types of value category:
An lvalue is a value, has a name, can (but not must) appear on the left-hand-side (LHS) of
=
(the assignment operator), and can have its address taken via&
(the address-of operator).An rvalue is a temporary value, has no name, can only appear on the right-hand-side (RHS) of
=
, and can not have its address taken via&
.The key thing that distinguishes and lvalue from an rvalue is: if something has a name, it’s not an rvalue.
These value categories were actually updated in C++11 by being renamed and expanded, but they’re quite a bit more complicated now — unnecessarily so for this article. The old terms of lvalue and rvalue are sufficient here.
Some examples:
int a, b, *p;
a = b; // "a" and "b" are lvalues (both have names)
a = a + b; // "a" = lvalue; "a + b" = rvalue
a + b = 42; // error: rvalue on left-hand-side
p = &a; // address of lvalue
p = &(a + b); // error: address of rvalue
Motivation for Rvalue References
Given:
class container {
public:
// ...
void push_back( std::string const &s );
};
int main() {
container c;
c.push_back( "hello" );
}
This code will compile and work as expected despite not having:
void push_back( char const *s );
because std::string
has a constructor:
string( char const *s );
that the compiler will use to construct a temporary string
from "hello"
that can then be passed to the push_back()
that exists as if:
int main() {
container c;
{
std::string __tmp{ "hello" };
c.push_back( __tmp );
}
}
More explicitly:
- For
__tmp
,string::string( char const* )
is called that dynamically allocates a buffer and copies the string literal"hello"
into it. -
push_back( std::string const& )
adds thatstring
to its container viastring::string(std::string const&)
that dynamically allocates another buffer and copies the string into it. -
string::~string()
destroys__tmp
by deallocating the first buffer.
That’s inefficient. If __tmp
is going to be destroyed anyway, why not move its buffer by moving only the string’s internal pointer rather than copying the whole buffer? This is precisely why rvalue references were added to C++.
Rvalue References
C++11 added rvalue references that refer to temporary objects for the purpose of “stealing” their resources for efficiency. (Plain references have since been known as lvalue references.) Rvalue references are declared with &&
.
The previous example can be augmented:
class container {
public:
// ...
void push_back( std::string const &s );
void push_back( std::string &&s ); // rvalue reference
};
int main() {
container c;
std::string s{ "hello" };
c.push_back( s ); // push_back( std::string const& );
c.push_back( "world" ); // push_back( std::string&& );
}
As before, in the case of "world"
, the compiler will create a temporary string
object. But now, the new overload of push_back( std::string &&s )
allows the compiler to call that function preferentially with the temporary object bound to the rvalue reference. The implementation of that push_back()
will “steal” (move) the string rather than copy it.
However, this requires cooperation from std::string
that must provide its own handling of rvalue references. A simplified partial declaration is:
class string {
public:
string();
string( char const *s );
string( string const &s ); // copy constructor
string( string &&s ); // move constructor
string& operator=( char const *s );
string& operator=( string const &s ); // copy assignment
string& operator=( string &&s ); // move assignment
// ...
private:
char *_buf;
size_type _cap, _len; // capacity & length
};
Specifically, string
now also has a move constructor and move assignment operator that constructs or assigns from an rvalue reference bound to a temporary string
. The move constructor is implemented something like:
string::string( std::string &&from ) :
_buf{ std::exchange( from._buf, nullptr ) },
_cap{ std::exchange( from._cap, 0 ) },
_len{ std::exchange( from._len, 0 ) }
{
}
That is it copies _buf
(just the pointer, not the string), _cap
, and _len
and sets from._buf
to nullptr
and both from._cap
and from._len
to 0. This effectively “steals” (moves) from
’s string leaving it empty — which is fine because, remember, it’s a temporary object that will soon be destroyed anyway. (The move assignment operator is implemented similarly.)
The addition of move constructors and move assignment operators to classes improves their efficiency with temporary objects, but only when at least one of their data members is a non-cheaply-copied type. A class like:
class point2d {
int _x, _y;
public:
point2d() : point2d{ 0, 0 } { }
point2d( int x, int y ) : _x{ x }, _y{ y } { }
// ...
};
wouldn’t benefit from adding a move constructor or move assignment operator because copying int
s is already cheap and can’t be made more efficient.
Default Move Constructors & Assignment Operators
Just as the compiler will automatically synthesize copy constructors and copy assignment operators for your class when possible, it will also synthesize move constructors and move assignment operators for your class when possible.
So when should you implement them yourself? The general rule is: if it’s necessary to implement the copy constructor or copy assignment yourself, you should at least think about whether it would be more efficient to implement their move counterparts yourself. This is part of the rule of 3/5/0.
std::move(T)
Consider the following:
void f( container *pc ) {
std::string s{ "hello" };
pc->push_back( s ); // push_back( std::string const& )
}
This will call push_back( std::string const& )
and copy the string even though we can see that s
is about to be destroyed by returning from the function. Why doesn’t the compiler call push_back( std::string&& )
instead? Because s
has a name (remember: if something has a name, it’s not an rvalue) and the compiler isn’t smart enough to see that s
will soon be destroyed anyway.
To get the compiler to call push_back( std::string&& )
, we have to convert the lvalue reference that is s
to an rvalue reference. This is precisely what std::move()
does:
void f( container *pc ) {
std::string s{ "hello" };
pc->push_back( std::move( s ) ); // push_back( std::string&& )
}
Using std::move()
tells the compiler that you want to call a function that accepts an rvalue reference, if one exists. (If no such function exists, the use of std::move()
will have no effect and the compiler will call a function that accepts an lvalue reference instead.)
A typical implementation of std::move()
is:
template<typename T>
inline std::remove_reference_t<T>&& move( T &&arg ) {
return static_cast<std::remove_reference_t<T>&&>( arg );
}
Hence, it’s really just a shorthand for a verbose static_cast
to an rvalue reference. Being just a cast, std::move()
:
- Incurs zero run-time performance penalty — it’s strictly compile-time.
- Is a misnomer since it doesn’t actually move anything.
- Does not guarantee that the argument will actually be moved.
The cast to an rvalue reference allows the compiler to call functions that have an rvalue reference parameter making the argument eligible to be moved, but the called functions are the ones that actually do the moving.
Rvalue References in Class Hierarchies
Suppose you have a base and a derived class and each provides a move constructor in addition to its copy constructor:
class B {
public:
B( B const& );
B( B&& );
// ...
};
class D : public B {
public:
D( D const& );
D( D&& );
// ...
};
You might think the implementation of D
’s constructors would be:
D::D( D const &from ) :
B{ from }
{
}
D::D( D &&from ) :
B{ from } // WRONG: calls B( B const& )
{
}
However, the implementation of D::D( D&& )
is wrong because even though the type of from
is an rvalue reference causing it to receive rvalues, once received and bound to a name, it “decays” into a lvalue. (Remember: if something has a name, it’s not an rvalue.) To convert it back to an rvalue, you need to use std::move()
again:
D::D( D &&from ) :
B{ std::move( from ) } // correct: calls B( B&& )
{
}
Motivation for Forwarding References
Suppose you want to implement a wrapper function to create objects of any type T and pass a single argument. You might write something like:
template<typename T, typename Arg>
T create_T( Arg &arg ) {
// ...
return T{ arg };
}
However, if you attempt to use it with a literal like:
T x = create_T( 42 ); // error
you’d get an error because you can’t initialize a non-const&
with a literal. To fix this, you can add an overload:
template<typename T, typename Arg>
T create_T( Arg const &arg ) {
// ...
return T{ arg };
}
T x = create_T( 42 ); // ok (now)
While that works, suppose you now want to allow rvalue references as well. To do that, you’d have to add yet a third overload:
template<typename T, typename Arg>
T create_T( Arg &&arg ) {
// ...
return T{ std::move( arg ) };
}
T x = create_T( std::move( arg ) );
That’s rather tedious. If you think that’s bad, if create_T()
were to take two parameters, you’d need nine overloads to have every combination of non-const&
, const&
, and &&
. In general, you need 3N overloads for N parameters. Clearly, this is unworkable. This is precisely why forwarding references (aka, universal references) were added to C++.
Formally, such references are known as “forwarding references”; however, Scott Meyers coined the term “universal references” that describes what they do rather than what they’re for.
Forwarding References
The way to define create_T()
once and have it work with all non-const&
, const&
, and &&
combinations is:
template<typename T, typename Arg>
T create_T( Arg &&arg ) {
// ...
return T{ std::forward<Arg>( arg ) };
}
While std::move()
was replaced by std::forward()
, the declaration of create_T()
itself hasn’t changed. That’s because X&&
for type X in a “type deduction context” (template) is a universal reference meaning it can bind to any kind of reference. That is the same syntax of &&
can either be an rvalue reference or a universal reference depending on the declaration. This can be summarized as:
Case | Declaration | Rvalue Reference? |
---|---|---|
1 | T &&x = f(); |
✅ |
2 | void f( std::vector<T> &&arg ); |
✅ |
3 | void f( T &&arg ); |
❌ |
4 | void f( T const &&arg ); |
✅ |
Cases 1, 2, and 4 declare rvalue references (as always). However, case 3 — that must exactly match the pattern T&&
— declares a forwarding reference. Notice that even adding const
as in case 4 does not match the pattern.
Why did the C++ Committee make
&&
confusingly mean two different things depending on context? They didn’t. The fact that the syntax ofT&&
came to function as a forwarding reference is a happy (?) accident of the separate reference collapsing rules in C++. Adding a new syntax to declare forwarding references would have been a much bigger change to C++.
While that explains the Arg&&
in the declaration of create_T()
, what does std::forward()
do? While std::move()
always converts and lvalue reference to an rvalue reference, std::forward()
forwards a reference as-is. In the line:
return T{ std::forward<Arg>( arg ) };
arg
has “decayed” into a lvalue reference because it has a name. (Remember: if something has a name, it’s not an rvalue.) If Arg
is an rvalue type, then std::forward()
behaves like std::move()
and converts arg
(now an lvalue because it has a name) back into an rvalue. However, if Arg
is an lvalue type, then std::forward()
leaves it alone. This is known as perfect forwarding.
Returning by Rvalue Reference
You might wonder whether it helps to return by rvalue reference like:
T&& f() { // return by rvalue reference
T v;
// ...
return std::move( v );
}
The intent is to avoid copying v
(that will be destroyed shortly anyway) and move it to its final destination instead. Unfortunately, this is equivalent to:
T& f() { // return by lvalue reference
T v;
// ...
return v; // WRONG: reference to temporary
}
This should now be obviously wrong because it will create a dangling reference to a destroyed object! To avoid copying, you simply return by value:
T f() { // return by value
T v;
// ...
return v; // correct: uses NRVO
}
In this one specific case, the compiler is smart enough to realize that v
should be treated as a temporary (despite having a name) and to elide the copy via the named return value optimization (NRVO).
You might also wonder whether returning by value and using std::move()
helps:
T f() { // return by value
T v;
// ...
return std::move(v); // WRONG: prevents NRVO
}
The short answer is no. The reason is that using std::move()
results in an rvalue expression (that has no name) and you can’t do NRVO with no name!
There are actually a very few legitimate cases for returning by rvalue reference. For example, both
std::move()
andstd::forward()
do. Generally, however, it’s very likely wrong.
Miscellaneous Reference Declarations
You might also wonder whether any of the following rvalue reference declarations are useful:
void f( T const &&p ) { // rvalue reference to const parameter
T &&x = g(); // local rvalue reference
}
The short answer is no:
A
const&&
parameter defeats the entire purpose of an rvalue reference that is to move objects. The reference must be non-const
since the move’d-from object is modified.An rvalue reference local variable is also useless. While it can bind to an rvalue expression, it immediately “decays” into an lvalue because it has a name. (Remember: if something has a name, it’s not an rvalue.)
Conclusion
C++ has always had lvalue references:
- As syntactic sugar for overloaded operator arguments.
- To pass and return non-cheaply-copied types to and from functions.
C++11 added rvalue references:
- For moving objects (to avoid copying).
C++11 also added forwarding references:
- Forwards function parameter references as-is for perfect forwarding.
Top comments (0)