C++ move semantics and rvalue reference
A collection of notes where I have tried to consolidate C++11 concepts such as move semantics, rvalue reference and forwarding, showing how these features affect compile time and runtime behavior of C++ programs.
Constructor, Copy Constructor and Move Constructor
Let’s consider a class with constructor, copy constructor, move constructor, copy assignment operator and move assignment operator. We’ll refer to it as CopyMoveAssign class, or CMS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CMS {
public:
int _value;
CMS() {
cout << "Default constructor " << endl;
}
CMS(int value) {
cout << "Constructor " << value << endl;
this->_value = value;
}
CMS(CMS &rhs) {
cout << "Copy constructor" << endl;
this->_value = rhs._value;
}
CMS& operator=(CMS &rhs) {
cout << "Copy assignment operator" << endl;
this->_value = rhs._value;
return *this;
}
CMS& operator=(CMS &&rhs) {
cout << "Move assignment operator" << endl;
this->_value = rhs._value;
rhs._value = -1;
return *this;
}
CMS(CMS &&rhs) {
cout << "Move constructor" << endl;
this->_value = rhs._value;
rhs._value = -1;
}
~CMS() {
cout << "Destructor" << endl;
}
};
Some first basic construction scenarios are shown below, with the code on the left and the output on the right.
1
2
3
4
5
6
7
CMS cms1(10); Constructor 10
CMS cms2(cms1); Copy constructor
cms2 = cms1; Copy assignment operator
CMS cms3 = cms2; Copy constructor
Destructor
Destructor
Destructor
These examples are all well known under C++98. It’s worth pointing
out is the invocation of the copy constructor on line 2 and 4.
In the context of C++11, the introduction of rvalue references allows to implement
move semantics: when constructing an object from a reference to an rvalue, the code
can transfer ownership of resources from that argument to the object being constructed,
with the awareness that the original one needs to be left in a consistent state but
the caller will not expect it to hold an initialized value anymore. As a matter
of fact, with a proper rvalue, the caller will not even be able to tell if the object
has been modified or not, due to the temporary nature of rvalues.
A simplified example of move semantics is implemented by the move constructor
of CMS
class. The old object loses ownership of a certain resource while still
being left in a consistent state. In this case there is solely an integer value
moved around: a more meaningful example would involve transferring ownership of a
dynamically allocated buffer while setting the old object’s pointer to nullptr
.
In the examples below, lines 2 and 4 result in a call to the move assignment
operator and move constructor.
1
2
3
4
5
6
7
8
CMS cms1(10); Constructor 10
cms1 = CMS(20); Constructor 20
Move assignment operator
Destructor
CMS cms2(CMS(10)); Constructor 10
Move constructor
Destructor
Destructor
On line 5, a temporary
object is created, which is again an rvalue and object cms2
is move constructed from it.
This is however not the default behaviour of gcc (version 4.9
in my case). The
compiler, if not asked otherwise, optimizes away the creation of the temporary.
Something very similar happens with RVO
(Return Value Optimization) and NRVO
(Named return value optimization). -fno-elide-constructors
will disable this behavior.
Moving from lvalues: std::move and std::forward
Sometimes it becomes necessary to treat lvalues
as rvalues
, thus allowing the
function being invoked to move from a specific argument.
rvalues
can be bound to rvalue
references or to lvalue
references to const
as in the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// fRvalueRef takes rvalue reference
void fRvalueRef(CMS&& cms) {
cout << cms._value << endl;
}
// fLvalueRefConst takes lvalue reference to const
void fLvalueRefConst(CMS const &cms) {
cout << cms._value << endl;
}
int main() {
fRvalueRef(CMS(10));
fLvalueRefConst(CMS(10));
}
It is not possible to bind a non-const lvalue
reference to an rvalue
. Calling mutable methods
on a temporary object is considered illegal as it’s probably a logic bug. The following
code:
1
2
3
4
5
6
7
void fLvalueRef(CMS &cms) {
cout << cms._value << endl;
}
int main() {
fLvalueRef(CMS(10));
}
will not compile:
A const reference to lvalue
(i.e. CMS& const) points to non-const object, therefore
this configuration is also not allowed. It is however possible to bind a lvalue
reference
to const to an rvalue
, as it is guaranteed that no change will be applied to the temporary object:
1
2
3
4
5
6
7
8
9
10
// fLvalueRefConst takes a lvalue reference to const
void fLvalueRefConst(const CMS& cms) {
cout << cms._value << endl;
}
int main() {
CMS cms(10);
fLvalueRefConst(std::move(cms));
cout << cms._value << endl;
}
std::move
carries out the operation of turning an lvalue
into an rvalue
,
by returning an rvalue reference
to its argument, which will eventually bind
according to the rules above. An rvalue reference
cannot bind to an lvalue
:
when working with an rvalue
, we assume we fully understand the scope of the
object and that it won’t survive past that, which is not the case for an lvalue
.
The following code:
1
2
3
4
5
6
7
8
void fRvalueRef(CMS&& cms) {
cout << cms._value << endl;
}
int main() {
CMS cms(10);
fRvalueRef(cms);
}
will not compile:
std::move
can be used to turn the lvalue
into and rvalue
. The reference argument of
fRvalueRef
will then bind to it:
1
2
3
4
5
6
7
8
void fRvalueRef(CMS&& cms) {
cout << cms._value << endl;
}
int main() {
CMS cms(10);
fRvalueRef(std::move(cms));
}
std::move
tells the compiler that the object is eligible to be moved from and
that we don’t care anymore about it holding an
initialized value. If that object can be used to construct more efficiently a copy, by
moving from the object itself, the compiler will do so. In the following code, instead of
copy constructing the argument to fRvalueRef
, it is move constructed, and the original object
will later hold a non-initialized value:
1
2
3
4
5
6
7
8
9
void fLvalue(CMS cms) {
cout << cms._value << endl;
}
int main() {
CMS cms(10);
fLvalue(std::move(cms));
cout << cms._value << endl;
}
The result is the following:
As std::move
, std::forward
is also responsible for casting the argument to an rvalue,
but it does so only if its argument was initialized with an rvalue. std::forward
is normally
used with function templates taking forwarding references (
T&&`). For example, consider the following
template function:
1
2
3
4
5
6
7
8
9
10
11
12
13
void fLvalue(CMS cms) {
cout << cms._value << endl;
}
template<typename T> void fUniversalRef(T&& param) {
fLvalue(param);
}
int main() {
CMS cms(10);
fUniversalRef(cms);
cout << cms._value << endl;
}
On invocation of fLvalue
, the argument will be copy constructed. We could explicitly ask for it
to be move constructured by invoking fLvalue(std::move(param))
. This would work, but param
is an lvalue
reference (T&&
is a forwarding reference, which follow specific rules for
type deducation), therefore the initial cms
object would be moved from, and would be invalid
at the end of main
. This is accepted, as fUniversalRef
doesn’t give any guarantee on the const-ness
for param
. The following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
void fLvalue(CMS cms) {
cout << cms._value << endl;
}
template<typename T> void fUniversalRef(T&& param) {
fLvalue(std::move(param));
}
int main() {
CMS cms(10);
fUniversalRef(cms);
cout << cms._value << endl;
}
would result in:
fUniversalRef
could decide to move from param
only if it was initially an rvalue
with std::forward
.
The following call would therefore result in calling the copy constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
void fLvalue(CMS cms) {
cout << cms._value << endl;
}
template<typename T> void func(T&& param) {
fLvalue(std::forward<T>(param));
}
int main() {
CMS cms(10);
func(cms);
cout << cms._value << endl;
}
It’s sufficient to cast cms
to an rvalue
when invoking func
, to make std::forward
cast param
to an rvalue
as well, invoking the move constructor.