Исключение исключений из деструктора

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

В этой статье, как представляется, говорится об обратном: бросание деструкторов более или менее хорошо.

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

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

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

Ответ 1

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

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

Ответ 2

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

Ответ 3

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

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

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

  • (R) освобождает семантику (также свободную от этой памяти)
  • (C) передать семантику (aka flush file to disk)

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

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

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


Что касается обработки ошибок (семантика Commit/Rollback) и исключений, есть хороший разговор от одного Andrei Alexandrescu: Обработка ошибок в потоке С++/декларативного управления (проведено на NDC 2014)

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

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

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

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

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

Примечания

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

Ответ 4

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

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

Ответ 5

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

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

int foo()
{
   Object o;
   // As foo exits, o 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 завершается с ошибкой, как я могу фактически удалить так, чтобы это не получилось? Его память проклята!

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

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

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

};

Также см. этот FAQ

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

Например:

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

Ответ 9

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

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

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

Ответ 10

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

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

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

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

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

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

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

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

Ответ 11

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

A: Существует несколько вариантов:

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

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

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

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

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

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

Ответ 12

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

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

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

Ответ 13

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

Для выпуска:

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

Для Commit:

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

Ответ 14

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

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

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

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

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

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

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

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

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

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

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

Ответ 15

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

Ответ 16

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

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

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