выбрасывать исключения из деструктора - PullRequest
240 голосов
/ 25 сентября 2008

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

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

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

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

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

Ответы [ 16 ]

183 голосов
/ 25 сентября 2008

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

#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 ++"

Edit:

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

51 голосов
/ 17 декабря 2008

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

46 голосов
/ 04 ноября 2010

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

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

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

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

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

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

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


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

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

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

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

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

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

Примечания

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

19 голосов
/ 25 сентября 2008

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

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

12 голосов
/ 11 апреля 2009

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

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

12 голосов
/ 25 сентября 2008

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

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

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

Что должно поймать исключение? Стоит ли звонить из foo? Или 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

7 голосов
/ 25 сентября 2008

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

5 голосов
/ 20 января 2009

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

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

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

5 голосов
/ 19 января 2009

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

Например:

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...
};
4 голосов
/ 02 января 2017

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

Andrzej Krzemieński имеет отличный пост на тему деструкторов, которые бросают:

Он указывает, что в 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().

...