Move semantics are created to avoid the unnecessary copying of objects.
Start with below simple example (stackoverflow) - a String class that holds a pointer to some allocated heap.
class String {
char* data;
public:
String(const char* p)
{
size_t size = strlen(p) + 1;
data = new char[size];
memcpy(data, p, size);
std::cout << data << " created" << std::endl;
}
void Print()
{
std::cout << "Printing " << data << std::endl;
}
};
Its destructor and copy constructor are implemented as following.
class String {
public:
~String()
{
delete[] data;
std::cout << "Destroyed" << std::endl;
}
String(const String& s)
{
size_t size = strlen(s.data) + 1;
data = new char[size];
memcpy(data, p, size);
std::cout << data << " copied" << std::endl;
}
};
The copy constructor defines what it means to copy string objects.
We then implement a second class, Person, to pass String into:
class Person {
String name;
public:
Person(const String& s) : name(s) { }
void PrintName()
{
name.Print();
}
};
In main, we will create a temporary String object as the parameter to create a Person object, and run the program to understand how the functions are being called.
int main()
{
Person foo {String("Foo")};
foo.PrintName();
return 0;
}
It produces the following output:
Foo created
Foo copied
Destroyed
Printing Foo
Destroyed
Now we can go through the prompts to understand.
Firstly, we create a String object, so the class constructor is called that prints "Foo created".
Next, in the constructor of the Person object, it copies the String object to data variable and prints "Foo copied". This is where the copy constructor gets called.
Then, the original String object that we created gets destroyed, so the descructor is called that prints "Destoryed".
Finally, we have the Person object print "printing Foo", and then exit the program, which destroys the created Person object and so prints the second "Destroyed".
Allocating memory twice is unnecessary. The unnecessary copies especially when we are dealing with a lot of dynamic data.
In the example, the created String object (Foo) is a rvalue, because underlying it has no name. It is a temporary object which is to be destroyed at the next semicolon - at the end of the full expression containing the ravlue. The client has no way to inspect the String object again later. So we could do whatever we want with the source String, and the client could not tell a difference.
C++11 introduces a new type of reference - rvalue reference, allowing to detect rvalue (temporary) arguements via function overloading. To do so, we add a constructor with an ravlue reference parameter. In that constructor we can do whatever we want with the source String:
class String {
String(String&& s)
{
data = s.data;
s.data = nullptr;
std::cout << data << " moved" << std::endl;
}
};
Instead of copying the heap data, we copy the pointer and set the original pointer to nullptr. In effect, we steal the data that originally belongs to the source String. Since there is no copy here, it is called a "move constructor". Its jobs is to move resources from one object to another.
It is required to set nullptr, in order later on when the destructor gets called on the source object, the memory (we stole) will not be deleted.
We have to do similarly to Person class too. Make it able to take a rvalue reference:
class Person {
public:
Person(String&& s) : name(std::move(s)) { }
};
The main function stays the same. The program shall produce the following output:
Foo created
Foo moved
Destroyed
Printing Foo
Destroyed
Great! No more copying. Rather than the costly allocating on the heap, we simply transfer the memory. This would be the basic idea behind move semantics.
The following is the full code. The copy constructor can be removed here actually. It is kept for your reference.
#include <cstring>
#include <iostream>
class String {
char* data;
public:
String(const char* p)
{
size_t size = strlen(p) + 1;
data = new char[size];
memcpy(data, p, size);
std::cout << data << " created" << std::endl;
}
~String()
{
delete[] data;
std::cout << "Destroyed" << std::endl;
}
String(const String& s)
{
size_t size = strlen(s.data) + 1;
data = new char[size];
memcpy(data, s.data, size);
std::cout << data << " copied" << std::endl;
}
String(String&& s)
{
data = s.data;
s.data = nullptr;
std::cout << data << " moved" << std::endl;
}
void Print()
{
std::cout << "Printing " << data << std::endl;
}
};
class Person {
String name;
public:
Person(const String& s) : name(s) { }
Person(String&& s) : name(std::move(s)) { }
void PrintName()
{
name.Print();
}
};
int main()
{
Person foo {String("Foo")};
foo.PrintName();
return 0;
}