Обработка сбоев деструкторами против catch (…) {fix (); бросить; }

Допустим, я делаю что-то, что требует очистки, когда выдается исключение.

Например, скажем, я создаю динамический массив, и мне нужно создавать объекты, но их конструкторы могут выдавать исключение:

size_t const n = 100;
T *const p = static_cast<T *>(operator new(sizeof(T) * n));
size_t i;
for (i = 0; i < n; ++i)
new (&p[i]) T(1, 2, 3);      // Not exception-safe if T::T(T const &) throws!

Я могу исправить это либо через catch (...) { ...; throw; }:

size_t const n = 100;
T *const p = static_cast<T *>(operator new(sizeof(T) * n));
size_t i;
try
{
for (i = 0; i < n; ++i)
new (&p[i]) T(1, 2, 3);
}
catch (...)
{
while (i > 0)
p[--i].~T();
operator delete(p);
throw;
}

или через ограниченный деструктор:

size_t n = 100;
struct Guard
{
T *p;
size_t i;
Guard(size_t n) : i(), p(static_cast<T *>(operator new(sizeof(T) * n))) { }
~Guard()
{
while (i > 0)
p[--i].~T();
operator delete(p);
}
} guard(n);

for (guard.i = 0; guard.i < n; ++guard.i)
new (&guard.p[guard.i]) T(1, 2, 3);

guard.i = 0;     // Successful... "commit" the changes
guard.p = NULL;  // or whatever is necessary to commit the changes

Какую технику я предпочитаю использовать, когда и почему?

(Замечания: Этот пример только предназначено, чтобы показать разницу между двумя методами. Я знаю, что это не идеальный код, поэтому, пожалуйста, делать не сосредоточиться на этом конкретном примере. Это только для иллюстрации.)

2

Решение

Решение с деструктором лучше явного try/catch:

  • его можно использовать повторно, если вам потребуется выполнить аналогичную инициализацию в другой функции, вы можете просто повторно использовать тот же класс защиты
  • его легче поддерживать — допустим, что в некоторых ситуациях ваша функция должна возвращаться с ошибкой, но исключение не выдается. С классом охраны это обрабатывается более или менее автоматически
  • это чище, так как код более модульный
2

Другие решения

В общем, я бы сказал, что это вопрос пересчет а также безопасность.

Проблема с try/catch является двойным:

  • проблема безопасности: любое раннее возвращение, которое игнорирует catch (каким-то образом) не удается очистить
  • проблема масштабирования: вложенная try/catch блоки делают беспорядок в коде
  • обзорная проблема: быть доступной в catch переменная должна быть определена до try и, таким образом, поддерживает default-construction / nullability; это может быть больно

Вместо этого, отложенные операторы и охранники не создают ненужных блоков / областей и, следовательно, не создают отступов, а читаются линейно.

Пример:

char buffer1[sizeof(T)];
try {
new (buffer1) T(original);

char buffer2[sizeof(T)];
try {
new (buffer2) T(original);

// stuff here

} catch(...) {
reinterpret_cast<T*>(buffer2)->~T();
throw;
}

} catch(...) {
reinterpret_cast<T*>(buffer1)->~T();
throw;
}

По сравнению с:

char buffer1[sizeof(T)];
new (buffer1) T(original);
Defer const defer1{[&buffer1]() { reinterpret_cast<T*>(buffer1)->~T(); } };

char buffer2[sizeof(T)];
new (buffer2) T(original);
Defer const defer1{[&buffer2]() { reinterpret_cast<T*>(buffer2)->~T(); } };

// stuff here

Я хотел бы отметить, что это хорошая идея, чтобы обобщить эти:

class Guard {
public:
explicit Guard(std::function<void()> f): _function(std::move(f)) {}

Guard(Guard&&) = delete;
Guard& operator=(Guard&&) = delete;

Guard(Guard const&) = delete;
Guard& operator=(Guard const&) = delete;

~Guard() {
if (not _function) { return; }
try { _function(); } catch(...) {}
}

void cancel() { _function = std::function<void()>{}; }

private:
std::function<void()> _function;
}; // class Guard

class Defer {
public:
explicit Defer(std::function<void()> f): _guard(std::move(f)) {}
private:
Guard _guard;
}; // class Defer
2

По вопросам рекламы [email protected]