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


257

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

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

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

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

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


36
«Два исключения одновременно» - это стандартный ответ, но это НЕ РЕАЛЬНАЯ причина. Настоящая причина заключается в том, что исключение следует выдавать тогда и только тогда, когда постусловия функции не могут быть выполнены. Постусловием деструктора является то, что объект больше не существует. Этого не может быть. Поэтому любая подверженная сбоям операция с истекшим сроком службы должна вызываться как отдельный метод, прежде чем объект выйдет из области видимости (разумные функции обычно имеют только один путь успеха в любом случае).
spraff

29
@ Spraff: Вы знаете, что то, что вы сказали, подразумевает "выбросить RAII"?
Кос

16
@spraff: необходимость вызова «отдельного метода до того, как объект выйдет из области видимости» (как вы написали) фактически отбрасывает RAII! Код, использующий такие объекты, должен будет гарантировать, что такой метод будет вызываться до вызова деструктора. Наконец, эта идея вообще не помогает.
Фрунси

8
@Frunsi нет, потому что эта проблема связана с тем, что деструктор пытается сделать что-то большее, чем просто высвобождение ресурсов. Соблазнительно сказать «я всегда хочу закончить XYZ» и думаю, что это аргумент для введения такой логики в деструктор. Нет, не ленитесь, пишите xyz()и держите деструктор в чистоте от не-RAII логики.
spraff

6
@Frunsi Например, фиксация чего-либо в файле не всегда подходит для деструктора класса, представляющего транзакцию. Если фиксация не удалась, уже слишком поздно ее обрабатывать, когда весь код, который был задействован в транзакции, вышел из области видимости. Деструктор должен отменить транзакцию, если commit()не вызван метод.
Николас Уилсон

Ответы:


198

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

#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: Предотвращение исключения из деструкторов


5
«Если вы не возражаете против потенциального завершения приложения, вам, вероятно, следует проглотить ошибку». - это, вероятно, должно быть исключением (простите за каламбур), а не правилом, то есть быстрым провалом.
Эрик Форбс

15
Я не согласен. Завершение программы останавливает разматывание стека. Больше не будет называться деструктор. Все открытые ресурсы останутся открытыми. Я думаю, что проглатывание исключения будет предпочтительным вариантом.
Мартин Йорк,

20
Операционная система может очистить ресурсы, которые она отключила. Память, FileHandles и т. Д. Как насчет сложных ресурсов: соединения с БД. Этот канал связи с МКС, который вы открыли (он автоматически отправляет закрытые соединения)? Я уверен, что НАСА захочет, чтобы вы аккуратно закрыли соединение!
Мартин Йорк,

7
Если приложение будет «быстро отказывать» из-за прерывания, оно не должно вызывать исключения в первую очередь. Если он потерпит неудачу, передав управление обратно в стек, он не должен делать это таким образом, чтобы программа могла быть прервана. Один или другой, не выбирайте оба.
Том

2
@LokiAstari Транспортный протокол, который вы используете для связи с космическим кораблем, не может обработать разорванное соединение? Хорошо ...
doug65536

54

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


1
не могли бы вы рассказать, как вызывался метод abort () в описанной выше ситуации. Означает, что контроль за исполнением был все еще с компилятором C ++
Кришна Оза

1
@Krishna_Oza: Довольно просто: всякий раз, когда выдается ошибка, код, который вызывает ошибку, проверяет некоторый бит, который указывает, что система времени выполнения находится в процессе разматывания стека (т. Е. Обрабатывает какой-то другой, throwно еще не нашел catchблок для него) в этом случае std::terminate(не abort) вызывается вместо вызова (нового) исключения (или продолжения разматывания стека).
Марк ван Леувен

53

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

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

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

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

Если мы рассмотрим вопрос таким образом, то я думаю, что можно утверждать, что семантика (R) никогда не должна вызывать исключение из dtor, так как a) мы ничего не можем с этим поделать и b) многие операции со свободными ресурсами не делают даже предусмотреть проверку ошибок, например void free(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::uncaught_exceptionsafaikt. Я быстро процитирую статью cppref:

Ноты

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


6
Высоко согласен. И добавление еще одной семантической (Ro) семантики отката. Обычно используется в области охраны. Как и в моем проекте, где я определил макрос ON_SCOPE_EXIT. Случай с семантикой отката заключается в том, что здесь может произойти что-либо значимое. Таким образом, мы действительно не должны игнорировать неудачу.
Weipeng L

Я чувствую, что единственная причина, по которой мы имеем семантику фиксации в деструкторах, заключается в том, что C ++ не поддерживает finally.
user541686

@Mehrdad: finally это dtor. Это всегда называется, несмотря ни на что. Для синтаксической аппроксимации finally см. Различные реализации scope_guard. В настоящее время, когда имеется механизм (даже в стандарте, это C ++ 14?), Чтобы определить, разрешено ли бросать dtor, его даже можно сделать абсолютно безопасным.
Мартин Ба

1
@MartinBa: я думаю, что вы пропустили смысл моего комментария, что удивительно, так как я согласился с вашим представлением о том, что (R) и (C) различны. Я пытался сказать, что дтор по своей сути является инструментом для (R) и finallyпо сути является инструментом для (С). Если вы не понимаете, почему: подумайте, почему допустимо бросать исключения друг на друга в finallyблоках, и почему это не относится к деструкторам. (В некотором смысле, это данные против контроля . Деструкторы предназначены для освобождения данных, finallyдля освобождения контроля. Они разные; к сожалению, C ++ связывает их вместе.)
user541686

1
@ Mehrdad: слишком долго здесь. Если вы хотите, вы можете создать свои аргументы здесь: programmers.stackexchange.com/questions/304067/… . Спасибо.
Мартин Ба

21

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

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


11
Просто заметил ... бросок из дтора никогда не бывает неопределенным поведением. Конечно, это может вызвать terminate (), но это очень хорошо определенное поведение.
Мартин Ба

4
std::ofstreamдеструктор сбрасывает, а затем закрывает файл. Во время очистки может возникнуть ошибка переполнения диска, с которой вы можете сделать что-то полезное: показать пользователю диалоговое окно с сообщением об ошибке, говорящее о том, что на диске недостаточно свободного места.
Энди

13

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

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

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


Воскресив этот ответ, о первом примере, int foo()вы можете использовать функцию-try-block, чтобы обернуть всю функцию foo в блок try-catch, включая перехват деструкторов, если вы захотите это сделать. Все еще не предпочтительный подход, но это вещь.
tyree731

13

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

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

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


1
Не ответил на вопрос - ОП уже знает об этом.
Арафангион

2
@Arafangion Я сомневаюсь, что он знал об этом (вызывается std :: terminate), так как принятый ответ сделал абсолютно то же самое.
Лотар

@Arafangion, поскольку в некоторых ответах здесь некоторые люди упоминали, что вызывается abort (); Или это то, что std :: terminate по очереди вызывает функцию abort ().
Кришна Оза

7

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


7

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


6

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

Например:

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

Я ищу решение, но они пытаются объяснить, что случилось и почему. Просто хочу прояснить, вызывается ли функция close внутри деструктора?
Джейсон Лю

5

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

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

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


1
Не неопределенное поведение, а скорее немедленное прекращение.
Марк ван Леувен

Стандарт гласит «неопределенное поведение». Такое поведение часто прекращается, но не всегда.
DJClayworth

Нет, прочитайте [исключением. Завершить] в Обработка исключений-> Специальные функции (в моем экземпляре стандарта 15.5.1, но его нумерация, вероятно, устарела).
Марк ван Леувен

2

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

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

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

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

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

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

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

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


4
Твой любимый - это то, что я недавно попробовал, и, оказывается, тебе не следует этого делать. gotw.ca/gotw/047.htm
GManNickG

1

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

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

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


1

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

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

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


1
Запись сообщения в файл журнала может вызвать исключение.
Конард

1

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

Для выпуска:

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

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

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


1

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

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

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

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

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

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

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

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

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

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

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


1
Разрушение сейчас провал?
любопытный парень

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

0

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.