Digital Garden
Computer Science
C++
Copy and Move Semantic

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.

Hierarchy of Lvalues and Rvalues in C++.

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
}
Output