Deep copy constructor and assignment operator
This is something I have seen at almost all C++ coding interviews I had.
Having a raw pointer in a class, there is a need for a constructor that copies not only the pointer but the data it points to as well. The same applies to overloaded assignment operator=.
If we don't do deep copy, the compiler automatically generates a shallow copy constructor, which is basically a memberwise copy. We can get serious troubles, for example when passing our objects to functions by value.
Here is a snippet code for a class that has a pointer and a deep copy constructor.
class ObjectPtr
{
private:
std::string* m_ps;
int m_value;
public:
//normal constructor
ObjectPtr(std::string const &s, int value) :
m_ps{ new std::string(s) }, m_value{ value }
{}
//deep copy constructor
ObjectPtr(ObjectPtr const& source) :
m_ps{ new std::string(*source.m_ps) }, m_value{ source.m_value }
{}
//deep copy assignment operator
ObjectPtr& operator=(ObjectPtr const& rhs);
~ObjectPtr() { delete m_ps; }
};
Notice, that deep copy constructor looks quite the same as the normal initialization constructor. This piece is critical
m_ps{ new std::string(*source.m_ps)
The pointer of the newly created object will point to a different memory address, where it copied the data from the source object. Had it been written like this
m_ps{ source.m_ps }
Next is the overloaded operator=
The operator= combines the functions of copy constructor and destructor. There is quite a simple pattern when writing a deep copy assignment operator.
1. check if there is self-assignment (operator= must work well in these cases), if so, just return this object, if not
2. delete the data from the left-hand side (here comes the similarity with destructor)
3. do the deep copying (here comes the similarity with copy constructor)
4. return this object
This is how it looks
ObjectPtr& ObjectPtr::operator=(ObjectPtr const& rhs)
{
if (this == &rhs) // 1. check for self assignement
return *this;
else
{
delete m_ps; // 2. delete the data from lhs
m_ps = new std::string(*rhs.m_ps); // 3. do the copying
m_value = rhs.m_value;
return *this; // 4. return this object
}
}
Another way is not to check for self-assignment but use a temporary object where we copy the data from the right-hand side operand. The pattern would be like this.
1. copy the RHS's data to a newly created temporary object
2. delete the LHS's data
3. copy the data from the temporary object to LHS
4. return this object
Here is the code
ObjectPtr& ObjectPtr::operator=(ObjectPtr const& rhs)
{
std::string* temp{ new std::string(*rhs.m_ps) }; // 1. copy the rhs's data to newly created temporary object
delete m_ps; // 2. delete the lhs's data
m_ps = temp; // 3. copy the data from the temporary object to lhs
m_value = rhs.m_value;
return *this; // 4. return this object
}
There is no need to check for self-assignment here because we copied the data first and after destroyed the RHS object.
It would make a serious bug if we destroyed the RHS object first, and after doing the copying because in case there would be a self-assignment, we would copy invalid data, from the object we just deleted.
ObjectPtr& ObjectPtr::operator=(ObjectPtr const& rhs)
{
delete m_ps; // WRONG first delete
m_ps = new std::string(*rhs.m_ps); // in case of self assignement now copying from invalid deleted memory -> CRASH
m_value = rhs.m_value;
return *this;
}