Grasping Move Semantics in C++
- Rodolfo Fava
- Aug 2
- 5 min read
Introduction
Why learn about move semantics?
In one word, efficiency. In strictly performance terms, moving an object from A to B is considerably cheaper than copying it. However, there are cases where you want to perform a copy even though it is technically a more expensive operation than moving, and that's due to intent. And that intent will boil down to one thing: how to manage the ownership of the object. Before deciding to implement move semantics in your project, it's necessary to analyse whether it's necessary and if it achieves the expected outcomes of the solution being designed.
The goals of this post:
Explain move semantics.
Compare moving with copying.
Explain the theory and use of perfect forwarding.
Understanding Move Semantics
Before discussing move semantics, it's crucial to understand its building blocks first.
LValues and RValues
An lvalue is an expression that identifies an object which can be modified (provided they don't hold a const type). An lvalue has a name, and an identifiable location in memory that can be accessed using '&'. An easy way to spot an lvalue is if it can be on the left side of an assignment operation.
Examples of lvalue:
int age = 25;
string name = "John";
float time = 2.5f;
float distance = 2000;In the code snippet above, age, name, time, and distance are lvalues.
An rvalue, on the other hand, does not have an identifiable location in memory and thus cannot be accessed with '&'. Examples include literals, function calls that return a non-reference type, and temporary objects.
Examples of rvalue:
int age = 25;
string name = "John";
float time = 2.5f;
float distance = 2000.f;
int Result = IncrementFunction(age);
int IncrementFunction(int Argument)
{
return ++Argument;
}In the code snippet above, the literals are highlighted and located on the right-hand side of the assignment operations. The IncrementFunction creates a temporary object to store the return value and is, by definition, also an rvalue.
The code above is a simple example to illustrate rvalues. Still, it's worth mentioning that modern compilers already have built-in optimisations (Copy Elision including RVO - Return Value Optimisation) which avoid the copy and move operations into a temporary object altogether by constructing the return value directly in the caller's memory.
RValue References (T&&)
C++11 introduced the concept of rvalue references, which consist of an rvalue bound to an rvalue reference and being modifiable through it. Its syntax is the double ampersand &&. This is a significant change, given that it enables the reuse of resources from expiring objects, which leads to better code performance. Below is an interactive code example of an rvalue reference storing a string value from the SayHello() function, and then being modified.
Move Semantics
To optimise the use of temporary values in code, C++11 allows the use of move semantics. That means, through the use of std::move, we can transfer the ownership of an object from A to B, and in turn, the source A turns into an unspecified state. See the example code below.
When trying to print IntA's value after the move operation to IntB, it may return a garbage value because of its unspecified state (still valid to reassign and destroy, but its contents are no longer reliable). After the move operation, the ownership is now IntB's, no longer IntA's. A common analogy used by coders is: it "steals" the resource from one variable to another. And that's why the move operation should be done carefully to avoid situations of dangling references.
The move operation under the hood:
It's a cast operation to an rvalue reference. Let's have a look at the assembly code the compiler generated for our move operation (https://godbolt.org/ is a great source for this!)
C++:
CustomInteger IntB(std::move(IntA));Assembly:

So if you replace std::move with the rather cumbersome cast (CustomInteger&&), you will notice the same result.
CustomInteger IntB((CustomInteger&&)IntA);Even so, for better readability and to convey intent, std::move is preferable.
Moving vs Copying
Although moving is more performant than copying objects, there is still a case for choosing a copy operation over a move one.
Copying trivial data
Primitives: int, float, bool (...).
FVector (e.g. position or rotation).
The difference in performance is often negligible, only noticeable in large values.
Try the code sample below to see a quick benchmark comparison!
When copies are preferred:
If you copy one actor's mesh to another, you probably don't want to invalidate the source's mesh.
Semantics Correctness - where two independent copies are needed.
When duplicating enemy characters in a horde, they will have different stats and health states, for instance.
And there are cases where you want to transfer ownership of an object across different functions. Such as:
Setting a weapon's owner actor, since each weapon can only have one owner actor at a time.
Setting the NPC's current exclusive AI behaviour state, as it cannot have more than one state simultaneously.
Defining the exclusive ability of the character.
In these instances where the intent is to preserve unique ownership of an object, move semantics can and should be used.
Perfect forwarding
As part of the move semantics, C++11 also brings about perfect forwarding. What for? To pass arguments preserving the original lvalue or rvalue properties when forwarding arguments. Without it, because of reference collapsing, T& &, T& &&, and T&& & all collapse to T&, while T&& && collapses to T&&. This is extremely limiting and downright frustrating, since it misses the whole point of using lvalue and rvalue, and used to be a real issue before C++11. See the table below to visualise what reference collapses are.
When passing | It collapses to | Meaning |
T& & | T& | When passing an lvalue reference to an lvalue reference, it collapses to an lvalue reference. |
T& && | T& | When passing an lvalue reference to an rvalue reference, it collapses to an lvalue reference too. |
T&& & | T& | When passing an rvalue reference to an lvalue reference, it also collapses to an lvalue reference. |
T&& && | T&& | When passing an rvalue reference to an rvalue reference, it remains an rvalue reference. |
The C++11 solution was to combine forwarding references with std::forward. The syntax for it looks like this:
template<typename T>
void wrapper(T&& arg)
{
func(std::forward<T>(arg));
}Let's breakdown these two new concepts below:
Forwarding References
Taking advantage of template argument deduction, the compiler can deduce if the type T is an lvalue (T&) or rvalue (T&&) during compile time, and generates two different versions of the function for T& and T&&. During runtime, the program just executes either of the functions that is appropriate according to the argument type.
It should be noted that forwarding references (T&&) only work with template argument deduction.
Forwarding with std::forward
std::forward preserves the lvalue or rvalue properties of the arguments, and lets the recipient handle it optimally.
std::forward<T>(arg)std::forward casts arg to T&&(rvalue) if T is not an lvalue reference, or just passes the argument as an lvalue. std::move, on the other hand, casts the argument to an rvalue (T&&) regardless.
See std::forward demonstrated by the factory function below.
Features | std::move | std::forward |
Purpose | Unconditionally enables moves | Preserve lvalue and rvalue properties in template function calls. |
Usage | Move constructors and assignments | Perfect Forwarding (e.g. factories) |
Effect | Always casts to T&& | Casts to T&& or T& (Conditional) |
Move semantics and perfect forwarding are powerful tools for optimising resource management in C++ applications. Use moves for transfers of ownership, and copies when independent states are required.
Have you ever encountered interesting situations with move semantics and perfect forwarding? Let me know in the comments!
References:
Comments