Как сделать "попробуй / наконец" в C ++, когда RAII не представляется возможным? - PullRequest
0 голосов
/ 11 сентября 2018

Я возвращаюсь к 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 ]

0 голосов
/ 11 сентября 2018

Сделать 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;
        }
    }
};
0 голосов
/ 11 сентября 2018

Я оставляю этот ответ здесь только для справки, в качестве примера ответа, который не был достаточно усердным в полном сценарии ОП. Так как сами ОП пересмотрели исключение и явно просто использовали предложение 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(), если хотите, чтобы семантика исключений была такой, чтобы в случае неудачной инициализации не было сделано никаких изменений в resource . Это предпочтительный способ, a.k.a. Сильная гарантия исключения . В противном случае оставьте его там, чтобы гарантировать пустой ресурс в случае сбоя инициализации.

Примечание 3: Я использую оболочку propagate_ptr вокруг unique_ptr, чтобы сохранить const-квалификацию члена _resource в const пути доступа, т.е. при работе с const ResourceConsumer. Не забудьте #include <experimental/propagate_const>.

...