To provide valuelike behavior, each object has to have its own copy of the resource that the class manages. That means each HasPtr
object must have its own copy of the string
to which ps
points. To implement valuelike behavior HasPtr
needs
• A copy constructor that copies the string
, not just the pointer
• A destructor to free the string
• A copy-assignment operator to free the object’s existing string
and copy the string
from its right-hand operand
The valuelike version of HasPtr
is
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
// each HasPtr has its own copy of the string to which ps points
HasPtr(const HasPtr &p):
ps(new std::string(*p.ps)), i(p.i) { }
HasPtr& operator=(const HasPtr &);
~HasPtr() { delete ps; }
private:
std::string *ps;
int i;
};
Our class is simple enough that we’ve defined all but the assignment operator in the class body. The first constructor takes an (optional) string
argument. That constructor dynamically allocates its own copy of that string
and stores a pointer to that string
in ps
. The copy constructor also allocates its own, separate copy of the string
. The destructor frees the memory allocated in its constructors by executing delete
on the pointer member, ps
.
Assignment operators typically combine the actions of the destructor and the copy constructor. Like the destructor, assignment destroys the left-hand operand’s resources. Like the copy constructor, assignment copies data from the right-hand operand. However, it is crucially important that these actions be done in a sequence that is correct even if an object is assigned to itself. Moreover, when possible, we should also write our assignment operators so that they will leave the left-hand operand in a sensible state should an exception occur (§ 5.6.2, p. 196).
In this case, we can handle self-assignment—and make our code safe should an exception happen—by first copying the right-hand side. After the copy is made, we’ll free the left-hand side and update the pointer to point to the newly allocated string
:
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); // copy the underlying string
delete ps; // free the old memory
ps = newp; // copy data from rhs into this object
i = rhs.i;
return *this; // return this object
}
In this assignment operator, we quite clearly first do the work of the constructor: The initializer of newp
is identical to the initializer of ps
in HasPtr
’s copy constructor. As in the destructor, we next delete
the string
to which ps
currently points. What remains is to copy the pointer to the newly allocated string
and the int
value from rhs
into this object.
To illustrate the importance of guarding against self-assignment, consider what would happen if we wrote the assignment operator as
// WRONG way to write an assignment operator!
HasPtr&
HasPtr::operator=(const HasPtr &rhs)
{
delete ps; // frees the string to which this object points
// if rhs and *this are the same object, we're copying from deleted memory!
ps = new string(*(rhs.ps));
i = rhs.i;
return *this;
}
If rhs
and this object are the same object, deleting ps
frees the string
to which both *this
and rhs
point. When we attempt to copy * (rhs.ps)
in the new
expression, that pointer points to invalid memory. What happens is undefined.
It is crucially important for assignment operators to work correctly, even when an object is assigned to itself. A good way to do so is to copy the right-hand operand before destroying the left-hand operand.
Exercise 13.23: Compare the copy-control members that you wrote for the solutions to the previous section’s exercises to the code presented here. Be sure you understand the differences, if any, between your code and ours.
Exercise 13.24: What would happen if the version of HasPtr
in this section didn’t define a destructor? What if HasPtr
didn’t define the copy constructor?
Exercise 13.25: Assume we want to define a version of StrBlob
that acts like a value. Also assume that we want to continue to use a shared_ptr
so that our StrBlobPtr
class can still use a weak_ptr
to the vector
. Your revised class will need a copy constructor and copy-assignment operator but will not need a destructor. Explain what the copy constructor and copy-assignment operators must do. Explain why the class does not need a destructor.
Exercise 13.26: Write your own version of the StrBlob
class described in the previous exercise.