У меня нетривиальный тип, которому принадлежат несколько ресурсов. Как я могу построить его безопасным образом?
Например, вот демон-класс X
, который содержит массив 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;
// ...
};
Теперь очевидным ответом для этого конкретного класса является использование std::vector<A>
. И этот хороший совет. Но X
является только резервной копией для более сложных сценариев, в которых X
должен владеть несколькими ресурсами, и нетрудно использовать хороший совет "использовать std:: lib". Я решил передать вопрос с этой структурой данных просто потому, что он знаком.
Чтобы быть кристально понятным: если вы можете создать свой X
таким образом, чтобы по умолчанию ~X()
правильно очистил все ( "правило нуля" ) или если ~X()
должен только освободить один ресурс, тогда это лучше всего. Однако в реальной жизни есть моменты, когда ~X()
приходится иметь дело с несколькими ресурсами, и этот вопрос затрагивает эти обстоятельства.
Итак, этот тип уже имеет хороший деструктор и хороший конструктор по умолчанию. Мой вопрос сосредотачивается на нетривиальном конструкторе, который принимает два A
's, выделяет для них пространство и строит их:
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};
}
У меня есть полностью оснащенный тестовый класс A
, и если из этого конструктора не исключены исключения, он работает отлично. Например, с помощью этого тестового драйвера:
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";
}
}
Вывод:
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)
У меня есть 4 конструкции и 4 разрушения, и каждое уничтожение имеет соответствующий конструктор. Все хорошо.
Однако, если конструктор копирования A{2}
выдает исключение, я получаю этот вывод:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)
Теперь у меня есть 3 конструкции, но только 2 разрушения. A
, полученный из A(A const& a): 1
, просочился !
Один из способов решения этой проблемы - крутить конструктор с помощью try/catch
. Однако этот подход не является масштабируемым. После каждого распределения ресурсов мне нужно еще одно вложенное try/catch
, чтобы протестировать следующее распределение ресурсов и освободить выделенное выделение. Держит нос:
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;
}
}
Это правильно выводит:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)
Но это уродливо! Что делать, если есть 4 ресурса? Или 400?! Что делать, если количество ресурсов неизвестно во время компиляции?!
Есть ли лучший способ?