Â
Â
This is a summary of Klaus Iglberger talk at CppCon 2019 titled “Back to Basics: Move Semantics (part 1 of 2)”.
What does it mean to move something?
Moving an object in C++ allows us to avoid a lot of wasteful copying in our projects.
Consider the function called below.
std::vector<int> createVector() { return {1, 2, 3}; } std::vector<int> v2; v2 = createVector();
Within the scope of
createVector()
a temporary object is created. On the assignment to v2
we do not want to create a deep copy of the temporary object since it will be destroyed once the function goes out of scope. Move allows us to efficiently transfer data from the temporary vector to v2
. In Klaus talk he simplifies a vector as having three fields: begin pointer, end pointer and pointer for end of memory allocated to vector. Our move operation will instead copy the three aforementioned pointers into v2
from the temporary object and set the three pointers in the temporary object to null.Lvalue vs. Rvalue
An lvalue is an object that occupies an identifiable location in memory. An rvalue is an object that does not represent an identifiable locaiton in memory.
int a = 2; // a is lval, 2 is rval std::string b = "blah"; // b is lval, b is rval auto res = createVector(); // res is lval, object created in function is rval
Functions can take an lvalue or an rvalue as input parameter.
&
for lvalue, &&
for rvalue.void someFunction(vector& vec); // 1. takes an lvalue void someFunction(vector&& vec); // 2. takes an rvalue vector<int> v1 = {1, 2}; someFunction(v1); // calls (1) someFunction({1, 2, 3}); // calls (2)
Move Constructor and Move Assignment Operator
Since C++11, classes have two additional member functions: move constructor and move operator.
class SomeClass { private: int i; std::string str; int *ptr = nullptr; public: SomeClass(SomeClass&& s); // move constructor SomeClass& operator=(SomeClass&& s); // move operator };
The goal of the move constructor is to transfer the content of
s
into the current object and leave s
in a valid but undefined state. In the code below we define a simple move constructor for SomeClass
. We see that the move constructor simply calls move
on all field of s
to achieve the transfer of contents. Before we terminate, we set s.ptr
to nullptr
to ensure that s
is left in a valid state. The C++ core guidelines also advises us to declare move constructors as noexcept
because none of the operations in our move constructor should throw exceptions. According to Klaus' experiments, using no except provided a 60% speedup.SomeClass::SomeClass(SomeClass&& s) noexcept : i ( std::move(s.i) ), str ( std::move(s.str) ), ptr ( std::move(s.ptr) ) { s.ptr = nullptr; }
The goal of the move assignment operator is to clean of all visible resources, transfer the contents of s and leave
s
in a valid but undefined state. delete
handles the deletion of cleaning up of resources. Once again, we move every single field of s
into our current object. Finally, we leave s
in a defined state by setting its ptr
field to nullptr
.SomeClass::SomeClass& operator=(SomeClass&& s) { delete ptr; i = std::move(s.i); str = std::move(s.str); ptr = std::move(s.ptr); s.ptr = nullptr; return *this; }
When will the compiler generate move operations (move constructor and move operator)?
If no copy operation (constructor or assignment operator) or destructor is user-defined then the compiler will NOT generate move operations.
If no move operation are user-defined, copy operations WILL be generated by the compiler.
One thing to note is that definition member functions with
=default
or =delete
are considered default operations.When to define your move operations?
In general try to avoid defining any move operations, copy operations and destructors yourself (rule of zero). But there are times when resource management needs to be user defined. The default move operations will simply call move on every single field of the object. Because we used a raw pointer in
SomeClass
above, we have to cleanup its resources with delete ptr
.