This post is a gentle introduction of the two semantics and shows how to support them in a class.

Copy semantics

Copy constructor

A copy constructor is a special type of constructor used to create a new object as a copy of an existing object (see Example 1.)

// =================== Example 1 ===================
#include <iostream>

class MyClass{
public:
    MyClass(int val=0)
        :m_int(val){};

    // Copy constructor
    MyClass(const MyClass &other) {
        m_int = other.m_int;
    }
    
private:
    int m_int;
};

Note that if you do not provide a copy constructor for your classes, the C++ compiler will automatically generate a public copy constructor (a.k.a default copy constructor) for you. However, the compiler does not know much about your class, so the generated copy constructor utilizes memberwise initialization (a.k.a shallow copying.) That is, the compiler will copy each member of the original object individually using its (overloaded) assignment operator (i.e., operator =.) This works well for simple classes, which do not have any dynamically-allocated-memory (e.g., pointer) member.)

However, when your class contains dynamically-allocated-memory members, the default copy constructor causes more harm than good. Because the shallow copy of a pointer does not allocate any memory (or copy the content) but just copies the pointer’s address. Thus, we need to create a deep copy constructor (see Example 2.)

A copy constructor may be elided when you use temporary objects as input parameters. As depicted in Example 3, the my_object3 is initialize by a temporary object created by MyClass2(ptr_i2) so the whole expression can be elided to MyClass2 my_object3(ptr_i2).

#include <iostream>

class MyClass2{
public:
    MyClass2(int *ptr=nullptr)
        :m_ptr(ptr){};
    
    // =================== Example 2 ===================
    // Copy constructor
    MyClass2(const MyClass2 &other) {
        std::cout << "invoking copy constructor\n";
        if(other.m_ptr != nullptr){
            m_ptr = new int;
            *m_ptr = *other.m_ptr;  // Copy the content of the pointer
        } else {
            m_ptr = nullptr;
        }
    }    
    
    virtual ~MyClass2(){
        std::cout << "object is destructed\n";
        if(m_ptr != nullptr)
            delete m_ptr;
    }
    
private:
    int *m_ptr; // Pointer to a single int
};
 
int main()
{
    int *ptr_i = new int(1);
    MyClass2 my_object(ptr_i);
    MyClass2 my_object2(my_object);

    // =================== Example 3 ===================
    int *ptr_i2 = new int(1);
    MyClass2 my_object3(MyClass2(ptr_i2)); // This is equivalent to MyClass2 my_object3(ptr_i2)
    
    return 0;
}

Copy assignment

The copy assignment operator (i.e., operator =) is used to copy values from one object to another already existing object. Although both the copy constructor and the copy assignment operator copy one object to another, the copy constructor initializes new objects. In contrast, the assignment operator replaces the contents of existing objects.

Like the copy constructor,

  • A default copy assignment operator (which also does shallow copy) will be automatically generated by the C++ compiler if you don’t provide one,
  • A copy assignment can be elided if its LHS is an newly created object (see Example 5.)

Examples:

...
class MyClass2{
public:
    ...
    // =================== Example 4 ===================
    // Deep copy assignment operator
    MyClass2& operator=(const MyClass2 &other){
        std::cout << "invoking copy assignment\n";
        // self assignment detection
        if(&other == this)
            return *this;
        
        // Remove the current assess
        if(m_ptr != nullptr)
            delete m_ptr;
        
        if(other.m_ptr == nullptr){
            m_ptr = nullptr;
        } else {
            m_ptr = new int;
            *m_ptr = *other.m_ptr;
        }
        
        return *this;
    }
    ...
};
...
int main() {
    ...
    // =================== Example 5 ===================
    MyClass2 my_object4 = my_object3; // This is equivalent to MyClass2 my_object4(my_object3) 
    ...
}
...

Note that to prevent the C++ compiler from generate default functions, we can use the delete keyword as follows:

class MyClass3{
    MyClass3(const MyClass3 &other) = delete;
    MyClass3& operator=(const MyClass3 &other) = delete;
};

Move semantics

C++11 defines the new move semantics, introducing move constructor and move assignment operator. Whereas the goal of the copy constructor and copy assignment is to make a copy of one object to another, the purpose of the move constructor and move assignment is to move ownership of the resources from one object to another (which is typically much less expensive than making a copy).

Defining a move constructor and move assignment work analogously to their copy counterparts. However,

  1. The C++ compiler does not automatically generate the move functions (i.e., the constructor and assignment operations);
  2. Whereas the copy functions take a const l-value reference parameter, the move functions use non-const r-value reference parameters (see Example 6 and Example 7);
  3. Instead of deep copying the source pointer’s resources to the target, the move functions simply move the source pointer’s address to the target (i.e., shallow copy) and set the source pointer to null (thus, the move functions are cheaper (i.e., faster or consuming less memory) that the copy ones);

Recall that to invoke move functions, we need to pass in r-value parameters, and std::move is a standard library function converting its argument into an r-value reference. That is, we can pass an l-value to std::move, and it will return an r-value reference (see Example 8.)

...
class MyClass2{
public:
    ...
    // =================== Example 6 ===================
    // Move constructor
    MyClass2(const MyClass2 &&other) noexcept
        : m_ptr(other.m_ptr) {
        std::cout << "invoking move constructor\n";
        other.m_ptr = nullptr;
    } 

    // =================== Example 7 ===================
    // Move assignment operator
    MyClass& operator=(const MyClass &&other){
        std::cout << "invoking move assignment\n";
        // self assignment detection
        if(&other == this)
            return *this;
        
        // Remove the current assess
        if(m_ptr != nullptr)
            delete m_ptr;
        
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
        
        return *this;
    }
    ...
};
...
int main() {
    ...
    // =================== Example 8 ===================
    MyClass2 my_object5;
    my_object5 = my_object3; // This invokes copy assignment operator.
    my_object5 = std::move(my_object3); // This invokes move assignment operator. 
    ...
}
...