Implementing the Prototype Pattern in C++: Shallow vs. Deep Copying
The Prototype patern is a creational design pattern used to create new objects by copying an existing instance, known as the prototype. In C++, this is typically achieved through copy constructors. When dealing with classes that manage dynamic memory, it is crucial to understand the distinction between shallow and deep copying to avoid memory corruption and unexpected behavior.
The Concept of Shallow Copying
A shallow copy creates a new object but does not duplicate the dynamically allocated memory that the object's members point to. Instead, the pointer in the new object is simply assigned the same memory address as the pointer in the original object. This results in both objects sharing the same underlying data buffer.
#include <iostream>
class DynamicArray {
private:
int* data;
size_t count;
public:
DynamicArray(size_t size) : count(size) {
data = new int[size];
for (size_t i = 0; i < size; ++i) data[i] = 0;
}
// Shallow Copy Constructor
DynamicArray(const DynamicArray& other) {
this->count = other.count;
this->data = other.data; // Only the pointer address is copied
}
~DynamicArray() {
// Note: In a shallow copy, this will cause a double-free error
// when both objects are destroyed.
delete[] data;
}
int& operator[](size_t index) { return data[index]; }
size_t getSize() const { return count; }
void print() const {
for (size_t i = 0; i < count; ++i) {
std::cout << data[i] << (i == count - 1 ? "" : ", ");
}
std::cout << std::endl;
}
};
int main() {
DynamicArray arr1(5);
for (int i = 0; i < 5; ++i) arr1[i] = i * 10;
std::cout << "Original array (arr1): ";
arr1.print();
// Perform shallow clone
DynamicArray arr2 = arr1;
arr2[0] = 999;
std::cout << "After modifying clone (arr2), arr1 becomes: ";
arr1.print();
return 0;
}
In the shallow copy example, modifying arr2 direct affects arr1 because they reference the same memory location. Furthermore, the program will likely crash during cleanup because the destructor attempts to delete[] the same pointer twice.
The Concept of Deep Copying
A deep copy allocates a fresh block of memory for the new object and copies the actual values from the original object into this new block. This ensures that the two objects are entirely independent; changes to one do not affect the other, and each handles its own memory lifecycle.
#include <iostream>
#include <algorithm> // For std::copy
class SecureBuffer {
private:
int* storage;
size_t length;
public:
SecureBuffer(size_t size) : length(size) {
storage = new int[size];
}
// Deep Copy Constructor
SecureBuffer(const SecureBuffer& source) {
this->length = source.length;
// Allocate new memory for the clone
this->storage = new int[this->length];
// Copy the actual content
std::copy(source.storage, source.storage + source.length, this->storage);
}
~SecureBuffer() {
delete[] storage;
}
int& at(size_t index) { return storage[index]; }
void display() const {
for (size_t i = 0; i < length; ++i) {
std::cout << storage[i] << (i == length - 1 ? "" : " | ");
}
std::cout << std::endl;
}
};
int main() {
SecureBuffer buffer1(5);
for (int i = 0; i < 5; ++i) buffer1.at(i) = i + 1;
std::cout << "Original Buffer: ";
buffer1.display();
// Perform deep clone
SecureBuffer buffer2 = buffer1;
buffer2.at(0) = 100;
std::cout << "After modifying buffer2, buffer1 remains: ";
buffer1.display();
std::cout << "Buffer 2 content: ";
buffer2.display();
return 0;
}
Key Differences and Considerations
- Resource Management: Shallow copies are computationally cheaper and faster because they only copy fixed-size pointers. However, deep copies are necessary for resource safety when heap memory is involved.
- Object Independence: Deep copying implements the true intent of the Prototype pattern, where the cloned object is a distinct entity that can be modified without side effects on the original instance.
- Default Behavior: In C++, the compiler-generated default copy constructor performs a shallow copy. If a class owns a raw pointer or a system resource (like a file handle), you must explicitly define a deep copy constructor or use smart pointers to manage ownership.