Examples
An example of how not to do it!
To illustrate the preceding concepts, we'll first work with an example of what
NOT to do.
Unfortunately, it's also a commonly used approach until people learn better.
Consider how we might implement the constructors and assignment operator for
a class that dynamically allocates a dynamically allocated array.
// example of poorly constructed resizable vector
template class PoorResizableVector
{
public:
PoorResizableVector(int default_size);
void Resize(int size);
PoorResizableVector(const PoorResizableVector &);
PoorResizableVector &operator=(const PoorResizableVector &);
~PoorResizableVector();
private:
X *data;
int size;
};
template PoorResizableVector::PoorResizableVector(int default_size)
{
data = NULL;
size = 0;
Resize(default_size);
}
template void PoorResizableVector::Resize(int newsize)
{
if (newsize > size)
{
double *newdata = new X[newsize];
if (size > 0)
for (i = 0; i < size; ++i) newdata[i] = data[i];
size = newsize;
delete [] data;
data = newdata;
}
else if (newsize < size)
{
size = newsize;
}
}
// copy constructor
template PoorResizableVector::PoorResizableVector(const PoorResizableVector &a)
{
data = new X[a.size];
size = a.size;
if (size > 0)
for (i = 0; i < size; ++i) data[i] = a.data[i];
}
template PoorResizableVector &PoorResizableVector::operator=(const PoorResizableVector &a)
{
if (this == &a) return *this;
data = new X[a.size];
size = a.size;
if (size > 0)
for (i = 0; i < size; ++i) data[i] = a.data[i];
return *this;
}
template PoorResizableVector::~PoorResizableVector()
{
delete [] data;
}
This example uses the "two-phase construction" approach, as it initialises into a default "safe" state, and requires resizing of the vector later. The problem is that the Resize() function may fail and throw exceptions in two distinct ways. The first way (dynamically allocating a new array) is harmless, as the C++ language guarantees that no memory will be leaked. The second possible failure occurs when copying the elements from the old vector to the new: these operations can fail if assignment of an X can fail (eg X::operator=() might dynamically allocate memory, and fail by throwing an exception). The copy constructor has exactly the same problems. The assignment operator has these problems plus one more: the approach will fail for self-assignment (eg v = v;), so a test is necessary.
The basic issue is that the logic of PoorResizableVector is muddled, and no care is taken to ensure exception safety. Modifying this example, without changing the core logic, to make it provide useful guarantees would actually be quite difficult. For example, when resizing an array, it is necessary to copy existing elements into the resized array. If an exception occurs, it needs to be caught and the memory allocated for the new size array needs to be released. I leave that as an exercise....
An example of how to do it
A better example is as follows.
// example of better constructed resizable vector
template class BetterResizableVector
{
public:
BetterResizableVector(int default_size = 10);
BetterResizableVector(const BetterResizableVector &);
BetterResizableVector &operator=(const BetterResizableVector &);
~BetterResizableVector();
X &operator[](int index);
private:
X *data;
int size;
};
template BetterResizableVector::BetterResizableVector(int default_size):
data(new X[default_size]), size(default_size)
{
}
// copy constructor
template BetterResizableVector::BetterResizableVector(const BetterResizableVector &a): data(new X[a.size]), size(a.size)
{
for (i = 0; i < size; ++i) data[i] = a.data[i];
}
template BetterResizableVector &BetterResizableVector::operator=(const BetterResizableVector &a)
{
BetterResizableVector temp(a);
// swap contents of temp and *this.
X *ptemp = data;
data = temp.data;
temp.data = ptemp;
int tsize = size;
size = temp.size;
temp.size = tsize;
// the preceding 6 lines may be more simply expressed as:
// std::swap(size, temp.size); std::swap(data, temp.data);
return *this;
}
template BetterResizableVector::~BetterResizableVector()
{
delete [] data;
}
This default constructor does not rely on "two phase construction". If memory cannot be assigned, an exception is thrown and it is (for the function trying to create the vector) as if the vector has never existed. Similarly, if the copying of elements fails in the copy constructor, the memory allocated previously is recovered. The assignment operator is interesting, as it immediately creates a temporary copy of the vector being assigned. If this fails, the temporary copy is cleaned up. The subsequent operations simply swap the data between temp and *this. As these swaps are simply swapping of two raw pointers and of two integers, no exception can occur when doing the swap. The variable temp will be destructed as the function returns, thereby cleaning up the original data in *this.
It must be noted that this approach comes at a cost: an additional object (and associated resources) must exist while the assignment operator is doing its work. In a lot of cases, safer code is sufficient to justify a short term requirement for additional resources. In some cases, it is not, and there is a trade-off between performance and the type of guarantee made when an exception is thrown.
The key, however, is that the type of exception that may be thrown by each line of code needs to be analysed, and the feasibility of recovering from it determined. In the example given, the order of operations (create a complete copy, swap over the internal representations) results in cleaner code because, if an exception occurs, the original object is left unchanged.
An alternate implementation of the assignment operator, which actually does almost the same thing, is;
template BetterResizableVector &BetterResizableVector::operator=(const BetterResizableVector &a)
{
X *tempX = new X[a.size];
try
{
for (int i = 0; i < a.size; ++i) tempX[i] = a.data[i];
}
catch (...)
{
delete [] tempX;
}
delete [] data;
data = tempX;
size = a.size;
return *this;
}
This implementation avoids creating a temporary BetterResizableVector, and saves resources by not needing to store an additional integer value or swapping operations. Because of the need for exception handling and recovery, I would argue it is slightly more difficult to understand. In particular, it is necessary to determine what happens if an exception occurs partway through the loop. Other people may prefer the second version.