Выбрасывать исключения из деструктора

Большинство людей говорят, что никогда не выбрасывают исключение из деструктора - это приводит к неопределенному поведению. Страуструп подчеркивает, что "векторный деструктор явно вызывает деструктор для каждого элемента. Это означает, что, если деструктор элемента выбрасывает, векторное разрушение завершается неудачно... На самом деле нет хорошего способа защиты от исключений, генерируемых деструкторами, поэтому библиотека не дает никаких гарантий, если деструктор элемента выбрасывает " (из Приложения E3.2).

Эта статья, кажется, говорит об обратном - бросать деструкторы более или менее хорошо.

Итак, мой вопрос заключается в следующем: если бросок из деструктора приводит к неопределенному поведению, как вы обрабатываете ошибки, возникающие во время деструктора?

Если во время операции очистки возникает ошибка, вы просто игнорируете ее? Если это ошибка, которая потенциально может быть обработана в стеке, но не прямо в деструкторе, не имеет ли смысл выбрасывать исключение из деструктора?

Очевидно, что такого рода ошибки редки, но возможны.

18 ответов

Решение

Выбрасывать исключение из деструктора опасно.
Если другое исключение уже распространяется, приложение будет прекращено.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

Это в основном сводится к:

Все опасное (то есть, что может вызвать исключение) должно быть сделано с помощью открытых методов (не обязательно напрямую). Затем пользователь вашего класса может потенциально справиться с этими ситуациями, используя открытые методы и перехватывая любые потенциальные исключения.

Затем деструктор завершит работу объекта, вызвав эти методы (если пользователь не сделал этого явно), но любые исключения throw будут перехвачены (после попытки решить проблему).

Таким образом, вы фактически перекладываете ответственность на пользователя. Если пользователь может исправить исключения, он вручную вызовет соответствующие функции и обработает все ошибки. Если пользователь объекта не беспокоится (так как объект будет уничтожен), то деструктор остается заботиться о бизнесе.

Пример:

станд::fstream

Метод close() потенциально может вызвать исключение. Деструктор вызывает close(), если файл был открыт, но следит за тем, чтобы любые исключения не распространялись из деструктора.

Поэтому, если пользователь файлового объекта хочет выполнить специальную обработку для проблем, связанных с закрытием файла, он будет вручную вызывать close() и обрабатывать любые исключения. Если, с другой стороны, им все равно, деструктор останется справиться с ситуацией.

У Скотта Майерса есть отличная статья на эту тему в его книге "Эффективный C++"

Редактировать:

Видимо, также в "Более эффективный C++"
Пункт 11: Предотвратить исключения из деструкторов

Выброс деструктора может привести к сбою, потому что этот деструктор может быть вызван как часть "разматывания стека". Разматывание стека - это процедура, которая имеет место при возникновении исключения. В этой процедуре все объекты, которые были помещены в стек с момента "try" и до тех пор, пока не было сгенерировано исключение, будут завершены -> будут вызваны их деструкторы. И во время этой процедуры другой выброс исключения не разрешен, потому что невозможно обрабатывать два исключения одновременно, таким образом, это вызовет вызов abort(), программа потерпит крах, и элемент управления вернется в ОС.

Здесь мы должны дифференцироваться, а не слепо следовать общим советам для конкретных случаев.

Обратите внимание, что следующее игнорирует проблему контейнеров объектов и того, что нужно делать перед лицом множества объектов внутри контейнеров. (И это может быть частично проигнорировано, поскольку некоторые объекты просто не подходят для размещения в контейнере.)

Вся проблема становится легче думать, когда мы разделяем классы на два типа. У класса dtor могут быть две разные обязанности:

  • (R) освободить семантику (иначе освободить эту память)
  • (C) зафиксировать семантику (иначе файл сброса на диск)

Если мы рассмотрим вопрос таким образом, то я думаю, что можно утверждать, что семантика (R) никогда не должна вызывать исключение из dtor, так как a) мы ничего не можем с этим поделать и b) многие операции со свободными ресурсами не делают даже предусмотреть проверку ошибок, например voidfree(void* p);,

Объекты с семантикой (C), такие как файловый объект, который должен успешно очистить свои данные, или (база данных, защищенная областью), которая выполняет фиксацию в dtor, имеют другой вид: мы можем что-то сделать с ошибкой (в уровень приложения), и мы действительно не должны продолжать, как будто ничего не произошло.

Если мы следуем по маршруту RAII и учитываем объекты, которые имеют (C) семантику в своих d'-dors, я думаю, что тогда мы также должны учитывать нечетный случай, когда такие d-dors могут генерировать. Из этого следует, что вы не должны помещать такие объекты в контейнеры, а также из этого следует, что программа все еще может terminate() если commit-dtor выбрасывает, когда другое исключение активно.


Что касается обработки ошибок (семантика фиксации / отката) и исключений, то один хороший докладчик Андрей Александреску: обработка ошибок в C++ / декларативный поток управления (проводится на NDC 2014)

В деталях он объясняет, как библиотека Folly реализует UncaughtExceptionCounter для них ScopeGuard инструментов.

(Я должен отметить, что у других также были подобные идеи.)

Хотя речь не идет о броске из д'Тора, он показывает инструмент, который можно использовать сегодня, чтобы избавиться от проблем с тем, когда бросать из д'Тора.

В будущем для этого может существовать стандартная функция, см. N3614 и обсуждение этого вопроса.

Upd '17: функция C++17 std для этого std::uncaught_exceptions afaikt. Я быстро процитирую статью cppref:

Заметки

Пример где int -returning uncaught_exceptions is is is...... сначала создает объект защиты и записывает число необработанных исключений в его конструкторе. Вывод выполняется деструктором объекта защиты, если только foo() не сгенерирует (в этом случае число необработанных исключений в деструкторе больше, чем наблюдал конструктор)

Реальный вопрос, который нужно задать себе для броска из деструктора: "Что может сделать с этим вызывающий абонент?" Есть ли на самом деле что-нибудь полезное, что вы можете сделать, за исключением исключения, которое бы компенсировало опасности, создаваемые броском деструктора?

Если я уничтожу Foo объект, а Foo деструктор выбрасывает исключение, что я могу с ним разумно сделать? Я могу войти, или я могу игнорировать это. Это все. Я не могу это "исправить", потому что Foo объект уже ушел. В лучшем случае я регистрирую исключение и продолжаю, как будто ничего не произошло (или прекращаю работу программы). Действительно ли это стоит того, чтобы вызывать неопределенное поведение, выбрасывая из деструктора?

Из проекта ISO для C++ (ISO/IEC JTC 1/SC 22 N 4411)

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

3 Процесс вызова деструкторов для автоматических объектов, созданных на пути от блока try к выражению throw, называется "разматыванием стека". [Примечание: если деструктор, вызванный при разматывании стека, завершается с исключением, вызывается std::terminate (15.5.1). Таким образом, деструкторы должны, как правило, перехватывать исключения и не позволять им распространяться за пределы деструктора. - конец примечания]

Это опасно, но также не имеет смысла с точки зрения читабельности / понятности кода.

Что вы должны спросить в этой ситуации

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

Что должно поймать исключение? Стоит ли звонить Фу? Или Foo должен справиться с этим? Почему вызывающий объект foo должен заботиться о каком-то внутреннем объекте foo? Может быть, язык определяет это, чтобы иметь смысл, но он будет нечитаемым и трудным для понимания.

Что еще более важно, куда уходит память для Object? Куда уходит память, принадлежащая объекту? Это все еще распределено (якобы, потому что деструктор вышел из строя)? Учтите также, что объект находился в стековом пространстве, поэтому его, очевидно, не было.

Тогда рассмотрим этот случай

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

Когда удаление obj3 не удается, как я могу удалить таким образом, который гарантированно не будет отказывать? Это моя память, черт возьми!

Теперь рассмотрим в первом фрагменте кода Object автоматически удаляется, потому что он находится в стеке, в то время как Object3 находится в куче. Поскольку указатель на Object3 исчез, вы вроде SOL. У вас утечка памяти.

Теперь один безопасный способ сделать следующее

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Также смотрите этот FAQ

Я нахожусь в группе, которая считает, что бросок паттерна "ограниченная область действия" в деструкторе полезен во многих ситуациях - особенно для юнит-тестов. Однако следует помнить, что в C++11 добавление деструктора приводит к вызову std::terminate поскольку деструкторы неявно помечены noexcept,

Анджей Кшеменский имеет отличный пост на тему деструкторов, которые бросают:

Он указывает, что в C++11 есть механизм для переопределения по умолчанию noexcept для деструкторов:

В C++11 деструктор неявно указывается как noexcept, Даже если вы не добавите спецификацию и не определите свой деструктор следующим образом:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

Компилятор все равно незаметно добавит спецификацию noexcept вашему деструктору. А это значит, что в тот момент, когда ваш деструктор выдает исключение, std::terminate будет вызван, даже если не было ситуации двойного исключения. Если вы действительно намерены разрешить бросать ваши деструкторы, вы должны будете указать это явно; у вас есть три варианта:

  • Явно укажите ваш деструктор как noexcept(false),
  • Унаследуйте свой класс от другого, который уже определяет его деструктор как noexcept(false),
  • Поместите нестатический член данных в ваш класс, который уже определяет его деструктор как noexcept(false),

Наконец, если вы решите добавить деструктор, вы всегда должны осознавать риск двойного исключения (выбрасывание, когда стек разворачивается из-за исключения). Это вызвало бы вызов std::terminate и это редко то, что вы хотите. Чтобы избежать такого поведения, вы можете просто проверить, существует ли уже исключение, прежде чем выдавать новое, используя std::uncaught_exception(),

Ваш деструктор может выполняться внутри цепочки других деструкторов. Создание исключения, которое не было перехвачено вашим непосредственным абонентом, может оставить несколько объектов в несогласованном состоянии, что вызовет еще больше проблем, чем игнорирование ошибки в операции очистки.

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

Например:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

В качестве дополнения к основным ответам, которые являются хорошими, исчерпывающими и точными, я хотел бы прокомментировать статью, на которую вы ссылаетесь - ту, которая гласит: "бросать исключения в деструкторах не так уж и плохо".

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

Проблема в том, что ни одна из перечисленных в ней проблем с альтернативами не так плоха, как поведение исключения, которое, давайте запомним, является "неопределенным поведением вашей программы". Некоторые из возражений автора включают "эстетически уродливый" и "поощряют плохой стиль". Что бы вы предпочли? Программа с плохим стилем или с неопределенным поведением?

Итак, мой вопрос заключается в следующем: если бросок из деструктора приводит к неопределенному поведению, как вы обрабатываете ошибки, возникающие во время деструктора?

Основная проблема заключается в следующем: вы не можете потерпеть неудачу. Что значит потерпеть неудачу, в конце концов? Если фиксация транзакции в базе данных не удалась и не удалась (не удалось выполнить откат), что произойдет с целостностью наших данных?

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

Это концептуально сложная проблема, но часто решение состоит в том, чтобы просто найти способ убедиться, что сбой не может закончиться неудачей. Например, база данных может записать изменения до фиксации во внешней структуре данных или файле. Если транзакция не удалась, то структура файла / данных может быть отброшена. Все, что от него требуется, - убедиться, что фиксация изменений из этой внешней структуры / файла является атомарной транзакцией, которая не может завершиться неудачей.

Прагматичное решение, возможно, состоит в том, чтобы просто убедиться, что шансы на неудачу при неудаче астрономически маловероятны, так как в некоторых случаях практически невозможно сделать так, чтобы вещи потерпели неудачу.

Самое правильное решение для меня - написать логику неочищения таким образом, чтобы логика очистки не могла потерпеть неудачу. Например, если у вас возникнет желание создать новую структуру данных, чтобы очистить существующую структуру данных, возможно, вы захотите заранее создать эту вспомогательную структуру, чтобы нам больше не приходилось создавать ее внутри деструктора.

Это все гораздо легче сказать, чем сделать, по общему признанию, но это единственный действительно правильный способ, которым я вижу, чтобы сделать это. Иногда я думаю, что должна быть возможность написать отдельную логику деструктора для нормальных путей выполнения, отличных от исключительных, поскольку иногда деструкторам кажется, что они выполняют двойную ответственность, пытаясь справиться с обоими (например, защитники области действия, которые требуют явного увольнения они не потребовали бы этого, если бы могли отличить исключительные пути разрушения от неисключительных).

Тем не менее, конечная проблема заключается в том, что мы не можем не потерпеть неудачу, и это сложная концептуальная проблема дизайна, которую необходимо решить во всех случаях. Это становится легче, если вы не слишком запутаетесь в сложных управляющих структурах с тоннами маленьких объектов, взаимодействующих друг с другом, и вместо этого смоделируете свои проекты немного громоздким способом (например: система частиц с деструктором, чтобы уничтожить всю частицу система, а не отдельный нетривиальный деструктор на частицу). Когда вы моделируете свои проекты на таком более грубом уровне, у вас есть меньше нетривиальных деструкторов, с которыми можно иметь дело, и вы также можете часто позволить себе любые дополнительные затраты памяти / обработки, чтобы убедиться, что ваши деструкторы не могут выйти из строя.

И это одно из самых простых решений, естественно - реже использовать деструкторы. В приведенном выше примере с частицами, возможно, при уничтожении / удалении частицы, должны быть сделаны некоторые вещи, которые могут потерпеть неудачу по любой причине. В этом случае, вместо того, чтобы вызывать такую ​​логику через dtor частицы, который мог бы выполняться по исключительному пути, вы могли бы вместо этого сделать все это системой частиц, когда она удаляет частицу. Удаление частицы всегда может быть сделано во время неисключительного пути. Если система разрушена, возможно, она может просто очистить все частицы и не беспокоиться об этой отдельной логике удаления частиц, которая может дать сбой, в то время как логика, которая может дать сбой, выполняется только во время нормального выполнения системы частиц, когда она удаляет одну или несколько частиц.

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

Было бы очень полезно, если бы nothrow / noexcept фактически транслировался в ошибку компилятора, если бы что-либо, что его указывает (включая виртуальные функции, которые должны наследовать спецификацию noexcept своего базового класса), попыталось вызвать что-нибудь, что может выдать. Таким образом, мы сможем поймать все эти вещи во время компиляции, если мы на самом деле напишем деструктор, который может сработать.

Q: Итак, мой вопрос заключается в следующем: если выброс из деструктора приводит к неопределенному поведению, как вы обрабатываете ошибки, возникающие во время деструктора?

A: Есть несколько вариантов:

  1. Позвольте исключениям вытекать из вашего деструктора, независимо от того, что происходит в другом месте. И при этом следует помнить (или даже бояться), что может последовать std::terminate.

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

  3. мой любимый: если std::uncaught_exception возвращает ложь, позволяют вам исключения. Если он возвращает true, вернитесь к подходу регистрации.

Но хорошо ли бросать д'торы?

Я согласен с большинством из вышеперечисленного, что бросать лучше всего в деструкторе, где это возможно. Но иногда вам лучше всего принять это, и справиться с этим хорошо. Я бы выбрал 3 выше.

Есть несколько странных случаев, когда на самом деле отличная идея выбросить из деструктора. Как и код ошибки "должен проверить". Это тип значения, который возвращается из функции. Если вызывающая сторона читает / проверяет содержащийся код ошибки, возвращаемое значение уничтожается молча.Но если возвращенный код ошибки не был прочитан к тому времени, когда возвращаемые значения выходят из области видимости, он выдаст исключение из своего деструктора.

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

Проблема возникает, когда из деструктора возникает исключение во время процесса раскрутки стека. Если это происходит, компилятор оказывается в ситуации, когда он не знает, следует ли продолжить процесс разматывания стека или обработать новое исключение. Конечным результатом является то, что ваша программа будет немедленно прекращена.

Следовательно, лучший способ действий - просто воздерживаться от использования исключений в деструкторах вообще. Напишите сообщение в лог-файл.

Мартин Ба (выше) на правильном пути - вы по-разному разбираетесь в логике RELEASE и COMMIT.

Для выпуска:

Вы должны есть любые ошибки. Вы освобождаете память, закрываете соединения и т. Д. Никто другой в системе не должен снова ВИДЕТЬ эти вещи, а вы возвращаете ресурсы ОС. Если кажется, что вам нужна настоящая обработка ошибок, это, вероятно, является следствием недостатков дизайна в вашей объектной модели.

Для фиксации:

Здесь вы хотите использовать те же типы объектов-оболочек RAII, которые для мьютексов предоставляют такие вещи, как std::lock_guard. С теми, кого вы не помещаете логику коммитов в dtor вообще. У вас есть специальный API для него, а затем объекты-обертки, которые RAII передадут его в свои ИХД и обработают там ошибки. Помните, что вы можете легко ловить исключения в деструкторе; его выдача им это смертельно. Это также позволяет вам реализовать политику и другую обработку ошибок, просто создав другую обертку (например, std::unique_lock и std::lock_guard), и гарантирует, что вы не забудете вызвать логику фиксации, которая является единственным промежуточным этапом. достойное оправдание для того, чтобы поставить его в дтор на 1-м месте.

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

Если исключение прерывает обработку неперехваченного исключения, то есть после создания объекта исключения и до завершения активации обработчика исключений, это вызовет вызов std :: terminate.

Если исключение вообще не обрабатывается, будет вызываться std :: terminate, как и для любой другой функции, генерирующей исключение.

Начиная с C ++ 17 существует безопасный способ определить, есть ли какие-либо неперехваченные исключения в текущем потоке - это с помощью std :: uncaught_exceptions (множественное число), здесь можно найти красивый пример: https://en.cppreference.com / w / cpp / error / uncaught_exception.

Обратите внимание, что безопасно определить это с помощью std :: uncaught_exception (single) невозможно.

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

В настоящее время я придерживаюсь политики (о которой так много говорят), что классы не должны активно генерировать исключения из своих деструкторов, а вместо этого должны предоставить публичный метод "close" для выполнения операции, которая может завершиться ошибкой...

... но я верю, что деструкторы для классов контейнерного типа, такие как вектор, не должны маскировать исключения, выбрасываемые из классов, которые они содержат. В этом случае я фактически использую метод "free/close", который вызывает себя рекурсивно. Да, я сказал рекурсивно. В этом безумии есть метод. Распространение исключений зависит от наличия стека: если возникает единственное исключение, то оба оставшихся деструктора все еще будут работать, и ожидающее исключение будет распространяться, как только подпрограмма вернется, и это здорово. Если возникает несколько исключений, то (в зависимости от компилятора) либо это первое исключение будет распространяться, либо программа завершится, что нормально. Если возникает так много исключений, что рекурсия переполняет стек, тогда что-то серьезно не так, и кто-то узнает об этом, что тоже хорошо. Лично я ошибаюсь в ошибке взрыва, а не в том, чтобы быть скрытым, тайным и коварным.

Дело в том, что контейнер остается нейтральным, и содержащиеся в нем классы должны решить, будут ли они вести себя или вести себя неправильно в отношении создания исключений из своих деструкторов.

По-видимому, в особых случаях, таких как следующий сценарий, вы МОЖЕТЕ создать исключение в деструкторе и перехватить его для правильной обработки.

Код:

      #include <iostream>
#include <unordered_map>

class Foo
{
    public:
    Foo(int i)
    {
        std::cout << "Created\n";
    }
    
    ~Foo() noexcept(false)
    {
        throw std::runtime_error("I am an exception thrown from the destructor!");
    }
};

std::unordered_map<std::string, Foo> MyList;

int main() {
    MyList.emplace("foo", 1);
    try
    {
        MyList.erase("foo");
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what();
    }

    return 0;
}

Выход:

      Created
I am an exception thrown from the destructor!

Установите тревожное событие. Обычно тревожные события являются лучшей формой уведомления о сбое при очистке объектов.

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