# Specific Topics
# Move Semantics
Source: The Cherno
# lvalues and rvalues
lvalues
are called located values.
Sometimes the lvalue
is something on the left side of the =
sign and rvalue
is on the right side. But this does not always apply.
We cannot assign something to a rvalue. like in this:
int Getvalue()
{
return 10;
}
int main()
{
int i = GetValue(); // this is not allowed
GetValue() = 5;
}
2
3
4
5
6
7
8
9
10
Although if we transform Getvalue()
to return a lvalue
by returning an int reference
, which is called an lvalue reference, and we have to provide a storage for the value like an static int
, then we can assign a value to Getvalue()
.
int& Getvalue()
{
static int value = 10;
return value;
}
int main()
{
int i = GetValue();
GetValue() = 5;
}
2
3
4
5
6
7
8
9
10
11
Expanding this we can have a function that can be called by a rvalue
and a lvalue
, like this:
void SetValue(int value)
{
}
int main()
{
int i = 10;
SetValue(i); // lvalue
SetValue(10); //rvalue
}
2
3
4
5
6
7
8
9
10
Although, We cannot take a lvalue reference
from a rvalue
, so you can only have a lvalue reference
from a lvalue
.
So this will give me an error because of SetValue(10); //rvalue
. Because now it is a lvalue reference
function, due to the int&
.
void SetValue(int& value)
{
}
int main()
{
int i = 10;
SetValue(i); // lvalue
SetValue(10); //rvalue
}
2
3
4
5
6
7
8
9
10
While we cannot have a lvalue reference from a rvalue (int& a = 10
), we can have if it is a const (const int& a = 10
).
So if we use a const int& value
the function works with rvalues
and lvalues
. Because a const lvalue reference can accept both of them.
void SetValue(const int& value)
{
}
int main()
{
int i = 10;
SetValue(i); // lvalue
SetValue(10); //rvalue
}
2
3
4
5
6
7
8
9
10
Analysing the function below we have lvalues on the left side of the =
and rvalues on the right side.
int main()
{
std::string firstName = "Thiago ";
std::string lastName = "Souto";
std::string fullName = firstName + lastName;
}
2
3
4
5
6
7
If we add a function that takes a std::string& name
it will take on the lvalues.
So this will not work because of the PrintName(firstName + lastName)
, that's because firstName + lastName
is a rvalue
;
void PrintName(std::string& name)
{
std::cout << name << std::endl;
}
int main()
{
std::string firstName = "Thiago ";
std::string lastName = "Souto";
std::string fullName = firstName + lastName;
PrintName(fullName)
PrintName(firstName + lastName); // this doesn't work
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
That's why many functions use const
like so:
void PrintName(const std::string& name)
{
std::cout << name << std::endl;
}
int main()
{
std::string firstName = "Thiago ";
std::string lastName = "Souto";
std::string fullName = firstName + lastName;
PrintName(fullName)
PrintName(firstName + lastName);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
There is also the rvalue reference
which accepts only temporary objects, rvalues
. It looks the same as the lvalue reference, but it uses &&
instead of &
.
In this case, the following function will work only for PrintName(firstName + lastName);
.
void PrintName(const std::string&& name)
{
std::cout << name << std::endl;
}
int main()
{
std::string firstName = "Thiago ";
std::string lastName = "Souto";
std::string fullName = firstName + lastName;
PrintName(fullName)
PrintName(firstName + lastName);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
But how is this useful?
The main advantage has to do with optimization, if we know that we are taking in a temporary object, we don't have to make sure that we keep it alive, or intact, or copy, we can simply steal the resources that might be attached to that specific object and use somewhere else; Because we know its temporary.
Summarizing:
lvalues
are variables that have some kind of storage backing them andrvalues
are temporary values;lvalues references
can only takelvalues
, unless they areconst
;rvalues references
can only take the temporaryrvalues
.
# Move semantics
Move semantics is the single biggest use case for rvalues
and rvalue references
.
It basically allows us to just move objects around, this was not possible before C++11, because C++11 introduced rvalue references.
There are a lot of cases where we don't want to copy an object from one place to another place but that's the only place we can get the information we want.
This is a problem if the object have to heap allocate memory or if it is a string you have to create heap allocation, that a heavy object to copy.
Heap Allocation:
# Example of move semantics
For the example We have a basic construct here that takes in a string and allocates memory and copies everything into that memory buffer. Also a destructor for that class that deletes it.
Another class that will consume the string.
#include <iostream>
using namespace std;
class String {
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
~String()
{
delete m_Data;
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
: m_Name(name)
{
}
private:
String m_Name;
};
int main()
{
Entity entity(String("Thiago"));
std::cin.get(); // used only to avoid the program to close
}
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
Although, I will need away to copy the string in the main()
to me string m_Name
for this we will have to make a copy constructor.
#include <iostream>
using namespace std;
class String {
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String()
{
printf("Destroyed!\n");
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
: m_Name(name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("Thiago"));
entity.PrintName();
std::cin.get(); // used only to avoid the program to close
}
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
So now we are just copying the string, and we have a valid way to actually get it in here.
Created!
Copied!
Destroyed!
Thiago
2
3
4
As we can see in the output we have Created
, Copied
, Destroyed
, and printed the name.
And the Copied
is really the problem here.
The reason its a problem is that to copy the string we need to allocate memory in the heap
, we have to call new char[m_Size];
, that's not a good thing.
The fact that we have to copy is not really necessary.
We are basically creating an entity called thiago and we just need a way to get the string into m_Name
. Why can't we just create it on the main function and move it to the m_Name
without copying it at : m_Name(name)
.
The answer is we can with move semantics.
We are going to make a move constructor which will take a rvalue reference
(a temporary). We should specify it with noexcept because it should not be throwing any exceptions anyway, VSC could give a warning. Also, we have to make sure that Entity
also has a constructor that takes in a temporary.
What we are doing basically is assigning the pointer to m_Data
. We are saying the the point on line 45 (char* m_Data;
), actually points to the same data. instead of allocating a intire new block of data and copying everything into it m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
, we are point to the same block of data as the old string.
Now this immediately presents a problem, because we do still have two string instances. What happens to the old one when it gets deleted? It's going to take the data with it. delete m_Data;
So what should we do?
We need to create a hollow object:
We set other.m_Size = 0
and other.m_Data = nullptr;
The other
is kind of an empty state.
What happens is when the old string instance gets destroyed, delete m_Data;
will now actually be deleting nullptr
, which is nothing to delete.
Last thing is to cast to a temporary, like this : m_Name((String&&) name)
on line 60.
#include <iostream>
using namespace std;
class String {
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copied!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
String(String&& other) noexcept
{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr;
}
~String()
{
printf("Destroyed!\n");
delete m_Data;
}
void Print()
{
for (uint32_t i = 0; i < m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
: m_Name(name)
{
}
Entity(String&& name)
: m_Name((String&&) name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity("Thiago");
entity.PrintName();
std::cin.get(); // used only to avoid the program to close
}
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
Output
Created!
Moved!
Destroyed!
Thiago
2
3
4
Accidentally, I didn't assign other.m_Size = 0
and other.m_Data = nullptr;
.
The output was:
Created!
Moved!
Destroyed!
▌▌▌▌▌▌
2
3
4
In practice, you wouldn't really cast it to a rvalue reference like this m_Name((String&&) name)
, instead you would use m_Name(std::move(name))
, which essentially does the same thing.