У меня есть нетривиальный тип, который владеет несколько ресурсов. Как мне сконструировать это безопасным способом?
Например, вот демо-класс 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
, выделяет для них пространство и конструирует их:
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 ?! Что делать, если количество ресурсов не известно во время компиляции ?!
Есть ли лучше путь?
Есть ли лучше путь?
ДА
C ++ 11 предоставляет новую функцию под названием делегирующие конструкторы которая имеет дело с этой ситуацией очень изящно. Но это немного неуловимо.
Проблема с генерированием исключений в конструкторах заключается в том, чтобы понять, что деструктор создаваемого вами объекта не запускается, пока конструктор не завершится. Хотя деструкторы подобъектов (базы и члены) будут запущены, если возникнет исключение, как только эти подобъекты будут полностью построены.
Ключ здесь заключается в том, чтобы полностью построить X
до вы начинаете добавлять ресурсы к нему, и затем добавить ресурсы один за раз, сохраняя X
в действительном состоянии при добавлении каждого ресурса. Однажды X
полностью построен, ~X()
уберет любой беспорядок при добавлении ресурсов. До C ++ 11 это могло бы выглядеть так:
X x; // no resources
x.push_back(A(1)); // add a resource
x.push_back(A(2)); // add a resource
// ...
Но в C ++ 11 вы можете написать конструктор множественных ресурсов следующим образом:
X(const A& x, const A& y)
: X{}
{
data_ = static_cast<A*>(::operator new (2*sizeof(A)));
::new(data_) A{x};
++size_;
::new(data_ + 1) A{y};
++size_;
}
Это очень похоже на написание кода, полностью игнорирующего безопасность исключений. Разница в этой строке:
: X{}
Это говорит: Построить мне по умолчанию X
, После этой конструкции, *this
полностью построен, и если исключение выдается в последующих операциях, ~X()
запускается Это революционно!
Обратите внимание, что в этом случае, построенный по умолчанию X
не приобретает ресурсов. Действительно, это даже неявно noexcept
, Так что эта часть не выбросит. И это устанавливает *this
к действительному X
который содержит массив размером 0. ~X()
знает, как бороться с этим состоянием.
Теперь добавьте ресурс неинициализированной памяти. Если это бросает, у вас все еще есть построенный по умолчанию X
а также ~X()
правильно справляется с этим, ничего не делая.
Теперь добавьте второй ресурс: Созданная копия x
, Если это бросает, ~X()
будет по-прежнему освобождать data_
буфер, но без запуска каких-либо ~A()
,
Если второй ресурс успешен, установите X
в допустимое состояние путем увеличения size_
который является noexcept
операция. Если что-нибудь после этого кидает, ~X()
правильно очистит буфер длины 1.
Теперь попробуйте третий ресурс: сконструированная копия y
, Если эта конструкция бросает, ~X()
правильно очистит ваш буфер длины 1. Если он не выдает, сообщите *this
что теперь он владеет буфером длины 2.
Использование этой техники не требовать X
быть конструктивным по умолчанию. Например, конструктор по умолчанию может быть закрытым. Или вы можете использовать какой-то другой частный конструктор, который помещает X
в безресурсное состояние:
: X{moved_from_tag{}}
В C ++ 11, как правило, это хорошая идея, если ваш X
может иметь состояние без ресурсов, так как это позволяет вам иметь noexcept
Переместить конструктор, который поставляется в комплекте со всеми видами благ (и является предметом другого поста).
Делегирующие конструкторы C ++ 11 — это очень хороший (масштабируемый) метод для написания безопасных исключений конструкторов, если у вас есть состояние без ресурсов для создания в начале (например, конструктор noexcept по умолчанию).
Да, в C ++ 98/03 есть способы сделать это, но они не такие красивые. Вы должны создать базовый класс деталей реализации X
которая содержит логику разрушения X
, но не строительная логика. Будучи там, сделал это, я люблю делегировать конструкторы.
Я думаю, что проблема связана с нарушением принципа единой ответственности: класс X должен иметь дело с управлением временем жизни нескольких объектов (и это, вероятно, даже не его основная ответственность).
Деструктор класса должен освобождать только те ресурсы, которые класс получил напрямую. Если класс является просто составным (то есть экземпляр класса владеет экземплярами других классов), он в идеале должен полагаться на автоматическое управление памятью (через RAII) и просто использовать деструктор по умолчанию. Если класс должен управлять некоторыми специализированными ресурсами вручную (например, открывает дескриптор файла или соединение, получает блокировку или выделяет память), я бы рекомендовал выделить ответственность за управление этими ресурсами классу, выделенному для этой цели, а затем использовать экземпляры этот класс в качестве членов.
Использование стандартной библиотеки шаблонов на самом деле помогло бы, потому что она содержит структуры данных (такие как умные указатели и std::vector<T>
) которые исключительно справляются с этой проблемой. Они также являются выполнимыми, поэтому даже если ваш X должен содержать несколько экземпляров объектов со сложными стратегиями получения ресурсов, проблема безопасного управления ресурсами решается как для каждого члена, так и для содержащего составного класса X.
В C ++ 11, возможно, попробуйте что-то вроде этого:
#include "A.h"#include <vector>
class X
{
std::vector<A> data_;
public:
X() = default;
X(const A& x, const A& y)
: data_{x, y}
{
}
// ...
};