Rule of Three in C++

Jay Shaw Oct 12, 2023
  1. Understanding the Rule of the Big Three in C++
  2. Implicit Definition of Constructors in C++
  3. Explicit Definition of Constructors in C++
  4. Conclusion
Rule of Three in C++

The rule of the big three is one of the most popular coding idioms worldwide. The law states that C++ has some special functions that must be declared together, even if one is required.

These functions are copy constructor, copy assignment operator, and destructor.

The law states that the other two must also follow if one of the three special functions is declared in a program. If not, the program gets run into serious memory leaks.

This article will explain in detail the law of the big three and how to get around with it.

Understanding the Rule of the Big Three in C++

It must be considered what occurs when a dynamically allocated resource is added to a class to find out about the big three.

In the below example, a constructor class is created that allocates some memory space to the pointer.

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
};

As seen above, the memory is allocated to the pointer. Now, it must be released too using a destructor.

~Big3() {
  std::cout << "Resource is released \n";
  delete x;
}

The common perception is that the work is done until this point. But the reality is nowhere near.

Some serious memory leaks occur when a copy constructor is initiated inside this class.

The reason behind this problem is the implicit definition of constructors. When an object is copied without defining a constructor, the compiler implicitly creates a copy constructor that does not create a clone but just a shadow of the same object.

Copy constructors, by default, run shallow copying on objects. The program runs fine with this copying method until some resource is dynamically allocated to an object.

For example, in the code below, an object p1 is created. Another object, p2, is a copy object of p1.

int main() {
  Big3 p1;
  Big3 p2(p1);
}

In this code, when the destructor destroys the object p1, p2 becomes a dangling pointer. This is because object p2 points to the reference of object p1.

The complete code is given below for better understanding.

#include <iostream>

class Big3 {
  int* x;

 public:
  Big3() : x(new int()) { std::cout << "Resource allocated \n"; }
  ~Big3() {
    std::cout << "Resource is released \n";
    delete x;
  }
};

int main() {
  Big3 p1;
  Big3 p2(p1);
}

To avoid issues like a dangling pointer, the programmers need to explicitly declare all the required constructors, which is what the big three rule is about.

Implicit Definition of Constructors in C++

There are two ways through which a copy of an object is made.

  1. Shallow copying - using a constructor to copy the address of an object and storing it in the new object.
  2. Deep copying - using a similar constructor, which copies values stored inside that address into the new one.

Usually, when some memory is allocated to an object, the implicit version of the copy constructor copies the reference of the pointer x instead of creating a new object with its own memory allocation set.

Below is a representation of how special member functions are implicitly defined.

book(const book& that) : name(that.name), slno(that.slno) {}

book& operator=(const book& that) {
  name = that.name;
  slno = that.slno;
  return *this;
}

~book() {}

In this example, the three special member functions are defined. The first is the copy constructor, which assigns the member variables through a member initializer list: name(that.name), slno(that.slno).

The second constructor is the copy assignment constructor that creates a copy of the objects of the class. Here, operator overloading is used to create copies of the object.

In the end, the destructor is kept empty as no resource is allocated. The code throws no errors as objects do not need any memory allocation.

Why Implicit Definitions Fails in Resource Management

Suppose a member pointer of the class receives memory allocation. The reference of this member pointer will be copied to the new object when the object of this class is copied using the default assignment operator and copy function constructor.

As a result, any changes made to one object will also affect the other because the new and old objects will both be pointing at the same memory location. The second object will keep attempting to use it.

class person {
  char* name;
  int age;

 public:
  // constructor acquires a resource
  // dynamic memory obtained via new
  person(const char* the_name, int the_age) {
    name = new char[strlen(the_name) + 1];
    strcpy(name, the_name);
    age = the_age;
  }

  // destructor must release this resource via delete
  ~person() { delete[] name; }
};

The above example copies name, which copies the pointer, leaving the values stored inside.

When the destructor is declared, it just deletes the instance of the original object. But the copied object keeps pointing to the same reference, which now stands destroyed.

This is the main cause of memory leaks. They arise when the destructor deletes the original object and its reference, while the object created by the copy constructor keeps on dangling and is also known as a dangling pointer.

Similarly, if a dangling pointer is kept unchecked, then that memory reference will create multiple memory leaks in the future.

The only solution to this problem is to explicitly declare constructors, or in simple words, creating own model of copy constructor and assignment operator to fix this.

The custom-made constructors copy the values that the initial pointer points to rather than its address, allocating separate memory to the new objects.

Explicit Definition of Constructors in C++

It was observed that implicitly defined constructors resulted in shallow copying of objects. To fix that, programmers explicitly define a copy constructor, which helps in deep copying objects in C++.

Deep copying is the method that assigns new memory blocks to store a pointer’s value instead of just saving memory reference.

This method requires explicitly defining all three special member methods so that the compiler allocates new memory while copying an object.

Role of Destructors in Explicitly Defined Constructors in C++

A destructor must be created to erase the memory that a function object is allotted. This risks memory leak if you don’t.

In implicit constructors, even after declaring a destructor, the problem persists. The issue arises because if the object allotted memory is copied, the copied object will point to the same memory as the original object.

When one deletes the memory in its destructor, the other will have a pointer to invalid memory, and things will get complicated when it tries to use it.

As a result, an explicitly defined copy constructor must be created, giving new objects their memory fragments to purge.

The program below shows a demo class that adheres to the rule of the big three.

class name {
 private:
  int* variable;  // pointer variable
 public:
  // Constructor
  name() { variable = new int; }

  void input(int var1)  // Parameterized method to take input
  {
    *variable = var1;
  }

  // Copy Constructor
  name(name& sample) {
    variable = new int;
    *variable = *(sample.variable);

    // destructor
    ~name() { delete variable; }
  };

Implement Copy Constructors in C++

The program has a class Book with a default, parameterized constructor, and a destructor. A default constructor returns null values when no input is provided, whereas a parameterized constructor initializes the values and copies them.

Here, an exception handling method (try-catch) is included, which throws exceptions when the variable m_Name cannot allocate a resource.

After the destructor, a copy constructor is created that makes a copy of the original object.

#include <cstring>
#include <exception>
#include <iostream>

using namespace std;

class Book {
  int m_Slno;
  char* m_Name;

 public:
  // Default Constructor
  Book() : m_Slno(0), m_Name(nullptr) {}

  // Parametarized Constructor
  Book(int slNumber, char* name) {
    m_Slno = slNumber;
    unsigned int len = strlen(name) + 1;
    try {
      m_Name = new char[len];
    } catch (std::bad_alloc e) {
      cout << "Exception received: " << e.what() << endl;
      return;
    }
    memset(m_Name, 0, len);
    strcpy(m_Name, name);
  }

  // Destructor
  ~Book() {
    if (m_Name) {
      delete[] m_Name;
      m_Name = nullptr;
    }
  }

  friend ostream& operator<<(ostream& os, const Book& s);
};

ostream& operator<<(ostream& os, const Book& s) {
  os << s.m_Slno << ", " << s.m_Name << endl;
  return os;
}

int main() {
  Book s1(124546, "Digital Marketing 101");
  Book s2(134645, "Fault in our stars");

  s2 = s1;

  cout << s1;
  cout << s2;

  s1.~Book();
  cout << s2;

  return 0;
}

In the main function, when object s1 is destroyed, s2 does not lose its dynamic object, which is the string variable of object s.

Another example below demonstrates how to deep copy objects using a copy constructor. In the code below, a constructor class design is created.

The class has three private variables - two of the static l and h, and a dynamic object w.

#include <iostream>
using namespace std;

// A class design
class design {
 private:
  int l;
  int* w;
  int h;

 public:
  // Constructor
  design() { w = new int; }

  // Method to take input
  void set_dimension(int len, int brea, int heig) {
    l = len;
    *w = brea;
    h = heig;
  }

  // Display Function
  void show_data() {
    cout << "The Length is = " << l << "\n"
         << "Breadth  of the design = " << *w << "\n"
         << "Height = " << h << "\n"
         << endl;
  }

  // Deep copy is initialized here
  design(design& sample) {
    l = sample.l;
    w = new int;
    *w = *(sample.w);
    h = sample.h;
  }

  // Destructor
  ~design() { delete w; }
};

// Driver Code
int main() {
  // Object of class first
  design first;

  // Passing Parameters
  first.set_dimension(13, 19, 26);

  // Calling display method
  first.show_data();

  // Copying the data of 'first' object to 'second'
  design second = first;

  // Calling display method again to show the 'second' object
  second.show_data();

  return 0;
}

Output:

The Length is = 13
Breadth  of the design = 19
Height = 26

The Length is = 13
Breadth  of the design = 19
Height = 26
--------------------------------
Process exited after 0.2031 seconds with return value 0
Press any key to continue . . .

Conclusion

This article presents a comprehensive and detailed explanation of the rule of big 3 and how it affects programming. The readers get to learn the need and importance of the rule of big 3.

Along with it, some new concepts are explained, like deep and shallow copying, which has several implementations.