Returning references in C++
Since C++ 11 introduces rvalue references, let's see which rules apply when they are returned by functions.
An overview of rvalue references and move semantics is given here. Some of the rules that should be noted are:
Those rules are implicitly used in the following examples.
- Rvalue references usually binds to a temporary variable, i.e. value returned from a function.
- Rvalue references allow a function to branch at compile time (via overload resolution) on the condition "Am I being called on an lvalue or an rvalue?"
- Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
A& &
becomesA&
;A& &&
becomesA&
;A&& &
becomesA&
;A&& &&
becomesA&&
.
Let's first define trivial class which is going to be used by the rest of the code.
class X { public: X() { cout << "X::X()" << endl; } X(const X& x) { cout << "X::X(const X&)" << endl; } X(X&& x) { cout << "X::X(X&&)" << endl; } X& operator=(const X& x) { cout << "X& X::operator=(const X&)" << endl; return *this; } X& operator=(X&& x) { cout << "X& X::operator=(const X&)" << endl; return *this; } string toString() const { return "string X::toString()"; } };
A number of functions will be defined which return values and lvalue/rvalue references. To understand which constructor is called when a reference is returned from a function, three cases are distinguished: value is returned, lvalue reference is returned, rvalue reference is returned.
The first case has three variants:
X val() { return X(); } X val_lref() { X x; return static_cast<X&>(x); } X val_rref() { return move(X()); }
val()
returns temporary value, val_lref()
casts a variable to lvalue reference and then returns it,
val_rref()
casts to rvalue reference (because that's what move
really does) and then returns. In each case,
X
has to be returned, so some of the constructors have to be called (see below for more details).
The second case also has three variants:
X& lref_val() { X x; return x; } X& lref_lref() { X x; return static_cast<X&>(x); } const X& lref_rref() { return move(X()); }
lref_val()
returns lvalue reference to temporary, lref_lref()
returns X& &
which is equivalent
to X&&
and lref_rref()
returns X& &&
which is equivalent to
X&
(const
is needed because temporary is bound to the reference).
Third case has two variants:
X&& rref_val() { return X(); } X&& rref_rref() { return move(X()); }
rref_val()
returns rvalue reference to temporary value and rref_rref()
returns X&& &&
which is equivalent to X&&
. Binding lvalue reference to rvalue reference
X&& rref_lref() { X x; return static_cast<X&>(x); }
Let's see some examples in case the returning value is accepted with variable of type X
.
X a1 = val(); // (1) X a2 = val_lref(); // (2) X a3 = val_rref(); // (3) X a6 = lref_rref(); // (4) X a7 = rref_val(); // (5)
X
is returned and the return value optimization (RVO) is performed. For
(2) the default constructor is called, copy constructor is called, X
is returned and RVO is performed. Similar approach is with rvalue
reference at (3) - the default constructor is called, move constructor is called, X
is returned and RVO is performed.
If reference is returned as in (4) then the default constructor is called, X&
is returned and the copy constructor is called.
Returning rvalue reference produces similar result in (5) - the default is constructor called, X&&
is returned and the move
constructor is called.
Let's see some examples in case the returning value is accepted with variable of type X&
.
const X& b3 = val_rref(); // (6) const X& b7 = rref_val(); // (7)
X
is returned and a lvalue reference to temporary is created. For the example (7) the default constructor is called,
X&&
is returned and lvalue reference of the copy of the returned reference is created.
The last group of examples is when the returning value is accepted with variable of type X&&
.
X&& c8 = rref_rref(); // (8)
X&&
is returned and lvalue reference of the copy of the returned
rvalue reference is created.
All samples including those not described in the text can be found in the source code.
/* references.cpp -------------- Deals with lvalue/rvalue references and move function. Compile with g++ -std=c++11 -oreferences references.cpp */ #include <iostream> #include <utility> #include <cstdlib> #include <string> using namespace std; class X { public: X() { cout << "X::X()" << endl; } X(const X& x) { cout << "X::X(const X&)" << endl; } X(X&& x) { cout << "X::X(X&&)" << endl; } X& operator=(const X& x) { cout << "X& X::operator=(const X&)" << endl; return *this; } X& operator=(X&& x) { cout << "X& X::operator=(const X&)" << endl; return *this; } string toString() const { return "string X::toString()"; } }; // returns X X val() { return X(); } // returns X X val_lref() { X x; return static_cast<X&>(x); } // returns X X val_rref() { return move(X()); } // returns X& X& lref_val() { X x; return x; } // returns X& & which is equivalent to X& X& lref_lref() { X x; return static_cast<X&>(x); } // lvalue reference must be const because temporary must bound to const lvalue reference // returns X& && which is equivalent to X& const X& lref_rref() { return move(X()); } // returns X&& X&& rref_val() { return X(); } // binding lvalue reference to rvalue reference is not allowed /* X&& rref_lref() { X x; return static_cast<X&>(x); } */ // returns X&& && which is equivalent to X&& X&& rref_rref() { return move(X()); } int main() { X a1 = val(); // default constructor called, X returned, return value optimization performed X a2 = val_lref(); // default constructor called, copy constructor called, X returned, return value optimization performed X a3 = val_rref(); // default constructor called, move constructor called, X returned, return value optimization performed X a4 = lref_val(); // default constructor called, X& returned, copy constructor called X a5 = lref_lref(); // default constructor called, X& returned, copy constructor called X a6 = lref_rref(); // default constructor called, X& returned, copy constructor called X a7 = rref_val(); // default constructor called, X&& returned, move constructor called X x8 = rref_rref(); // default constructor called, X&& returned, move constructor called // lvalue references cannot bind to temporaries, so const lvalue references have to be used const X& b1 = val(); // default constructor called, X returned, created lvalue reference to temporary const X& b2 = val_lref(); // default constructor called, copy constructor called, X returned, created lvalue reference to temporary const X& b3 = val_rref(); // default constructor called, move constructor called, X returned, created lvalue reference to temporary const X& b4 = lref_val(); // default constructor called, X& returned, created lvalue reference of the copy of the returned reference const X& b5 = lref_lref(); // default constructor called, X& returned, created lvalue reference of the copy of the returned reference const X& b6 = lref_rref(); // default constructor called, X& returned, created lvalue reference of the copy of the returned reference const X& b7 = rref_val(); // default constructor called, X&& returned, created lvalue reference of the copy of the returned reference const X& b8 = rref_rref(); // default constructor called, X&& returned, created lvalue reference of the copy of the returned reference X&& c1 = val(); // default constructor called, X returned, created rvalue reference to temporary X&& c2 = val_lref(); // default constructor called, copy constructor called, X returned, created rvalue reference to temporary X&& c3 = val_rref(); // default constructor called, move constructor called, X returned, created rvalue reference to temporary // X&& c4 = lref_val(); // default constructor called, X& returned, cannot create rvalue reference of the lvalue reference // X&& c5 = lref_lref(); // default constructor called, X& returned, cannot create rvalue reference of the lvalue reference // X&& c6 = lref_rref(); // default constructor called, X& returned, cannot create rvalue reference of the lvalue reference X&& c7 = rref_val(); // default constructor called, X&& returned, created lvalue reference of the copy of the returned reference X&& c8 = rref_rref(); // default constructor called, X&& returned, created lvalue reference of the copy of the returned reference return EXIT_SUCCESS; }
Gcc 4.8.1 on Linux is used for testing these samples, but any C++ 11 compliant compiler should work.