Copy, Assignment, and Destructors

reading

Cyganek section 4.4

So far, we’ve been relying on the compiler to provide a default copy constructor, assignment operator, and destructor. As long as our data is simple, we can do this, since the compiler will know how to work with it.

Here we’ll explicitly write this out anyway, just to understand how they would look.

We’ll use the Example: Mathematical Vectors Vector2d class as our example here.

The copy constructor is invoked when we do something like:

Vector2d vec(1, 2);

auto vec2(vec);
auto vec3 = vec;

In both of these cases, we need to create a separate Vector2d with its own memory and copy the data over. The copy constructor has the function signature:

Vector2d(const Vector2d& vec)

The assignment operator is invoked when we do something like:

Vector2d vec(1, 2);
Vector2d vec2;

vec2 = vec;

The assignment operator has the signature:

Vector2d& operator= (const Vector2d& vec)

Note

The reason that the assignment operator returns a reference is to allow us to chain assignment, e.g.:

v1 = v2 = v3;

Finally, the destructor is written as:

~Vector2d()

Its job is to clean up any of the memory we allocated. But for our Vector2d class, all the data are automatic variables, so they will be deleted when they go out of scope by the stack. This is the reason we really don’t need to write a destructor for this class.

Tip

Unless our class is complicated and manages its own memory (e.g., via pointers), we don’t need to write these functions. The compiler will automatically generate them for us.

This is the Rule of 3 (or Rule of 5, as we’ll see later) that we previously mentioned.

Here’s an example of our Vector2d class that explicitly implements the copy constructor, assignment operator, and destructor. Additionally, we add a print to stdout in each of these so we can see when each of them are called.

Listing 69 a newer vector2d.H
#ifndef VECTOR_2D_H
#define VECTOR_2D_H

#include <iostream>
#include <cmath>

class Vector2d {

private:

    // our member data

    double x;
    double y;

public:

    // default constructor

    Vector2d()
        : x{0.0}, y{0.0}
    {}

    // another constructor

    Vector2d(double _x, double _y)
        : x{_x}, y{_y}
    {}

    // the destructor

    ~Vector2d() {std::cout << "in the destructor" << std::endl;}

    // copy and assignment constructors -- we don't need to define them, but we
    // explicitly say to use the default

    //Vector2d(const Vector2d& vec) = default;
    Vector2d(const Vector2d& vec)
        : x(vec.x), y(vec.y)
        {std::cout << "in the copy constructor" << std::endl;}

    // assignment here means that we want to set our current class's values to
    // those to the right of the = -- that's the Vector2d coming in through the
    // argument list

    //Vector2d& operator= (const Vector2d& vec) = default;
    Vector2d& operator= (const Vector2d& vec) {
        std::cout << "in the assignment operator" << std::endl;
        x = vec.x;
        y = vec.y;
        return *this;
    }


    // setters

    inline void set_x(double _x) {x = _x;}

    inline void set_y(double _y) {y = _y;}

    // operators

    // add two vectors
    
    Vector2d operator+ (const Vector2d& vec) {
        Vector2d vec_out(x + vec.x, y + vec.y);
        return vec_out;
    }

    // subtract two vectors

    Vector2d operator- (const Vector2d& vec) {
        Vector2d vec_out(x - vec.x, y - vec.y);
        return vec_out;
    }

    // unary minus

    Vector2d operator- () {
        Vector2d vec_out(-x, -y);
        return vec_out;
    }

    // multiplication

    // vec * a

    Vector2d operator* (double a) {
        Vector2d vec_out(a * x, a * y);
        return vec_out;
    }

    // a * vec -- needs to be a friend

    friend Vector2d operator* (double a, const Vector2d& vec);

    // division by a scalar

    Vector2d operator/ (double a) {
        Vector2d vec_out(x / a, y / a);
        return vec_out;
    }

    // comparison operators

    bool operator== (const Vector2d& vec) const {
        return x == vec.x && y == vec.y;
    }

    bool operator!= (const Vector2d& vec) const {
        return x != vec.x || y != vec.y;
    }

    double abs() {
        return std::sqrt(x * x + y * y);
    }

    double dot(const Vector2d& vec) {
        return x * vec.x + y * vec.y;
    }
    
    // << and >> are not class members, but need access to the member data

    friend std::ostream& operator<< (std::ostream& os, const Vector2d& v);
    friend std::istream& operator>> (std::istream& is, Vector2d& v);
};

std::ostream& operator<< (std::ostream& os, const Vector2d& vec)
{
    os << "(" << vec.x << ", " << vec.y << ")";
    return os;
}

std::istream& operator>> (std::istream& is, Vector2d& vec)
{
    is >> vec.x >> vec.y;
    return is;
}

Vector2d operator*(double a, const Vector2d& vec) {
        Vector2d vec_out(a * vec.x, a * vec.y);
        return vec_out;
}

#endif

Here’s a driver that exercises them.

Listing 70 test_vectors.cpp
#include <iostream>

#include "vector2d.H"

int main() {

    Vector2d v1(1.0, 2.0);
    Vector2d v2, v3;

    // test assignment

    std::cout << "doing v3 = v2 = v1" << std::endl;

    v3 = v2 = v1;

    std::cout << v2 << " " << v3 << std::endl;

    // test copy

    std::cout << "doing auto v4 = v1" << std::endl;
    auto v4 = v1;

    std::cout << v4 << std::endl;

}

Some notes:

  • We see that the destructor is called for each of the Vector2d ‘s we created.