Как сделать "попробуй / наконец" в 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>,

Другие вопросы по тегам