Recap - Copy Constructors

Motivation

By default, when objects of user-defined types are copied, the compiler simply does a copy of all values of all member variables of that object.

Suppose a class has member variables which are pointers to memory dynamically allocated; when a copy of an object of this class is made, it copies the memory address stored by this pointer. This is known as a shallow copy.

This is problematic if the destructor of the class deletes the memory stored by these pointers, in an attempt to clean up after itself. This will cause a double-free memory error when both the original and copied object goes out of scope, as the destructor of the first object that is destructed invalidates the pointer copy held by the other object.

struct Foo {
    int* value_ptr;  // A pointer as a member variable

    Foo(int value) {
        value_ptr = new int(value);  // Dynamically allocated `int` 
    }

    ~Foo() {
        delete value_ptr;  // Delete the value stored since we dynamically allocated it 
    }
};

int main() {
    Foo foo1(1);  // Create an instance of `Foo`
    Foo foo2 = foo1; // Create a copy based on `foo1`
}

/* Output:
      free(): double free detected in tcache 2
      signal: aborted (core dumped)
*/

Copy Constructors

What is a Copy Constructor?

The copy constructor is a special type of constructor that is called when a new object is created from an existing object as a copy of the existing object.

The following cases fall into situations where the copy constructor is invoked:

  • When an object of the class is returned by value

// The return type `Foo` is returning BY VALUE
// the variable created in line 4
Foo MakeFoo(int value) {
    Foo foo(value);
    return foo;
}
  • When an object of the class is passed by value to a function as an argument

// The argument type `Foo` means that the argument 
// `some_foo` is copied from the instance that is 
// used to invoke this function.
void TakeFoo(Foo some_foo) {
    // ...
}
  • When an object is constructed based on another object of the same class

int main() {
    Foo foo(1);
    // Constructing a new instance based on copying
    // the one above 
    Foo bar = foo;  
}
  • When the compiler generates a temporary object

    • We'll get into this much later.

How do Copy Constructors help?

The copy constructor is akin to a function, one whose body we can supply with logic to ensure deep copies of all dynamic member variables whenever it is invoked:

struct Foo {
    // ...

    Foo(const Foo& other) {
        std::cout << "Invoking the copy constructor..." << std::endl;
        this.value_ptr = new int(*other.value_ptr);
    }
};

The copy constructor takes an object of the same class as a constant reference parameter, this will be the source object (i.e. the one being copied from) that we can use to write our custom copy code.

By default, the compiler would've done:

this.value_ptr = other.value_ptr;

But we've now replaced that with:

this.value_ptr = new int(*other.value_ptr);

Which ensures that the underlying value of value_ptr is copied, instead of the memory address of the one held by the original instance; therefore, the destructors of each instance then go on to free different memory addresses, and our predicament has been solved.

Copy Assignment Operator

The copy assignment operator is a regular assignment operator (operator=) whose intention is to copy an existing object into an already initialised object of the same type:

int main() {
    Foo first(1);
    Foo second(2);
    
    // Calls the copy assignment operator, this is
    // the same as "first.operator=(second);"
    first = second;  
}

Do not be mistaken, the above code will not invoke the copy constructor as it is not constructing a new instance from an existing value, but assigning an existing value to another.

As before, this also performs a shallow copy of the Foo objects and with it the same issues we were just covering before.

Similar to defining custom copy code with the Copy Constructor, we can override this behaviour with the Copy Assignment operator.

struct Foo {
    // ...

    Foo& operator=(const Foo& other) {
        std::cout << "Invoking the assignment operator..." << std::endl;
        delete value_ptr;
        this->value_ptr = new int(*other.value_ptr);
        return *this;
    }
};

The syntax for this is a little more nuanced – it returns a reference to either a new Foo object, or you can return the current one with its values properly copied if you're able to re-use it.

The main difference is that we also have to free the memory held by value_ptr of the instance that is to be copied to, since it was already initialised to some other value prior to being assigned a new one.

Copy constructor is called when a new object is created from an existing object, as a copy of the existing object. And assignment operator is called when an already initialized object is assigned a new value from another existing object.

Last updated

Was this helpful?