The Rule of Three states that if a type ever needs to have a user-defined copy constructor, copy assignment operator, or destructor, then it must have all three.
The reason for the rule is that a class which needs any of the three manages some resource (file handles, dynamically allocated memory, etc), and all three are needed to manage that resource consistently. The copy functions deal with how the resource gets copied between objects, and the destructor would destroy the resource, in accord with RAII principles.
Consider a type that manages a string resource:
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
Since name
was allocated in the constructor, the destructor deallocates it to avoid leaking memory. But what happens if such an object is copied?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
First, p1
will be constructed. Then p2
will be copied from p1
. However, the C++-generated copy constructor will copy each component of the type as-is. Which means that p1.name
and p2.name
both point to the same string.
When main
ends, destructors will be called. First p2
's destructor will be called; it will delete the string. Then p1
's destructor will be called. However, the string is already deleted. Calling delete
on memory that was already deleted yields undefined behavior.
To avoid this, it is necessary to provide a suitable copy constructor. One approach is to implement a reference counted system, where different Person
instances share the same string data. Each time a copy is performed, the shared reference count is incremented. The destructor then decrements the reference count, only releasing the memory if the count is zero.
Or we could implement value semantics and deep copying behavior:
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
Implementation of the copy assignment operator is complicated by the need to release an existing buffer. The copy and swap technique creates a temporary object which holds a new buffer. Swapping the contents of *this
and copy
gives ownership to copy
of the original buffer. Destruction of copy
, as the function returns, releases the buffer previously owned by *this
.