I've got a non-trivial type that owns multiple resources. How do I construct it in an exception safe manner?
For example, here is a demo class X
that holds an array of A
:
#include "A.h"
class X
{
unsigned size_ = 0;
A* data_ = nullptr;
public:
~X()
{
for (auto p = data_; p < data_ + size_; ++p)
p->~A();
::operator delete(data_);
}
X() = default;
// ...
};
Now the obvious answer for this particular class is to use std::vector<A>
. And that's good advice. But X
is only a stand-in for more complicated scenarios where X
must own multiple resources and it isn't convenient to use the good advice of "use the std::lib." I've chosen to communicate the question with this data structure simply because it is familiar.
To be crystal clear: If you can design your X
such that a defaulted ~X()
properly cleans everything up ("the rule of zero"), or if ~X()
only has to release a single resource, then that is best. However, there are times in real life when ~X()
has to deal with multiple resources, and this question addresses those circumstances.
So this type already has a good destructor, and a good default constructor. My question centers on a non-trivial constructor that takes two A
's, allocates space for them, and constructs them:
X::X(const A& x, const A& y)
: size_{2}
, data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
::new(data_) A{x};
::new(data_ + 1) A{y};
}
I have a fully instrumented test class A
and if no exceptions are thrown from this constructor it works perfectly well. For example with this test driver:
int
main()
{
A a1{1}, a2{2};
try
{
std::cout << "Begin\n";
X x{a1, a2};
std::cout << "End\n";
}
catch (...)
{
std::cout << "Exceptional End\n";
}
}
The output is:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
A(A const& a): 2
End
~A(1)
~A(2)
~A(2)
~A(1)
I have 4 constructions, and 4 destructions, and each destruction has a matching constructor. All is well.
However if the copy constructor of A{2}
throws an exception, I get this output:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)
Now I have 3 constructions but only 2 destructions. The A
resulting from A(A const& a): 1
has been leaked!
One way to solve this problem is to lace the constructor with try/catch
. However this approach is not scalable. After every single resource allocation, I need yet another nested try/catch
to test the next resource allocation and deallocate what has already been allocated. Holds nose:
X(const A& x, const A& y)
: size_{2}
, data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
try
{
::new(data_) A{x};
try
{
::new(data_ + 1) A{y};
}
catch (...)
{
data_->~A();
throw;
}
}
catch (...)
{
::operator delete(data_);
throw;
}
}
This correctly outputs:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)
But this is ugly! What if there are 4 resources? Or 400?! What if the number of resources is not known at compile time?!
Is there a better way?