Copy and Move Semantic
When working with big objects for example an Image copying objects around can get very expensive. For example if create a factory function
string factory() {
string t = "text";
return t; // t gets copied using a temp object
}
string s = factory(); // might get copy elision
s = factory() // another temp is created
To improve this we need to understand a few things.
Lvalues and Rvalues
In C++ you can split values into groups.
- Lvalues are values whos adresses you can get and in most times last a long time. For example x here is a Lvalue
int x = 3;
. - Rvalues however are transient or temporary and are destroyed when no longer needed. For example (x+y) here is a Rvalue
int z = x+y
because it is temporary and as as soon as it is copied to z it is destroyed and we can't get its address. Some goes for the 3 above you can not do&3
so it is an Rvalue.
Rvalues can be split up into 2 further groups, pure-right values which are
- Literals: 42, true, nullptr
- Arithmetic expressions: a + b, a < b
- Function calls: str.substr(1, 2)
and Xvalues (eXpiring). Xvalues are functions calls with an Rvalue reference as return or array indexing or attribut calls on an Rvalue.
GL values that are lValues and Xvalues.
Rvalue reference
You can prolong the life of an Rvalue by using a Rvalue reference
#include <iostream>
using namespace std;
int main()
{
string s1 = "Test";
//string&& r1 = s1; Rvalue reference cant have Lvalue
const string& r2 = s1 + s1; // constant Lvalue referenz can have Rvalue, life prolonged
string&& r3 = s1 + s1; // Rvalue reference has Rvalue, life prolonged
r3 += "Test";
}
You can then use Rvalue references as parameters to make sure things do not get copied or wasted. You can also remember when you a returning an Rvalue reference you should always use move. But with C++20 this becomes all irrelevant (opens in a new tab).
#include <iostream>
using namespace std;
string foo(string&& s) { // s becomes Lvalue in function
s += "456";
return move(s); // if not move then it is return by value and gets copied
}
int main()
{
string s = "Test";
string f = foo(s + "abc");
// string t = foo(s); not possible because s is Lvalues
cout << s <<endl;
cout << f << endl;
}
Copying and moving
You often want to create copies of other objects (also commonly known as prototypes). You need to very careful with what you are doing then especially when you are working with pointers and don't want a shallow copy but a deep copy. Instead of creating a copy you can also create a move constructor which moves all the values from object to another. You can then also override the assignment operator to either copy or move the data depending on what is on the right side. All the move function from the standard library does is make an Rvalue out of an Lvalue.
#include <iostream>
using namespace std;
class Person
{
private:
string last_name;
string first_name;
int* age;
void invalidate() {
this->last_name = "";
this->first_name = "";
this->age = nullptr;
}
public:
explicit Person(const string& last_name_param = "", const string& first_name_param = "", int age_param = 0)
: last_name(last_name_param), first_name(first_name_param), age(new int(age_param)) // age(source_p.get_age()) would have 2 pointers to same value
{}
// copy constructor
Person(const Person& source)
: last_name(source.last_name), first_name(source.first_name), age(new int(*source.age))
{
cout << "Copy constructor called" << endl;
}
// move constructor
Person(Person&& source)
: last_name(source.last_name), first_name(source.first_name), age(source.age)
{
cout << "Move constructor called" << endl;
source.invalidate();
}
~Person() {
delete age; // Make sure that you are not leaking memory
}
// copy assignment operator
void operator=(const Person& source) noexcept {
cout << "Copy assignment operator called" << endl;
// Check for self-assignment:
if (this == &source)
return;
this->set_last_name(source.get_last_name());
this->set_first_name(source.get_first_name());
this->set_age(*source.get_age());
}
// move assignment operator
void operator=(Person&& source) noexcept {
cout << "Move assignment operator called" << endl;
// Check for self assignment
if (this == &source)
return;
this->set_last_name(source.get_last_name());
this->set_first_name(source.get_first_name());
this->age = source.get_age();
source.invalidate();
}
void set_first_name(const string& first_name) { this->first_name = first_name; }
void set_last_name(const string& last_name) { this->last_name = last_name; }
void set_age(int age) { *(this->age) = age; } // Remember to dereference
const string& get_first_name() const { return first_name; }
const string& get_last_name() const { return last_name; }
int* get_age() const { return age; };
//Utilities
void print_info() {
cout << "Person object at : " << this
<< " [ Last_name : " << last_name
<< ", First_name : " << first_name
<< " ,age : " << *age
<< " , age address : " << age
<< " ]" << endl;
}
};
int main()
{
cout << "Testing copy constructor" << endl;
Person p1("John", "Snow", 25);
p1.print_info();
//Create a person copy
Person p2(p1);
p2.print_info();
cout << "Testing move constructor" << endl;
Person p3(move(p2)); // moved, move() just makes p2 a Rvalue
p3.print_info();
//p2.print_info(); now basically dead
cout << "Testing copy operator" << endl;
// Person p4 = p3; will use the copy constructor because the object first needs to be constructed
Person p4;
p4 = p3;
p3.print_info();
p4.print_info();
cout << "Testing move operator" << endl;
Person p5;
p5 = move(p4);
p5.print_info();
//p4.print_info(); now basically dead
}