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:
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:
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:
Benefits of Deep Copy:
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:
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:
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:
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??
Software Engineer , Deevia
6 个月Albert Einstein is writing with apple pen on blackboard??????