Большинство людей говорят, что никогда не выбрасывают исключение из деструктора - это приводит к неопределенному поведению. Страуструп подчеркивает, что «векторный деструктор явно вызывает деструктор для каждого элемента. Это означает, что, если деструктор элемента выбрасывает, векторное разрушение завершается неудачно ... На самом деле нет хорошего способа защиты от исключений, генерируемых деструкторами, поэтому библиотека не дает никаких гарантий, если деструктор элемента выбрасывает "(из Приложения E3.2) .
Эта статья, кажется, говорит об обратном - бросать деструкторы более или менее хорошо.
Итак, мой вопрос заключается в следующем: если выброс из деструктора приводит к неопределенному поведению, как вы обрабатываете ошибки, возникающие во время деструктора?
Если во время операции очистки возникает ошибка, вы просто игнорируете ее? Если это ошибка, которая потенциально может быть обработана в стеке, но не прямо в деструкторе, не имеет ли смысл выбрасывать исключение из деструктора?
Очевидно, что такого рода ошибки редки, но возможны.
источник
xyz()
и держите деструктор в чистоте от не-RAII логики.commit()
не вызван метод.Ответы:
Выбрасывать исключение из деструктора опасно.
Если другое исключение уже распространяется, приложение будет прекращено.
Это в основном сводится к:
Все опасное (то есть, что может вызвать исключение) должно быть сделано с помощью открытых методов (не обязательно напрямую). Пользователь вашего класса может потенциально справиться с этими ситуациями, используя открытые методы и перехватывая любые потенциальные исключения.
Затем деструктор завершит работу объекта, вызвав эти методы (если пользователь не сделал этого явно), но любые исключения throw будут перехвачены (после попытки решить проблему).
Таким образом, вы фактически перекладываете ответственность на пользователя. Если пользователь может исправить исключения, он вручную вызовет соответствующие функции и обработает все ошибки. Если пользователь объекта не беспокоится (так как объект будет уничтожен), то деструктор остается заниматься бизнесом.
Пример:
станд :: fstream
Метод close () потенциально может вызвать исключение. Деструктор вызывает close (), если файл был открыт, но следит за тем, чтобы любые исключения не распространялись из деструктора.
Поэтому, если пользователь файлового объекта хочет выполнить специальную обработку для проблем, связанных с закрытием файла, он будет вручную вызывать close () и обрабатывать любые исключения. Если, с другой стороны, им все равно, деструктор останется справиться с ситуацией.
Скотт Майерс имеет отличную статью на эту тему в своей книге «Эффективный C ++»
Редактировать:
По-видимому, также в «Более эффективный C ++»
пункт 11: Предотвращение исключения из деструкторов
источник
Выброс деструктора может привести к сбою, потому что этот деструктор может быть вызван как часть «разматывания стека». Разматывание стека - это процедура, которая имеет место при возникновении исключения. В этой процедуре все объекты, которые были помещены в стек с момента «try» и до тех пор, пока не было сгенерировано исключение, будут завершены -> будут вызваны их деструкторы. И во время этой процедуры другой выброс исключения не разрешен, потому что невозможно обработать два исключения одновременно, таким образом, это вызовет вызов abort (), программа потерпит крах, и элемент управления вернется в ОС.
источник
throw
но еще не нашелcatch
блок для него) в этом случаеstd::terminate
(неabort
) вызывается вместо вызова (нового) исключения (или продолжения разматывания стека).Здесь мы должны дифференцироваться, а не слепо следовать общим советам для конкретных случаев.
Обратите внимание, что следующее игнорирует проблему контейнеров объектов и того, что нужно делать перед лицом множества объектов внутри контейнеров. (И это может быть частично проигнорировано, поскольку некоторые объекты просто не подходят для размещения в контейнере.)
Вся проблема становится легче думать, когда мы разделяем классы на два типа. У класса dtor могут быть две разные обязанности:
Если мы рассмотрим вопрос таким образом, то я думаю, что можно утверждать, что семантика (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_exceptions
afaikt. Я быстро процитирую статью cppref:источник
finally
.finally
это dtor. Это всегда называется, несмотря ни на что. Для синтаксической аппроксимации finally см. Различные реализации scope_guard. В настоящее время, когда имеется механизм (даже в стандарте, это C ++ 14?), Чтобы определить, разрешено ли бросать dtor, его даже можно сделать абсолютно безопасным.finally
по сути является инструментом для (С). Если вы не понимаете, почему: подумайте, почему допустимо бросать исключения друг на друга вfinally
блоках, и почему это не относится к деструкторам. (В некотором смысле, это данные против контроля . Деструкторы предназначены для освобождения данных,finally
для освобождения контроля. Они разные; к сожалению, C ++ связывает их вместе.)Реальный вопрос, который нужно задать себе для броска из деструктора: «Что может сделать с этим вызывающий абонент?» Есть ли на самом деле что-нибудь полезное, что вы можете сделать, за исключением исключения, которое бы компенсировало опасности, создаваемые броском из деструктора?
Если я уничтожу
Foo
объект, аFoo
деструктор выбрасывает исключение, что я могу с ним разумно сделать? Я могу войти или я могу проигнорировать это. Вот и все. Я не могу это исправить, потому чтоFoo
объект уже исчез. В лучшем случае я регистрирую исключение и продолжаю, как будто ничего не произошло (или прекращаю работу программы). Действительно ли это стоит того, чтобы вызывать неопределенное поведение, выбрасывая из деструктора?источник
std::ofstream
деструктор сбрасывает, а затем закрывает файл. Во время очистки может возникнуть ошибка переполнения диска, с которой вы можете сделать что-то полезное: показать пользователю диалоговое окно с сообщением об ошибке, говорящее о том, что на диске недостаточно свободного места.Это опасно, но также не имеет смысла с точки зрения читабельности / понятности кода.
Что вы должны спросить в этой ситуации
Что должно поймать исключение? Стоит ли звонить из foo? Или Foo должен справиться с этим? Почему вызывающий объект foo должен заботиться о каком-то внутреннем объекте foo? Может быть, язык определяет это, чтобы иметь смысл, но он будет нечитаемым и трудным для понимания.
Что еще более важно, куда уходит память для Object? Куда уходит память, принадлежащая объекту? Это все еще распределено (якобы, потому что деструктор вышел из строя)? Учтите также, что объект находился в стековом пространстве , поэтому его, очевидно, не было.
Тогда рассмотрим этот случай
Когда удаление obj3 завершается неудачно, как мне на самом деле удалить таким образом, который гарантированно не потерпит неудачу? Это моя память, черт возьми!
Теперь рассмотрим в первом фрагменте кода Object автоматически удаляется, потому что он в стеке, а Object3 в куче. Так как указатель на Object3 исчез, вы вроде SOL. У вас утечка памяти.
Теперь один безопасный способ сделать следующее
Также смотрите этот FAQ
источник
int foo()
вы можете использовать функцию-try-block, чтобы обернуть всю функцию foo в блок try-catch, включая перехват деструкторов, если вы захотите это сделать. Все еще не предпочтительный подход, но это вещь.Из проекта ISO для C ++ (ISO / IEC JTC 1 / SC 22 N 4411)
Таким образом, деструкторы должны, как правило, перехватывать исключения и не позволять им распространяться за пределы деструктора.
источник
Ваш деструктор может выполняться внутри цепочки других деструкторов. Создание исключения, которое не было перехвачено вашим непосредственным абонентом, может оставить несколько объектов в несовместимом состоянии, что вызовет еще больше проблем, чем игнорирование ошибки в операции очистки.
источник
Я нахожусь в группе, которая считает, что бросок паттерна "ограниченная область действия" в деструкторе полезен во многих ситуациях - особенно для юнит-тестов. Однако следует помнить, что в C ++ 11 добавление деструктора приводит к вызову,
std::terminate
поскольку деструкторы неявно помечаютсяnoexcept
.Анджей Кшеменский имеет отличный пост на тему деструкторов, которые бросают:
Он указывает, что в C ++ 11 есть механизм для переопределения по умолчанию
noexcept
для деструкторов:Наконец, если вы решите добавить деструктор, вы всегда должны осознавать риск двойного исключения (выбрасывание, когда стек разворачивается из-за исключения). Это может вызвать вызов,
std::terminate
и это редко то, что вы хотите. Чтобы избежать такого поведения, вы можете просто проверить, существует ли уже исключение, прежде чем выдавать новое с помощьюstd::uncaught_exception()
.источник
Все остальные объяснили, почему бросать деструкторов ужасно ... что вы можете с этим поделать? Если вы выполняете операцию, которая может дать сбой, создайте отдельный открытый метод, который выполняет очистку и может выдавать произвольные исключения. В большинстве случаев пользователи будут игнорировать это. Если пользователи хотят отслеживать успех / неудачу очистки, они могут просто вызвать явную процедуру очистки.
Например:
источник
В качестве дополнения к основным ответам, которые являются хорошими, исчерпывающими и точными, я хотел бы прокомментировать статью, на которую вы ссылаетесь - ту, которая гласит: «бросать исключения в деструкторах не так уж плохо».
Статья принимает строку «каковы альтернативы выбрасыванию исключений» и перечисляет некоторые проблемы с каждой из альтернатив. Сделав это, он приходит к выводу, что, поскольку мы не можем найти беспроблемную альтернативу, мы должны продолжать генерировать исключения.
Беда в том, что ни одна из перечисленных проблем с альтернативами не так плоха, как поведение исключения, которое, давайте запомним, является «неопределенным поведением вашей программы». Некоторые из возражений автора включают «эстетически уродливый» и «поощряют плохой стиль». Что бы вы предпочли? Программа с плохим стилем или с неопределенным поведением?
источник
A: Есть несколько вариантов:
Позвольте исключениям вытекать из вашего деструктора, независимо от того, что происходит в другом месте. И при этом помните (или даже опасайтесь), что может последовать std :: terminate.
Никогда не позволяйте исключению вытекать из вашего деструктора. Может быть, напишите в журнал, какой-нибудь большой красный плохой текст, если можете.
Мой любимый : Если
std::uncaught_exception
вернет false, пусть появятся исключения. Если он возвращает true, вернитесь к подходу регистрации.Но хорошо ли бросать д'торы?
Я согласен с большинством из вышеперечисленного, что бросать лучше всего в деструкторе, где это возможно. Но иногда лучше всего признать, что это может случиться, и справиться с этим хорошо. Я бы выбрал 3 выше.
Есть несколько странных случаев, когда на самом деле отличная идея выбросить из деструктора. Как и код ошибки «должен проверить». Это тип значения, который возвращается из функции. Если вызывающая сторона читает / проверяет содержащийся код ошибки, возвращаемое значение уничтожается молча. Но если возвращенный код ошибки не был прочитан к тому времени, когда возвращаемые значения выходят из области видимости, он выдаст исключение из своего деструктора .
источник
В настоящее время я придерживаюсь политики (о которой так много говорят), что классы не должны активно генерировать исключения из своих деструкторов, а вместо этого должны предоставить публичный метод «close» для выполнения операции, которая может завершиться ошибкой ...
... но я верю, что деструкторы для классов контейнерного типа, такие как вектор, не должны маскировать исключения, выбрасываемые из классов, которые они содержат. В этом случае я фактически использую метод «free / close», который вызывает себя рекурсивно. Да, я сказал рекурсивно. В этом безумии есть метод. Распространение исключений зависит от наличия стека: если возникает единственное исключение, то и остальные деструкторы все равно будут работать, а ожидающее исключение будет распространяться, когда подпрограмма вернется, и это здорово. Если возникает несколько исключений, то (в зависимости от компилятора) либо это первое исключение будет распространяться, либо программа завершится, что нормально. Если возникает так много исключений, что рекурсия переполняет стек, тогда что-то серьезно не так, и кто-то узнает об этом, что тоже хорошо. Лично,
Дело в том, что контейнер остается нейтральным, и содержащиеся в нем классы должны решить, будут ли они вести себя или вести себя неправильно в отношении создания исключений из своих деструкторов.
источник
В отличие от конструкторов, где выдача исключений может быть полезным способом указать, что создание объекта завершилось успешно, исключения не должны создаваться в деструкторах.
Проблема возникает, когда из деструктора выдается исключение во время процесса раскрутки стека. Если это происходит, компилятор оказывается в ситуации, когда он не знает, продолжать ли процесс разматывания стека или обработать новое исключение. Конечным результатом является то, что ваша программа будет немедленно прекращена.
Следовательно, лучший способ действий - просто воздерживаться от использования исключений в деструкторах вообще. Напишите сообщение в лог-файл.
источник
Мартин Ба (выше) находится на правильном пути - вы по-разному разрабатываете логику RELEASE и COMMIT.
Для выпуска:
Вы должны есть любые ошибки. Вы освобождаете память, закрываете соединения и т. Д. Никто другой в системе не должен снова ВИДЕТЬ эти вещи, а вы возвращаете ресурсы ОС. Если кажется, что вам нужна настоящая обработка ошибок, это, вероятно, является следствием недостатков дизайна в вашей объектной модели.
Для фиксации:
Здесь вы хотите использовать те же объекты-обертки RAII, которые для мьютексов предоставляют такие вещи, как std :: lock_guard. С теми, кто не помещает логику коммита в dtor ВСЕ. У вас есть специальный API для него, а затем объекты-обертки, которые RAII передадут его в свои ИХД и обработают там ошибки. Помните, что вы можете легко ловить исключения в деструкторе; его выдача им это смертельно. Это также позволяет вам реализовать политику и другую обработку ошибок, просто создав другую обертку (например, std :: unique_lock и std :: lock_guard), и гарантирует, что вы не забудете вызвать логику фиксации, которая является единственным промежуточным этапом. достойное оправдание для того, чтобы поставить его в дтор на 1-м месте.
источник
Основная проблема заключается в следующем: вы не можете потерпеть неудачу . Что значит потерпеть неудачу, в конце концов? Если фиксация транзакции в базе данных не удалась и не удалась (не удалось выполнить откат), что происходит с целостностью наших данных?
Поскольку деструкторы вызываются как для нормальных, так и для исключительных (сбойных) путей, они сами не могут потерпеть неудачу, иначе мы «не сможем потерпеть неудачу».
Это концептуально сложная проблема, но часто решение состоит в том, чтобы просто найти способ убедиться, что неудача не может закончиться неудачей. Например, база данных может записать изменения до фиксации во внешней структуре данных или файле. Если транзакция не удалась, то структура файла / данных может быть отброшена. Все, что от него требуется, - убедиться, что фиксация изменений из этой внешней структуры / файла является атомарной транзакцией, которая не может завершиться неудачей.
Самое правильное решение для меня - написать логику неочищения таким образом, чтобы логика очистки не могла потерпеть неудачу. Например, если у вас возникает желание создать новую структуру данных, чтобы очистить существующую структуру данных, возможно, вы можете заранее попытаться создать эту вспомогательную структуру, чтобы нам больше не приходилось создавать ее внутри деструктора.
Это все гораздо легче сказать, чем сделать, по общему признанию, но это единственный действительно правильный способ, которым я вижу, чтобы сделать это. Иногда я думаю, что должна быть возможность написать отдельную логику деструктора для нормальных путей выполнения, отличных от исключительных, поскольку иногда деструкторы чувствуют, что у них двойная ответственность, пытаясь справиться с обоими (например, защитники области действия, которые требуют явного увольнения они не потребовали бы этого, если бы могли отличить исключительные пути разрушения от неисключительных).
Тем не менее, конечная проблема заключается в том, что мы не можем не потерпеть неудачу, и это сложная концептуальная проблема дизайна, которую необходимо решить во всех случаях. Это становится легче, если вы не слишком запутаетесь в сложных управляющих структурах с тоннами маленьких объектов, взаимодействующих друг с другом, и вместо этого смоделируете свои проекты немного громоздким способом (например: система частиц с деструктором, чтобы уничтожить всю частицу система, а не отдельный нетривиальный деструктор на частицу). Когда вы моделируете свои проекты на таком более грубом уровне, у вас есть меньше нетривиальных деструкторов, с которыми вы можете иметь дело, и вы также можете часто позволить себе любую дополнительную нагрузку на память / обработку, чтобы ваши деструкторы не могли выйти из строя.
И это одно из самых простых решений, естественно - реже использовать деструкторы. В приведенном выше примере с частицами, возможно, при уничтожении / удалении частицы, должны быть сделаны некоторые вещи, которые могут потерпеть неудачу по любой причине. В этом случае, вместо того, чтобы вызывать такую логику через dtor частицы, который мог бы выполняться по исключительному пути, вы могли бы вместо этого сделать все это системой частиц, когда она удаляет частицу. Удаление частицы всегда может быть сделано во время неисключительного пути. Если система разрушена, возможно, она может просто очистить все частицы и не беспокоиться об этой логике удаления отдельных частиц, которая может дать сбой, в то время как логика, которая может дать сбой, выполняется только во время нормального выполнения системы частиц, когда она удаляет одну или несколько частиц.
Часто есть решения, подобные тем, которые возникают, если вы избегаете иметь дело с множеством маленьких объектов с нетривиальными деструкторами. Где вы можете запутаться в беспорядке, где кажется почти невозможным быть безопасным от исключений, - это когда вы запутываетесь в множестве маленьких объектов, у которых все есть нетривиальные dtors.
Было бы очень полезно, если бы nothrow / noexcept фактически транслировался в ошибку компилятора, если бы что-либо, что его указывает (включая виртуальные функции, которые должны наследовать спецификацию noexcept своего базового класса), попыталось вызвать что-нибудь, что может выдать. Таким образом, мы сможем поймать все эти вещи во время компиляции, если мы на самом деле напишем деструктор, который может сработать.
источник
Установите событие тревоги. Обычно тревожные события являются лучшей формой уведомления о сбое при очистке объектов.
источник