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 | // The LValue example where we can identify them and can point to their memory location |
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 |
|
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 | void print(const int& val) { |
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 | void process(int&& temp) { |
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 |
|
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 | class Product { |
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 | Product(Product&& move) noexcept |
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 | Product& operator=(Product&& move) noexcept { |
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 |
|
When we will execute the above code, we can see the logs which reflects the move and move via move assignment:
1 | LOG: product CREATED - Laptop |
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!