C++ Move Semantics

Move Semantics is interesting in context of modern C++ which is introduced with C++11. It deals with transferring ownership and avoiding creating copies which is expensive computation at large scale. But before directly jumping to it we have to cover a few basic concepts:

  • LValue & RValue
  • References
  • The cast - std::move

LValues & RValues

Traditionally the terms came from the position of a variable in an assignment statement L = R

LValue (Left Value): Anything that can appear on the Left side of an assignment and RValue (Right Value): Anything that can only appear on the Right side of an assignment.

However, in C++, we need to look at Memory and Identity.

The LValue has an identity, we can take its address using the & operator and it’s lifespan persists beyond a single expression. While RValue is temporary. It does not have a stable address in memory that we can access and it usually dies immediately after the semicolon.

Let’s have a look at code examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
// The LValue example where we can identify them and can point to their memory location
// x and p are LValue
int x = 10;
int* p = &x;
x = 20;

// The RValue example where we cannot identify them neither we can point to their memory location
int x = 10;
int y = x + 5;

// 10 is RValue
10 = x;
&10;

This is so normal, we have been doing this for so long in so many programming language. The concept of References makes LValue & RValue very important in modern C++.

References

In older versions of C++, until C++03, references in are essentially aliases. Just another name for an existing variable. However, C++11 expanded this concept significantly to optimize performance. We will try to understand this with help of a few examples but first let’s look at the types of references.

The LValue Reference T&

This is the classic reference. It acts as an alias for an existing object/variable and we use this for modifying variables inside a function without copying them.

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

void increment(int& num) {
num++;
}

int main() {
int x = 10;
int& ref = x;

increment(x);
}

In the code above, we are passing x as a reference to the function increment which will change the value of x. At line 9, x will become 11. And the variable ref is just an alias for variable x. We can use them interchangeably.

The Const Reference const T&

We have been using this since a long time in C++ for passing large objects to functions efficiently and we don’t want to change the object we are passing inside the function. Efficiency is achieved by avoiding pass by value (i.e. making a copy that can be computation heavy).

It can bind to anything, but we cannot modify it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void print(const int& val) {
// note: we cannot modify val here
std::cout << val << std::endl;
}

int main() {
int x = 10;

// passing LValue - x
print(x);

// passing RValue - 20
print(20);
}

Note: If we bind a const reference to a temporary object (passing 20 to printValue, 20 is RValue), C++ compiler extends the lifetime of that temporary object/value until the reference goes out of scope.

The RValue Reference T&&

This has been added in C++11 which is an alias particularly for the temporary objects or values that are about to be destroyed. It only binds to rvalues.

1
2
3
4
5
6
7
8
9
10
11
12
void process(int&& temp) {
// 'temp' is a temporary object.
// We can modify it or steal its resources safely
temp += 5;
}

int main() {
int x = 10;

process(20);
process(x + 5);
}

The cast - std::move

The std::move() name is bit misleading. It doesn’t actually move anything, it strictly performs a cast. It casts an lvalue into an rvalue.

1
2
3
4
5
6
7
8
9
10
11
12
#include <utility>

void transfer_resource(int&& r)
{
// some logic
}

int main() {
int x = 10;
transfer_resource(std::move(x));
return 0;
}

Let’s understand the code above. Assuming we have a function transfer_resource which has logic to transfer properties of an object to another. We cannot pass x to transfer_resource directly because it is not a RValue, but we can convert LValue to RValue using the cast operator std::move.

Now we are ready to learn and explore the move constructor.

Move Constructor

Before C++11, copying objects could be expensive because resources (heap memory, file handles, sockets) has to ve duplicated when copy is invoked. Move semantics lets us transfer a resource from one object to another without expensive allocations.

Let’s consider a scenario where we have a vector containing a million of product objects (which can contain a field member product_name using std::string). If we try to pass it to functions or carry out assignments then we will face multi fold copy operations to generate duplicates.

Considering a minimal Product class definition:

1
2
3
4
5
6
7
8
9
10
11
class Product {
public:
std::string name;
std::string manufacturer;
double price;
short year;
};

int main() {
std::vector<Product> product_list;
}

For a vector with 1M products, here’s how many times the copy constructor will be invoked:

  • 1M calls to Product::Product(const Product&)
  • 2M calls to std::string‘s copy constructor (one for each of the two std::string members per Product)

A total of 3M operations will be executed. At scale, things become terrible if we have to make copies and most of the times these copies are not useful because they frequently run out of scope.

Why not pass by Reference?

Right catch! The references be it the normal or the const reference, can be used in the above case to avoid copy but there is a caveat. The problem with references is that it simply points to something. This means if we need new objects but want to avoid the computations we saw above, a new melanism is required. That’s where Move Constructor kicks in. We need a move constructor whenever we want C++ to create a new object, and references cannot create new objects.

Move constructors allows us to transfer an object from existing objects or objects which are going out of scope.

Move semantics were introduced in C++11 to eliminate unnecessary deep copies and enable efficient transfer of resources such as dynamic memory, file handles, buffers, and other non-trivial state. They allow objects to be moved rather than copied, resulting in significant performance improvements in modern C++ programs.

1
2
3
Product(Product&& move) noexcept
: name(std::move(move.name)), manufacturer(std::move(move.manufacturer)),
price(move.price), year(move.year) {}

A move constructor creates a new object by stealing internal resources from another object, rather than copying them.

Move Assignment Operator

The move assignment operator replaces the contents of an existing object by stealing resources from another object.

1
2
3
4
5
6
7
8
9
Product& operator=(Product&& move) noexcept {
if (this != &move) {
name = std::move(move.name);
manufacturer = std::move(move.manufacturer);
price = move.price;
year = move.year;
}
return *this;
}

Complete Code

Now we can look at the complete code where we can demonstrate the creation, copy and moving of objects using C++11 move constructors.

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <iostream>
#include <string>
#include <utility>

class Product {
private:
std::string _name;
std::string _manufacturer;
double _price;
short _year;

public:
// C++11 initializer list to assign default values
Product()
: _name(), _manufacturer(), _price(0.0), _year(0)
{
std::cout << "LOG: product CREATED - " << _name << std::endl;
}

Product(std::string name, std::string manufacturer, double price, short year)
: _name(std::move(name)), _manufacturer(std::move(manufacturer)), _price(price), _year(year)
{
std::cout << "LOG: product CREATED - " << _name << std::endl;
}

// the copy constructor
Product(const Product& copy)
: _name(copy._name), _manufacturer(copy._manufacturer), _price(copy._price), _year(copy._year)
{
std::cout << "LOG: product COPIED - " << _name << std::endl;
}

// the copy assignment operator
Product& operator=(const Product& copy) {
if (this != &copy) {
_name = copy._name;
_manufacturer = copy._manufacturer;
_price = copy._price;
_year = copy._year;
}
std::cout << "LOG: product properties COPIED using copy assignment - " << _name << std::endl;
return *this;
}

// the move constructor
Product(Product&& move) noexcept
: _name(std::move(move._name)), _manufacturer(std::move(move._manufacturer)),
_price(move._price), _year(move._year)
{
std::cout << "LOG: product properties MOVED - " << _name << std::endl;
}

// the move assignment operator
Product& operator=(Product&& move) noexcept {
if (this != &move) {
_name = std::move(move._name);
_manufacturer = std::move(move._manufacturer);
_price = move._price;
_year = move._year;
}
std::cout << "LOG: product properties MOVED using move assignment - " << _name << std::endl;
return *this;
}

// we are using the default destructor
~Product() = default;

// good way to avoid getter/setters
std::string to_string() const {
return "Product(name=" + _name +
", manufacturer=" + _manufacturer +
", price=" + std::to_string(_price) +
", year=" + std::to_string(_year) +
")";
}
};

int main() {
Product p1("Laptop", "Dell XPS 11", 114000, 2013);

std::cout << "P1 =" << p1.to_string() << std::endl;

// copy
Product p2 = p1;
// move
Product p3 = std::move(p2);

std::cout << "P3 =" << p3.to_string() << std::endl;
std::cout << "P2 =" << p2.to_string() << std::endl;

Product p4("Phone", "Samsung Galaxy S4", 55000, 2014);
Product p5("X", "Y", 1.0, 1990);

// copy assignment
p4 = p3;
std::cout << "P4 =" << p4.to_string() << std::endl;

// move assignment
p5 = std::move(p4);

std::cout << "P5 =" << p5.to_string() << std::endl;
std::cout << "P4 =" << p4.to_string() << std::endl;

return 0;
}

When we will execute the above code, we can see the logs which reflects the move and move via move assignment:

1
2
3
4
5
6
7
8
9
10
11
12
13
LOG: product CREATED - Laptop
P1 =Product(name=Laptop, manufacturer=Dell XPS 11, price=114000.000000, year=2013)
LOG: product COPIED - Laptop
LOG: product properties MOVED - Laptop
P3 =Product(name=Laptop, manufacturer=Dell XPS 11, price=114000.000000, year=2013)
P2 =Product(name=, manufacturer=, price=114000.000000, year=2013)
LOG: product CREATED - Phone
LOG: product CREATED - X
LOG: product properties COPIED using copy assignment - Laptop
P4 =Product(name=Laptop, manufacturer=Dell XPS 11, price=114000.000000, year=2013)
LOG: product properties MOVED using move assignment - Laptop
P5 =Product(name=Laptop, manufacturer=Dell XPS 11, price=114000.000000, year=2013)
P4 =Product(name=, manufacturer=, price=114000.000000, year=2013)

Closing Notes

Move semantics are one of the most transformative features introduced in C++11 which allow programs to avoid unnecessary deep copies and instead transfer ownership of resources efficiently. This leads to major performance improvements in scenarios such as returning large objects, passing objects by value, inserting into containers, vector reallocations and lot more.

So when you are going to implement move constructors in your C++ code?

Happy Coding. Bye!