Вопрос

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

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

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

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

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

Это было полезно?

Решение

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

#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";
    }

}

В основном это сводится к:

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

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

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

Пример:

std::fstream

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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


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

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

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

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

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

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

Примечания

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

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

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

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

То, что вы должны спросить, относится к данной ситуации

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

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

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

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

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 завершается неудачей, как мне на самом деле удалить таким образом, чтобы гарантированно не произошло сбоя?Это моя память, черт возьми!

Теперь рассмотрим, что в первом фрагменте кода Объект автоматически удаляется, потому что он находится в стеке, в то время как Object3 находится в куче.Поскольку указатель на Object3 исчез, ты вроде как СОЛЬ.У вас утечка памяти.

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

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

};

Также смотрите на это Вопросы и ответы

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

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

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

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

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

Например:

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...
};

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

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

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

Я нахожусь в группе, которая считает, что шаблон "scoped guard", добавляемый в деструктор, полезен во многих ситуациях - особенно для модульных тестов.Однако имейте в виду, что в 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().

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

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

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

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

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

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

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

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

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

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

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

Мартин Ба (выше) на правильном пути - вы по-разному разрабатываете логику ВЫПУСКА и фиксации.

Для Освобождения:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top