Home Rule of three in c++
Post
Cancel

Rule of three in c++

There are three special functions in C++ class

  • destructor
  • copy constructor
  • overload assignment operator

If one of these functions is not defined by programmer, then compiler defines it for you with default behavior

What are those default behaviors

  • destructor - It calls destructor of the member variables
  • copy constructor - Copy states of other object to this class and do shallow copy that means, if pointer is the state of other object, then it copies the pointer and doesn’t create a new pointer and copy the content
  • overload assignment operator - It is similar to copy constructor

If class has a pointer, then we have to follow this rule of thumb and define all of these three member function explicitly, So that we can avoid shallow copy

Let’s see with example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
enum class Color 
{
    Red,
    Black,
    Silver,
    Invalid
};

const char* colorToStr(const Color& color)
{
    switch (color)
    {
    case Color::Black:
        return "Black";
    case Color::Red:
        return "Red";
    case Color::Silver:
        return "Silver";
    case Color::Invalid: 
    default:
        return "Invalid Color";
    }
}

class Car 
{
public:

    Car()
    {
        std::cout << "Default ctor called \n";
        m_pColor = new Color(Color::Invalid);
    }

    Car(const Color& color)
    {
        std::cout << "Parameterized ctor called \n";
        m_pColor = new Color(color);
    }

    ~Car()
    {
        std::cout << "Dtor called \n";
        if (m_pColor) {
            delete m_pColor;
        }
    }

    Car(const Car& other)
    {
        std::cout << "Copy ctor called \n";
        if (other.m_pColor) {
            m_pColor = new Color(*other.m_pColor);
        }
    }

    void operator=(const Car& other)
    {
        std::cout << "Overload assignment operator called \n";
        if (other.m_pColor) {
            if (m_pColor) {
                *m_pColor = *other.m_pColor;
            }
        }
    }

    void displayColor() const
    {
        if (m_pColor) {
            std::cout << "Car color is " << colorToStr(*m_pColor) << "\n";
        }
    }

private:
    Color* m_pColor;
};

int main()
{
    Car blackCar(Color::Black); // Parameterized ctor called
    Car anotherBlackCar = blackCar; //Copy ctor called also `Car anotherBlackCar(blackCar);` is valid syntax
    Car thirdBlackCar; // Default ctor called
    thirdBlackCar = anotherBlackCar; // overload assignment operator is called

    blackCar.displayColor();
    anotherBlackCar.displayColor();
    thirdBlackCar.displayColor();

    return 0;
}

Output:

1
2
3
4
5
6
7
8
9
10
11
12
bhavith@bhavith:$ g++ hello.cpp
bhavith@bhavith:$ ./a.out 
Parameterized ctor called 
Copy ctor called 
Default ctor called 
Overload assignment operator called 
Car color is Black
Car color is Black
Car color is Black
Dtor called 
Dtor called 
Dtor called

In the above example we can see, Car has a member variable as a pointer to Color enum class, So according to rule of three, We have to define destructor, copy constructor and overload assignment operator so that we can do deep copy.

What if we use RAII for example shared pointer as a member variable?

In such case we can have only rule of two, that is copy constructor and overload assignment operator and destructor is useless

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
enum class Color 
{
    Red,
    Black,
    Silver,
    Invalid
};

const char* colorToStr(const Color& color)
{
    switch (color)
    {
    case Color::Black:
        return "Black";
    case Color::Red:
        return "Red";
    case Color::Silver:
        return "Silver";
    case Color::Invalid: 
    default:
        return "Invalid Color";
    }
}

class Car 
{
public:

    Car()
    {
        std::cout << "Default ctor called \n";
        m_pColor = std::make_shared<Color>(Color::Invalid);
    }

    Car(const Color& color)
    {
        std::cout << "Parameterized ctor called \n";
        m_pColor = std::make_shared<Color>(color);
    }

    ~Car()
    {
        std::cout << "Dtor called \n";
    }

    Car(const Car& other)
    {
        std::cout << "Copy ctor called \n";
        if (other.m_pColor) {
            m_pColor = other.m_pColor;
        }
    }

    void operator=(const Car& other)
    {
        std::cout << "Overload assignment operator called \n";
        if (other.m_pColor) {
            if (m_pColor) {
                m_pColor = other.m_pColor;
            }
        }
    }

    void displayColor() const
    {
        if (m_pColor) {
            std::cout << "Car color is " << colorToStr(*m_pColor) << "\n";
        }
    }

    void refCnt() const
    {
        std::cout << "Ref count is " << m_pColor.use_count() << "\n";
    }

private:
    std::shared_ptr<Color> m_pColor;
};

int main()
{
    Car blackCar(Color::Black); // Parameterized ctor called
    blackCar.refCnt(); // 1

    Car anotherBlackCar = blackCar; //Copy ctor called
    anotherBlackCar.refCnt(); // 2 since copied

    Car thirdBlackCar; // Default ctor called
    thirdBlackCar.refCnt(); // 1 

    thirdBlackCar = anotherBlackCar; // overload assignment operator is called
    thirdBlackCar.refCnt(); // 3 since copied

    blackCar.displayColor();
    anotherBlackCar.displayColor();
    thirdBlackCar.displayColor();

    return 0;
}

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bhavith@bhavith:$ g++ hello.cpp
bhavith@bhavith:$ ./a.out 
Parameterized ctor called 
Ref count is 1
Copy ctor called 
Ref count is 2
Default ctor called 
Ref count is 1
Overload assignment operator called 
Ref count is 3
Car color is Black
Car color is Black
Car color is Black
Dtor called 
Dtor called 
Dtor called 

Note: Observer how reference counts are increasing, It is also a shallow copy, but the life time of the shared pointer is when last object is destructed. That is the beauty of smart pointers

This post is licensed under CC BY 4.0 by the author.

Single Responsibility Principle

Track heap allocation as well check memory leaks in C++ programmatically