Move Semantics in C++
This is a note for Lecture 13, CS106L, Spring 2023.
DEFINITION
L-VALUE
l-value can appear on the left or right of an =
.
For example, here x
is an l-value:
1 | int x = 3; |
- l-values have names
- l-values are not temporary
- l-values live until the end of the scope
R-VALUE
r-value can ONLY appear on the right of an =
For example, here 3
is an r-value:
1 | int x = 3; |
- r-values don't have names
- r-values are temporary
- r-values live until the end of the line
EXAMPLES
1 | int x = 3; // 3 is an r-value |
MOVE SEMANTICS
In our generic vector
class, we have a vector copy assignment operator like this:
1 | template <typename T> |
Aside: std::copy
is a generic copy function used to copy a range of elements from one container to another.
And in the code fragment below:
1 | int main() { |
vec
is created using the default constructormake_me_a_vec
creates a vector using the default constructorvec
is reassigned to a copy of that return value using copy assignment- copy assignment creates a new array and copies the contents of the old one
- The original return value's lifetime ends and it calls its destructor
vec
's lifetime ends and it calls its destructor
Here is a problem: make_me_a_vec(123)
is an r-value, and in vector<T>::operator=(const vector<T>& other)
, other
should be an l-value (referenced using &). Can r-values be bound to const &
?
The answer is Yes.
Another problem is that, we creates a vector, copies its content to another and deleted it. Can we do better?
We can use move assignment like this:
1 | template <typename T> |
But what about this?
1 | int main() { |
A problem occurs here!We need both a copy assignment AND a move assignment.
How do we know when to use move assignment and when to use copy assignment?
When the item on the right of the = is an r-value we should use move assignment.
Why? r-values are always about to die, so we can steal their resources.
1 | int main() { |
1 | int main() { |
And now the question is: how to make two different assignment operator?
Answer: Overload vector::operator=
!
Introducing... the r-value reference using &&
R-VALUE REFERENCE
By using r-value reference, we can do this:
1 | int main() { |
So, we should keep our copy assignment:
1 | vector<T>& operator=(const vector<T>& other) { |
And overload vector::operator=
(move assignment) like this:
1 | vector<T>& operator=(vector<T>&& other) { |
But actually, we still copy _size
and _capacity
, etc.
Introducing...std::move
!
std::move
std::move(x)
doesn't do anything except cast x as an r-value- It is a way to force C++ to choose the
&&
version of a function
1 | int main() { |
We can modify our move assignment like this:
1 | vector<T>& operator=(vector<T>&& other) { |
This works!
1 | int main() { |
However, what if we wanted to declare and initialize a vec on the same line?
1 | int main() { |
Similarly, vector<string> vec1 = {"hello", "world"};
will use move constructor.
1 | vector<T>(vector<T>&& other) { |
Where else should we use std::move
?
Rule of Thumb:
Wherever we take in aconst &
parameter in a class member function and assign it to something else in our function
(TO BE CONTINUED)
For example:
1 | // copy push_back |
Be careful with std::move
1 | int main() { |
Rule of Thumb:
Wherever we take in aconst &
parameter in a class member function and assign it to something else in our function
Don't usestd::move
outside of class definitions, never use it in application code!
TLDR
If your class has copy constructor and copy assignment defined, you should also define a move constructor and move assignment
Define these by overloading your copy constructor and assignment to be defined for
Type&& other
as well asType& other
Use
std::move
to force the use of other types' move assignments and constructorsAll
std::move(x)
does is castx
as an r-valueBy wary of
std::move(x)
in main function code!
PHILOSOPHY about SMFs
The 6 Special Member Functions
- Default constructor: Initializes an object to a default state
- Copy constructor: Creates a new object by copying an existing object
- Move constructor: Creates a new object by moving the resources of an existing object
- Copy Assignment Operator: Assigns the contents of one object to another object
- Move Assignment Operator: Moves the resources of one object to another object
- Destructor: Frees any dynamically allocated resources owned by an object when it is destroyed
Some Philosophy about SMFs
There are three guiding rules:
Rule of Zero
- If you can avoid defining default operations, do
- Why? It's the simplest and gives the cleanest semantic
- Example: Since
std::map
andstd::string
have all the special functions, no further work is needed.
1 | Class Named_map { |
Rule of Three
- If you need to implement a custom destructor, you almost certainly need to define a copy constructor and copy assignment operator
- Why? You are probably managing your own memory somehow, so the shallow copies provided by the default operations won't work correctly
Rule of Five
- If you define custom copy constructor/assignment operator, you should define move constructor/assignment operator as well
- Why? This is about efficiency rather than correctness. It's inefficient to make extra copies (although it's "correct")