Rule of 3 in C++

Rule of 3 in C++

Recently I have implemented Rule 3 in our codebase, I'll break down how I implemented and why according to me everyone who is working on C++ should implement it.

Pre-requisite: when a class is defined, the compiler automatically generates certain special member functions if they are not explicitly provided by the programmer i.e:

  1. Default constructor
  2. Destructor
  3. Copy Constructor
  4. Assignment Operator

Problem in compiler generated function

Compiler generated Copy constructor and Assignment Operator does shallow copy instead of Deep copy. Here is a code explaining the same and what the issue is

#include <iostream>

class MyClass {
public:
    int x;
    int* y;

    // Constructor
    MyClass(int val1, int val2) : x(val1) {
        y = new int(val2);
    }

    // Destructor
    ~MyClass() {
        delete y;
    }

    // Compiler-Generated Copy Constructor (Shallow Copy)
    MyClass(const MyClass& other) : x(other.x), y(other.y) {
        // This is essentially what the compiler does:
        // x = other.x; // Copy the value of x
        // y = other.y; // Copy the pointer (shallow copy)
    }

    // Compiler-Generated Copy Assignment Operator (Shallow Copy)
    MyClass& operator=(const MyClass& other) {
        if (this != &other) { // Check for self-assignment
            x = other.x;       // Copy the value of x
            y = other.y;       // Copy the pointer (shallow copy)
        }
        return *this;
    }
};

int main() {
    MyClass obj1(10, 20);
    MyClass obj2 = obj1; // Invokes the copy constructor

    std::cout << "After copy constructor:\n";
    std::cout << "obj1.x: " << obj1.x << ", obj1.y: " << *obj1.y << std::endl;
    std::cout << "obj2.x: " << obj2.x << ", obj2.y: " << *obj2.y << std::endl;

    *obj2.y = 30; // Modify obj2's y

    std::cout << "\nAfter modifying obj2.y:\n";
    std::cout << "obj1.y: " << *obj1.y << std::endl;
    std::cout << "obj2.y: " << *obj2.y << std::endl;

    MyClass obj3(40, 50);
    obj3 = obj1; // Invokes the copy assignment operator

    std::cout << "\nAfter copy assignment:\n";
    std::cout << "obj3.x: " << obj3.x << ", obj3.y: " << *obj3.y << std::endl;

    return 0;
}
        

Output:

After copy constructor:
obj1.x: 10, obj1.y: 20
obj2.x: 10, obj2.y: 20

After modifying obj2.y:
obj1.y: 30
obj2.y: 30

After copy assignment:
obj3.x: 10, obj3.y: 30
free(): double free detected in tcache 2        

Issues with Shallow Copy:

  • As seen in the output, modifying obj2.y affects obj1.y because both objects share the same memory location. Similarly, after the copy assignment, obj3 also shares the same memory.
  • This can lead to problems, such as double deletion (when multiple objects try to delete the same memory), which is why in many cases, a deep copy is preferred when managing resources like dynamic memory.

How to solve this Issue:

Here is the code explaining the solution for shallow copy

#include <iostream>

class MyClass {
public:
    int x;
    int* y;

    // Constructor
    MyClass(int val1, int val2) : x(val1) {
        y = new int(val2);
    }

    // Copy Constructor (Deep Copy)
    MyClass(const MyClass& other) : x(other.x) {
        y = new int(*other.y);  // Allocate new memory and copy the value
    }

    // Copy Assignment Operator (Deep Copy)
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {           // Check for self-assignment
            x = other.x;                // Copy the value of x

            delete y;                   // Free existing memory
            y = new int(*other.y);      // Allocate new memory and copy the value
        }
        return *this;
    }

    // Destructor
    ~MyClass() {
        delete y; // Free the allocated memory
    }
};

int main() {
    MyClass obj1(10, 20);
    MyClass obj2 = obj1; // Invokes the deep copy constructor

    std::cout << "After copy constructor:\n";
    std::cout << "obj1.x: " << obj1.x << ", obj1.y: " << *obj1.y << std::endl;
    std::cout << "obj2.x: " << obj2.x << ", obj2.y: " << *obj2.y << std::endl;

    *obj2.y = 30; // Modify obj2's y

    std::cout << "\nAfter modifying obj2.y:\n";
    std::cout << "obj1.y: " << *obj1.y << std::endl;
    std::cout << "obj2.y: " << *obj2.y << std::endl;

    MyClass obj3(40, 50);
    obj3 = obj1; // Invokes the deep copy assignment operator

    std::cout << "\nAfter copy assignment:\n";
    std::cout << "obj3.x: " << obj3.x << ", obj3.y: " << *obj3.y << std::endl;

    return 0;
}        

Output:

After copy constructor:
obj1.x: 10, obj1.y: 20
obj2.x: 10, obj2.y: 20

After modifying obj2.y:
obj1.y: 20
obj2.y: 30

After copy assignment:
obj3.x: 10, obj3.y: 20        

Explanation:

  1. Deep Copy Constructor: The deep copy constructor allocates new memory for y in the new object and copies the value from the source object. This ensures that obj1 and obj2 have separate memory locations for y.
  2. Deep Copy Assignment Operator: The deep copy assignment operator first checks for self-assignment (to avoid problems if the object is assigned to itself). It then frees the existing memory for y (to avoid memory leaks) and allocates new memory, copying the value from the source object. This ensures that after the assignment, both objects (obj3 and obj1) have their own separate memory.

Benefits of Deep Copy:

  • Independent Memory: Each object now has its own memory for y, so modifying one object does not affect another.
  • No Double Deletion: Each object deletes its own memory without causing any conflicts or crashes due to double deletion.

How Rule of 3 fits in all of this:

This rule basically states that if a class defines one(or more) of the following, it should probably explicitly define all three:

  1. Destructor
  2. Copy Constructor(CC)
  3. Assignment Operator(AO)

Let's understand why is this rule case by case

Case 1: If I define a destructor and not define CC and AO which is against Rule 3 let's see what happens

#include <iostream>

class MyClass {
public:
    int* data;

    // Constructor
    MyClass(int val) {
        data = new int(val);
        std::cout << "Constructor: Allocated memory for data\n";
    }

    // Destructor
    ~MyClass() {
        delete data;
        std::cout << "Destructor: Released memory for data\n";
    }

    // Copy Constructor and Assignment Operator are not defined,
    // so the compiler will generate default shallow copies.
};

int main() {
    MyClass obj1(10);

    // Using the default copy constructor (shallow copy)
    MyClass obj2 = obj1;

    std::cout << "After copying:\n";
    std::cout << "obj1.data: " << *obj1.data << std::endl;
    std::cout << "obj2.data: " << *obj2.data << std::endl;

    // Modifying obj2's data
    *obj2.data = 20;

    std::cout << "\nAfter modifying obj2:\n";
    std::cout << "obj1.data: " << *obj1.data << std::endl;
    std::cout << "obj2.data: " << *obj2.data << std::endl;

    // Destructor will be called twice, once for obj1 and once for obj2
    // Both will try to delete the same memory, leading to undefined behavior
    return 0;
}        

Output:

Constructor: Allocated memory for data
After copying:
obj1.data: 10
obj2.data: 10

After modifying obj2:
obj1.data: 20
obj2.data: 20
Destructor: Released memory for data
Destructor: Released memory for data        

Explanation:

  1. Constructor: The constructor dynamically allocates memory for an integer and assigns it a value.
  2. Destructor: The destructor releases the dynamically allocated memory. This is explicitly defined to prevent memory leaks.
  3. Copy Constructor and Assignment Operator: These are not explicitly defined, so the compiler generates default versions that perform shallow copies. This means that the pointer data is copied directly, causing both objects (obj1 and obj2) to point to the same memory location.

Case 2: If I define a CC and not define destructor and AO which is against Rule 3 let's see what happens

#include <iostream>

class MyClass {
public:
	int* data;

	// Constructor
	MyClass(int val) {
		data = new int(val);
	}

	// Copy Constructor (Deep Copy)
	MyClass(const MyClass& other) {
		data = new int(*other.data);
		std::cout << "Copy constructor called, memory allocated.\n";
	}

	// Destructor and Copy Assignment Operator are not defined,
	// so the compiler will generate default versions (shallow destructor, shallow assignment).
};

int main() {
	MyClass obj1(10);
	MyClass obj2 = obj1;  // Invokes the deep copy constructor

	*obj2.data = 20;  // Modify obj2's data

	std::cout << "obj1.data: " << *obj1.data << std::endl;  // Should be 10
	std::cout << "obj2.data: " << *obj2.data << std::endl;  // Should be 20

	MyClass obj3(30);
	obj3 = obj1;  // Invokes the default assignment operator (shallow copy)

	*obj3.data = 50; //Modify obj2's data

	std::cout << "\nAfter assignment:\n";
	std::cout << "obj3.data: " << *obj3.data << std::endl;  //Should be 50 as we modified it
	std::cout << "obj1.data: " << *obj1.data << std::endl;  //Should be 10 but as obj3 has shallow copied pointer it will be 50

	// Memory leaks will occur because the default destructor doesn't free the deep copied memory

	return 0;
}
        

Output:

Copy constructor called, memory allocated.
obj1.data: 10
obj2.data: 20

After assignment:
obj3.data: 50
obj1.data: 50        

Explanation:

  1. Constructor: Allocates dynamic memory for an integer and assigns it a value.
  2. Copy Constructor: Defined explicitly to perform a deep copy. When obj2 is created as a copy of obj1, new memory is allocated, and the value is copied over.
  3. Copy Assignment Operator and Destructor: Neither are explicitly defined. Therefore, the compiler generates a default shallow copy assignment operator and a shallow destructor. The shallow copy assignment operator just copies the pointer, which can lead to multiple objects pointing to the same memory. The shallow destructor doesn't free the memory allocated by the deep copy constructor, leading to memory leaks.

Case 3: Similar to other two cases If I define a AO and not define destructor and CC which is against Rule 3. The shallow destructor doesn't free the memory allocated by the deep copy constructor, leading to memory leaks. The shallow copy constructor just copies the pointer, which can lead to multiple objects pointing to the same memory.

If you read till here then Albert Einstein will Bless you??


Nikhil Kumar

Software Engineer , Deevia

6 个月

Albert Einstein is writing with apple pen on blackboard??????

要查看或添加评论,请登录

Ajit Sakri的更多文章

社区洞察

其他会员也浏览了