Как сделать "попробуй / наконец" в C++, когда RAII не представляется возможным?
Я возвращаюсь к C++ с тяжелого C# фона и унаследовал некоторую кодовую базу C++, которая, я думаю, могла не соответствовать лучшим практикам C++.
Например, я имею дело со следующим случаем (упрощенно):
// resource
class Resource {
HANDLE _resource = NULL;
// copying not allowed
Resource(const Resource&);
Resource& operator=(const Resource& other);
public:
Resource(std::string name) {
_resource = ::GetResource(name); if (NULL == _resource) throw "Error"; }
~Resource() {
if (_resource != NULL) { CloseHandle(_resource); _resource = NULL; };
}
operator HANDLE() const { return _resource; }
};
// resource consumer
class ResourceConsumer {
Resource _resource;
// ...
public:
void Initialize(std::string name) {
// initialize the resource
// ...
// do other things which may throw
}
}
Вот ResourceConsumer
создает экземпляр Resource
и делает некоторые другие вещи. По какой-то причине (вне моего контроля) это подвергает Initialize
метод для этого, вместо того, чтобы предлагать конструктор не по умолчанию, что явно нарушает шаблон RAII. Это библиотечный код, и API не может быть реорганизован без внесения серьезных изменений.
Итак, мой вопрос, как кодировать Initialize
правильно в этом случае? Является ли приемлемой практикой использование постепенного строительства / разрушения и повторного броска, как показано ниже? Как я уже сказал, я пришел из C#, где я просто использовал try/finally
или using
образец для этого.
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
_resource.~Resource();
// then construct it in-place
new (&_resource) Resource(name);
try {
// do other things which may throw
// ...
}
catch {
// we don't want to leave _resource initialized if anything goes wrong
_resource.~Resource();
throw;
}
}
2 ответа
Делать Resource
подвижный тип. Дайте ему ход строительства / назначения. Тогда ваш Initialize
Метод может выглядеть так:
void ResourceConsumer::Initialize(std::string name)
{
//Create the resource *first*.
Resource res(name);
//Move the newly-created resource into the current one.
_resource = std::move(res);
}
Обратите внимание, что в этом примере нет необходимости в логике обработки исключений. Все работает само собой. Создав новый ресурс первым, если это создание выдает исключение, то мы сохраняем ранее созданный ресурс (если есть). Это обеспечивает надежную гарантию исключения: в случае исключения состояние объекта сохраняется точно так, как оно было до исключения.
И обратите внимание, что нет необходимости явного try
а также catch
блоки. RAII просто работает.
Ваш Resource
Операции перемещения будут выглядеть так:
class Resource {
public:
Resource() = default;
Resource(std::string name) : _resource(::GetResource(name))
{
if(_resource == NULL) throw "Error";
}
Resource(Resource &&res) noexcept : _resource(res._resource)
{
res._resource = NULL;
}
Resource &operator=(Resource &&res) noexcept
{
if(&res != this)
{
reset();
_resource = res._resource;
res._resource = NULL;
}
}
~Resource()
{
reset();
}
operator HANDLE() const { return _resource; }
private:
HANDLE _resource = NULL;
void reset() noexcept
{
if (_resource != NULL)
{
CloseHandle(_resource);
_resource = NULL;
}
}
};
Я оставляю этот ответ здесь просто для справки, в качестве примера ответа, который недостаточно усердно рассматривал полный сценарий ОП. Поскольку ФП сами сняли исключение и явно просто использовали предложение try/catch для предполагаемых целей RAII, не имея другого использования для него.
Николь Болас ответ определенно путь.
Оригинальный ответ:
Если вы хотите убедиться, что деструктор для _resource
вызывается в случае, если что-то пойдет не так, то вы могли бы иметь Resource _resource
какой-то уникальный смарт-указатель, а затем сделать временный смарт-указатель в области видимости ResourceConsumer::Initialize()
и в конечном итоге переместить темп _resource
если все пойдет хорошо. Во всех других сценариях область будет закрыта до того, как перемещение и размотка стека вызовут соответствующий деструктор для временного.
Пример кода, пытаясь как можно больше придерживаться своего фрагмента в вопросе:
// resource consumer
class ResourceConsumer {
template<class T> using prop_ptr = std::experimental::propagate_const<std::unique_ptr<T>>;
prop_ptr<Resource> _resource;
// ...
public:
void Initialize(std::string name);
};
void ResourceConsumer::Initialize(std::string name) {
// first destroy _resource in-place
std::experimental::get_underlying(_resource).reset(); // See 'Note 2' below.
// then construct it in-place
auto tempPtr = std::make_unique<Resource>(name);
// do other things which may throw
// ...
// Initialization is done successfully, move the newly created one onto your member
_resource = move(tempPtr);
// we don't want to leave _resource initialized if anything goes wrong
// Fortunately, in case we didn't get here, tempPtr is already being destroyed after the next line, and _resource remains empty :-)
}
Примечание 1: так как я понял catch
пункт был просто перебрасыванием, мы получаем тот же эффект без него просто отлично.
Примечание 2: Вы можете безопасно удалить звонок reset()
если вы хотите, чтобы семантика исключений была такой, чтобы в случае неудачной инициализации в ресурс не вносилось никаких изменений. Это предпочтительный способ, ака сильная гарантия исключения. В противном случае оставьте его там, чтобы гарантировать пустой ресурс в случае сбоя инициализации.
Примечание 3: я использую propagate_ptr
обертка вокруг unique_ptr
сохранить const-квалификацию _resource
член под const
путь доступа, т.е. при работе с const ResourceConsumer
, Не забудь #include <experimental/propagate_const>
,