Let's first assume that for this exercise, for whatever reason, you can't use std::vector
or std::array
. Assuming we follow the design of your classes so far, we can implement a copy constructor like this:
B::B(const B &other)
{
for (std::size_t i = 0; i < constant; ++i) {
// Use `new` to allocate memory and also call `A`'s copy constructor.
array[i] = new A(*other.array[i]);
}
}
What this does, as array
is an array of pointers to A
s, is allocates dynamic memory for every element in the array, and populates the array with pointers to these dynamically allocated A
objects using new
, while also calling the copy constructor you have made for A
, by using the syntax new A(other_a)
, which calls your A::A(const A &other)
. As other
is a reference to an A
, rather than a pointer to an A
, the pointer held in other.array[i]
is dereferenced, which is why the call is A(*other.array[i])
, rather than A(other.array[i])
.
As we have allocated dynamic memory here with new
, the destructor must call delete
for each of our calls to `new. This can be implemented similarly as so:
B::~B()
{
for (std::size_t i = 0; i < constant; ++i) {
// As each `A` has been allocated with `new`, they should now be
// destroyed.
delete array[i];
}
}
So what we have now is something which seems to work as we want and we may assume that's all there is to it. However, things start to get complicated, as what would happen if one of the allocations performed by new
fails and throws an exception? Or what if A
's constructor throws an exception? delete
will never be called on the elements which have been allocated with new
so far.
To make our copy constructor exception safe requires some slightly more convoluted code. For example, something like this:
B::B(const B &other)
{
std::size_t i;
try {
for (i = 0; i < constant; ++i) {
array[i] = new A(*other.array[i]);
}
} catch (...) {
// Delete all elements allocated so far
for (std::size_t d = 0; d < i; ++d) {
delete array[i];
}
// Re-throw the exception to the caller
throw;
}
}
Code like this can quickly become unmaintainable. To avoid such problems, a good guideline is that managing the lifetime of a resource which must be created and destroyed should be encapsulated by a class which only manages the lifetime of that one resource. This is important because if you start adding more constructs to your class similar to this array, then your class would be responsible for constructing and destructing even more than just this array, which would make exception safety even more difficult than it already is. In fact, the reason constructing and destructing your array is already quite convoluted is because your class is responsible for the lifetime of 7 separate resources (dynamically allocated objects), one for each array element.
With this in mind, a way to simplify this would be to use a class which encapsulates dynamically allocating and deallocating an object with new
and delete
. C++11 includes several classes which encapsulate the deallocation part at least, the most relevant being std::unique_ptr
and std::shared_ptr
. These classes however are designed to avoid copying. unique_ptr
is explicitly non-copyable, and copying a shared_ptr
just creates a new reference to the same resource, while keeping a reference count. This means you still have to manually implement copying.
You could switch to unique_ptr
by changing your declaration in B
from:
A *array[constant];
to:
std::unique_ptr<A> array[constant];
Then you could populate each member in your copy constructor with:
array[i] = std::unique_ptr<A>(new A(*other.array[i]));
With this approach, you would no longer have to worry about catching exceptions, as the destructor will be called automatically for each unique_ptr
in the array if an exception is thrown somewhere in the constructor. The unique_ptr
s which have not yet been assigned to will hold null pointers by default, and will safely do nothing when they are destroyed.
There is however one other approach: not using pointers / dynamic memory at all. You already have a class (A
) which is responsible for its own resource's lifetime.
To do this, the following declaration in B
can be changed from:
A *array[constant];
to:
A array[constant];
This would mean that you no longer need to define a copy constructor (or copy assignment operator) at all. If no copy constructor is provided in a C++ class, the class can be copied as though it has a simple memberwise copy constructor, which also works for arrays and will call the copy constructor for each element in the array. And as the array itself is part of the class and holds no pointers to dynamic memory, each element does not need to be manually deallocated with delete
.