Большинство людей говорят, что никогда выбрасывает исключение из деструктора - это приводит к неопределенному поведению. Страуструп подчеркивает, что "векторный деструктор явно вызывает деструктор для каждого элемента. Это означает, что если деструктор элемента выбрасывает, уничтожение вектора не удается ... На самом деле нет хорошего способа защиты от исключений, генерируемых деструкторами, поэтому библиотека не дает никаких гарантий, если деструктор элемента выбрасывает "(из Приложения E3.2).
эта статья, кажется, говорит иначе - бросание деструкторов более или менее нормально.
Итак, мой вопрос таков: если выброс из деструктора приводит к неопределенному поведению, как вы обрабатываете ошибки, возникающие во время деструктора?
Если во время операции очистки возникает ошибка, вы ее просто игнорируете? Если это ошибка, которая потенциально может быть обработана в стеке, но не прямо в деструкторе, не имеет ли смысла выдавать исключение из деструктора?
Очевидно, что такие ошибки редки, но возможны.
@spraff Значит, любая функция с пустым постусловием может перехватывать и отбрасывать все исключения?
Он должен перехватить и обрабатывать все исключения (сброс может быть приемлемым) ИЛИ он должен переписать свое постусловие, чтобы сказать: «если вход не удовлетворяет X, выход Y / undefined»
Если деструктор может выйти из строя, ваш дизайн нарушен: все, что необходимо для успешного разрушения, должно быть установлено конструктором.
@spraff: Вы знаете, что то, что вы сказали, подразумевает «выбросить RAII»?
На самом деле, как раз наоборот.
@spraff: необходимость вызова «отдельного метода до того, как объект выйдет из области видимости» (как вы написали), фактически выбрасывает RAII! Код, использующий такие объекты, должен будет гарантировать, что такой метод будет вызван до вызова деструктора. Наконец, эта идея совершенно не помогает.
@Frunsi нет, потому что эта проблема связана с тем, что деструктор пытается сделать что-то помимо простого высвобождения ресурсов. Заманчиво сказать: «Я всегда хочу закончить работу с XYZ» и подумать, что это аргумент в пользу помещения такой логики в деструктор. Нет, не поленитесь, напишите xyz() и очистите деструктор от логики, отличной от RAII.
> Если во время операции очистки возникает ошибка, вы ее просто игнорируете? Это верный вопрос. Упоминание «деструктора» вызывает только бесполезные автоматические ответы, такие как «деструктор не должен генерировать период». Единственно возможный ответ на такие общие вопросы - «как бывает».
@Frunsi Например, фиксация чего-либо в файле не обязательно ОК, чтобы сделать это в деструкторе класса, представляющего транзакцию. Если фиксация завершилась неудачно, уже слишком поздно обрабатывать ее, когда весь код, который был задействован в транзакции, вышел за рамки. Деструктор должен отменить транзакцию, если не вызывается метод commit().
Эта статья является более поздним продолжением обсуждения этой проблемы. cpp-next.com/archive/2012/08/evil-or-just-misunderstanding
Вы можете вызвать исключение в деструкторе, но это исключение не должно покидать деструктор; если деструктор завершается с помощью броска, вероятно, произойдут всевозможные неприятности, потому что будут нарушены основные правила стандартной библиотеки и самого языка. Не делай этого.
@Jnana Этот «аргумент» явно абсурден. Не каждый объект попадает в контейнер.
@spraff "Нет, не поленитесь, напишите xyz () и очистите деструктор от логики, отличной от RAII." в блоке catch?





Выбрасывать исключение из деструктора опасно. Если другое исключение уже распространяется, приложение будет остановлено.
#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: не позволяйте исключениям оставлять деструкторы
«Если вы не возражаете против возможного завершения работы приложения, вам, вероятно, следует проглотить ошибку». - это, вероятно, должно быть исключением (простите за каламбур), а не правилом - то есть быстро терпеть неудачу.
Я не согласен. Завершение программы останавливает раскрутку стека. Деструктор больше не будет вызываться. Все открытые ресурсы останутся открытыми. Я думаю, что предпочтительным вариантом было бы проглотить исключение.
Когда приложение выходит из строя, ОС должна обрабатывать любые оставшиеся ресурсы.
Операционная система может очищать ресурсы, принадлежащие ее владельцу. Память, FileHandles и т. д. Что насчет сложных ресурсов: подключения к БД. Этот восходящий канал к ISS, который вы открыли (будет ли он автоматически отправлять закрытые соединения)? Я уверен, что НАСА захочет, чтобы вы полностью закрыли соединение!
Если приложение собирается «быстро дать сбой» из-за прерывания, оно не должно в первую очередь генерировать исключения. Если он собирается потерпеть неудачу при передаче управления обратно в стек, он не должен делать это таким образом, чтобы программа могла быть прервана. Одно или другое, не выбирайте оба.
@Martin: если приложение закрывает соединения с БД в деструкторе, возможно, оно неправильно спроектировано.
@Eclipse "Операционная система должна обрабатывать любые оставшиеся ресурсы." некоторые ресурсы разделяются между процессами: Sys V IPC; POSIX mutex / sema ... тоже могут использоваться совместно. Или просто временные файлы. Существует множество ресурсов, которые операционная система не может безопасно очистить. Возможно, вы захотите иметь процесс-наблюдатель, который обнаруживает завершение вашего процесса и занимается этим.
@ dog44wgm: исходный плакат ссылался на книгу Скотта Майерса «Эффективный C++». «Более эффективный C++» - это еще одна книга, и хотя она может говорить об одном и том же предмете, формулировка может быть совершенно иной.
@LokiAstari Транспортный протокол, который вы используете для связи с космическим кораблем, не может обработать разорванное соединение? В порядке...
@ doug65536: Ты хочешь сделать такое предположение (с твоим кораблем стоимостью 100 миллиардов долларов на пути к Марсу)?
@Deduplicator: Что это добавляет к текущему обсуждению.
@LokiAstari: Просто, если я правильно читаю стандарты C++ 11 и C++ 14, dtor будет иметь спецификацию noexcept(true) по умолчанию, что означает, что он не должен генерировать исключение ни при каких обстоятельствах. Поправьте меня если я ошибаюсь.
Я работаю над встраиваемыми системами. Я никогда не хочу, чтобы деструктор генерировал исключение ... весь бит о том, что "это задача ОС, которая должна обрабатывать очистку любых оставшихся ресурсов", НЕ МОЖЕТ полагаться в этом контексте. Я бы оставил все в плохом состоянии.
@mangguo: Значит, у вас еще хуже с C++ 11. По умолчанию деструктор помечен как noexcept, что означает, что если программа выбрасывает и исключение из деструктора, приложение завершается без раскрутки стека и без вызова других деструкторов (вы даже не можете ожидать, что деструкторы между точкой выброса и деструктором будут называться либо).
Но на самом деле не отключайте исключения при выполнении встроенной работы (прошло уже два десятилетия с тех пор, как я что-то сделал, и тогда мы не использовали исключения для встроенных вещей). В противном случае вы должны явно добавить блок try / catch ко всем своим деструкторам и отбросить все исключения.
@LokiAstari Я думал, что прекращение вызова было само собой разумеющимся;) Кроме того, мы находимся в процессе рефакторинга нашего использования исключений ... проблема, с которой я сейчас сталкиваюсь, заключается в том, что бывший коллега блестяще добавил что-то, что может вызвать исключение в деструктор без попытки / улова. Так что да, я согласен с вами. С помощью какой-то черной магии мы раньше не сталкивались с проблемами.
@mangguo: Было бы еще лучше объявить деструктор, который отбрасывает исключения. ~myClass() noexcept(true | false | discard) :-) Примечание. В C++ 03 выдача исключения из деструктора не является проблемой и не вызывает вызова std::terminate() (если только исключение уже не распространяется).
@LokiAstari, это было бы неплохо ... но я не хочу оставлять оборудование в плохом состоянии. Или, может быть, так и сделаю, тогда я уйду с работы и оставлю какую-нибудь беднягу с кошмарной ситуацией, которую нужно отлаживать. Муахаха
Небольшое примечание ... Поскольку деструкторы C++ 11 по умолчанию не имеют значения noexcept, поэтому для того, чтобы этот пример работал должным образом, нам нужно использовать ~Bad() noexcept(false) { throw 1; }.
@LokiAstari - это не остановка выполнения, сначала бросок в деструктор, а затем выход из блока try. почему идет во второй строке "бросить 2"?
@LokiAstari - это не остановка выполнения, сначала бросок в деструктор, а затем выход из блока try. почему идет во второй строке "бросить 2"?
@EmptyData: вы правы для публикации C++ 11 (действие деструктора по умолчанию - завершить бросок, поскольку деструкторы по умолчанию noexcept). Этот ответ был написан, когда C++ 03 был стандартом. Я обновлю, чтобы учесть изменение языка.
@EmptyData Теперь должно быть точно.
Цитата из стандарта C++ (Рабочий проект, 15.2): «(...) 3. Процесс вызова деструкторов для автоматических объектов, созданных на пути от блока try к выражению throw, называется« раскручиванием стека ». Если деструктор, вызываемый во время раскрутки стека, завершается с исключением, вызывается std :: terminate (15.5.1). [Примечание: деструкторы обычно должны перехватывать исключения и не позволять им распространяться за пределы деструктора. - конец примечания "]"
@ Sonic78 Я не могу найти вашу цитату в стандарте. Укажите точный ссылочный номер раздела и номер параграфа. Текущий стандарт здесь n4659 Ближайшее, что я смог найти, было: when the destruction of an object during stack unwinding (18.2) terminates by throwing an exception, в Раздел 18.5.1 параграф 1 пункт (1.4). Но это примечание. Обратите внимание, что примечания не являются нормативными и предназначены для пояснения. В случае по умолчанию ваша цитата сохраняется (поскольку деструктор по умолчанию noexcept(true)
Но деструктор может быть явно помечен как noexcept(false), что приводит к поведению C++ 03, если исключения, экранирующие деструктор, не вызывают terminate(). Вы также заметите, что я добавил подробные комментарии в приведенный выше пример, чтобы указать на это.
@Loki Astari: Извините, похоже, я использовал старый рабочий черновик и забыл добавить номер документа. Не существует еще в рабочем проекте под номером N4296 на странице 417 (Раздел 15.2, абзац 1). И да, это всего лишь примечание, но ИМХО хороший совет. (Как и ваше первое предложение «Выбрасывать исключение из деструктора опасно.»: Поэтому я поставил +1.) Тем временем я нашел хорошую запись в «блоге Анджея C++» (Деструкторы, которые выбрасывают) [akrzemi1.wordpress.com/2011/09/21/destructors-that-th row /].
Полностью согласен, что деструкторы не должны бросать (когда они это делают, мы должны завершать работу), это лучший совет. Но это не требование языка.
Я думаю, что люди недостаточно рассматривают возможность передачи классами ошибок от разрушения к функции обратного вызова в качестве потенциальной альтернативы ручному вызову close().
@MartinYork Я думаю, что здесь есть урок, что RAII, изначально предназначенный для управления локальными ресурсами, к сожалению, не совсем так идеален для многих других типов очистки, для которых он используется (хотя он по-прежнему превосходит ручную очистку во многих способов).
«... что-нибудь опасное должно происходить в ...» ----------- Не правда ли, что каждая чертова строчка кода может вызвать исключение? Даже простое добавление может сделать это (Целочисленное переполнение).
@ Migrate2Lazarusseemyprofile Целочисленное переполнение не является исключением "C++". ЧТОБЫ получить исключение C++, некоторый код должен явно выполнить оператор throw. Таким образом, довольно легко понять, где и когда потенциально могут произойти исключения. 1) Оператор Throw 2) Все, что может содержать оператор throw => Функция / метод без маркировки noexcept
@MartinYork - Блин, я забыл, что этой функции (docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/…) нет в C++. Извиняюсь.
@ Migrate2Lazarusseemyprofile Это не имеет ничего общего с C++. Нигде в стандарте вы не найдете ничего подобного. Я предполагаю, что вы путаете эту документацию со стандартом C++. Это было бы прямым нарушением стандарта C++. Если вы собираетесь что-то процитировать, используйте стандарт. Здесь: stackoverflow.com/a/4653479/14065 Ссылка на текущую версию - n849
@MartinYork - это то, что я сказал в предыдущем комментарии: эта функция (вызывающая исключение EIntOverflow при нарушении диапазона) не существует в C++.
@ Migrate2Lazarusseemyprofile Зачем вообще упоминать об этом. Почему бы вам просто не удалить ошибочный комментарий.
@MartinYork - Слишком поздно. Информация "просочилась" :). Сообщите разработчикам C, что другие языки могут обрабатывать такие исключения / ошибки. Может быть, позже попросят реализовать его на C / ++.
Ваш деструктор может выполняться внутри цепочки других деструкторов. Создание исключения, которое не было обнаружено вашим непосредственным вызывающим пользователем, может привести к тому, что несколько объектов окажутся в несогласованном состоянии, что вызовет еще больше проблем, чем игнорирование ошибки в операции очистки.
Настоящий вопрос, который следует задать себе о выбросе из деструктора: «Что вызывающий может с этим делать?» Есть ли что-нибудь полезное, что вы можете сделать с исключением, что компенсировало бы опасности, создаваемые выбросом из деструктора?
Если я уничтожу объект Foo, а деструктор Foo выбросит исключение, что я могу с ним сделать? Я могу это записать или проигнорировать. Это все. Я не могу это «исправить», потому что объекта Foo уже нет. В лучшем случае я регистрирую исключение и продолжаю, как будто ничего не произошло (или завершаю программу). Стоит ли потенциально вызывать неопределенное поведение, выбрасывая его из деструктора?
Только что заметил ... бросание с дтора - это никогда Undefined Behavior. Конечно, он может вызвать terminate (), но это очень хорошо определенное поведение.
Деструктор std::ofstream сбрасывает, а затем закрывает файл. Во время промывки может произойти ошибка переполнения диска, с которой вы можете сделать что-то полезное: показать пользователю диалоговое окно с сообщением об ошибке, в котором говорится, что на диске нет свободного места.
Это опасно, но также не имеет смысла с точки зрения читабельности / понятности кода.
Вы должны спросить в этой ситуации
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 не удается, как мне на самом деле удалить, гарантированно не потерпев неудачу? Черт возьми, это моя память!
Теперь рассмотрим, что в первом фрагменте кода объект автоматически удаляется, потому что он находится в стеке, а Object3 находится в куче. Поскольку указатель на Object3 исчез, вы вроде SOL. У вас утечка памяти.
Вот один из безопасных способов сделать что-то:
class Socket
{
virtual ~Socket()
{
try
{
Close();
}
catch (...)
{
// Why did close fail? make sure it *really* does close here
}
}
};
Также смотрите этот Часто задаваемые вопросы
Воскрешая этот ответ, re: первый пример о int foo(), вы можете использовать блок function-try-block, чтобы обернуть всю функцию foo в блок try-catch, включая перехват деструкторов, если вы захотите это сделать. По-прежнему не самый предпочтительный подход, но это действительно так.
Выброс деструктора может привести к сбою, потому что этот деструктор может быть вызван как часть «раскрутки стека». Раскрутка стека - это процедура, которая выполняется при возникновении исключения. В этой процедуре все объекты, которые были помещены в стек с момента «попытки» и до тех пор, пока не было сгенерировано исключение, будут завершены -> будут вызваны их деструкторы. И во время этой процедуры другой выброс исключения не разрешен, потому что невозможно обработать два исключения одновременно, таким образом, это вызовет вызов abort (), программа выйдет из строя, и управление вернется в ОС.
не могли бы вы уточнить, как был вызван abort () в вышеупомянутой ситуации. Означает, что контроль за выполнением по-прежнему оставался компилятором C++.
@Krishna_Oza: Довольно просто: всякий раз, когда возникает ошибка, код, который вызывает ошибку, проверяет некоторый бит, который указывает, что система времени выполнения находится в процессе раскрутки стека (т.е. обрабатывает какой-то другой throw, но не нашла для него блок catch пока), и в этом случае вызывается std::terminate (не abort) вместо того, чтобы вызывать (новое) исключение (или продолжать раскручивание стека).
Все остальные объяснили, почему бросать деструкторы ужасно ... что вы можете с этим поделать? Если вы выполняете операцию, которая может завершиться ошибкой, создайте отдельный общедоступный метод, который выполняет очистку и может генерировать произвольные исключения. В большинстве случаев пользователи проигнорируют это. Если пользователи хотят отслеживать успешность / неудачу очистки, они могут просто вызвать явную процедуру очистки.
Например:
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 внутри деструктора?
В дополнение к основным ответам, которые являются хорошими, исчерпывающими и точными, я хотел бы прокомментировать статью, на которую вы ссылаетесь - ту, в которой говорится, что «бросать исключения в деструкторах не так уж и плохо».
В статье используется строка «каковы альтернативы выдаче исключений» и перечисляются некоторые проблемы с каждой из альтернатив. Сделав это, он заключает, что, поскольку мы не можем найти беспроблемную альтернативу, мы должны продолжать генерировать исключения.
Проблема в том, что ни одна из проблем, которые он перечисляет с альтернативами, даже близко не так плоха, как поведение исключения, которое, давайте помнить, является «неопределенным поведением вашей программы». Некоторые из возражений автора включают «эстетически некрасиво» и «поощрять плохой стиль». Что бы вы предпочли? Программа с плохим стилем или с неопределенным поведением?
Не неопределенное поведение, а немедленное прекращение.
В стандарте указано «неопределенное поведение». Такое поведение часто приводит к прекращению действия, но не всегда.
Нет, прочтите [except.terminate] в разделе Обработка исключений-> Специальные функции (в моей копии стандарта это 15.5.1, но его нумерация, вероятно, устарела).
Из проекта ISO для C++ (ISO / IEC JTC 1 / SC 22 N 4411)
Таким образом, деструкторы обычно должны перехватывать исключения и не позволять им распространяться за пределы деструктора.
3 The process of calling destructors for automatic objects constructed on the path from a try block to a throw- expression is called “stack unwinding.” [ Note: If a destructor called during stack unwinding exits with an exception, std::terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. — end note ]
Не ответил на вопрос - ОП уже в курсе.
@Arafangion Я сомневаюсь, что он знал об этом (вызов std :: terminate), поскольку принятый ответ содержал точно то же самое.
@Arafangion, поскольку в некоторых ответах здесь некоторые люди упоминали, что вызывается abort (); Или std :: terminate по очереди вызывает функцию abort ().
В настоящее время я следую политике (которую так многие говорят), что классы не должны активно генерировать исключения из своих деструкторов, а вместо этого должны предоставлять общедоступный метод «закрытия» для выполнения операции, которая может потерпеть неудачу ...
... но я верю, что деструкторы для классов контейнерного типа, такие как вектор, не должны маскировать исключения, возникающие из классов, которые они содержат. В этом случае я фактически использую метод «free / close», который вызывает себя рекурсивно. Да, сказал я рекурсивно. У этого безумия есть свой метод. Распространение исключения зависит от наличия стека: если возникает единственное исключение, то оба оставшихся деструктора все равно будут работать, а ожидающее исключение будет распространяться после возврата из подпрограммы, что прекрасно. Если возникает несколько исключений, то (в зависимости от компилятора) либо это первое исключение будет распространяться, либо программа завершится, что нормально. Если происходит так много исключений, что рекурсия переполняет стек, значит, что-то серьезно не так, и кто-то собирается об этом узнать, что тоже нормально. Лично я ошибаюсь в том, что ошибки раздуваются, а не являются скрытыми, секретными и коварными.
Дело в том, что контейнер остается нейтральным, и это зависит от содержащихся в нем классов, чтобы решить, будут ли они вести себя или неправильно вести себя в отношении выдачи исключений из своих деструкторов.
Q: So my question is this - if throwing from a destructor results in undefined behavior, how do you handle errors that occur during a destructor?
О: Есть несколько вариантов:
Позвольте исключениям вытекать из вашего деструктора, независимо от того, что происходит в другом месте. И при этом имейте в виду (или даже опасайтесь), что за ним может последовать std :: terminate.
Никогда не позволяйте исключениям вытекать из вашего деструктора. Можно записать в журнал какой-нибудь большой красный плохой текст, если можно.
моя любимая: Если std::uncaught_exception возвращает false, вы можете исключить утечку. Если он вернет true, то вернитесь к подходу регистрации.
Но хорошо ли бросать д'торс?
Я согласен с большинством из вышеперечисленных, что бросания лучше избегать в деструкторе, где это возможно. Но иногда лучше смириться с тем, что это может случиться, и хорошо с этим справиться. Я бы выбрал 3 выше.
Есть несколько странных случаев, когда на самом деле деструктор выбрасывает отличная идея. Как и код ошибки "необходимо проверить". Это тип значения, возвращаемый функцией. Если вызывающий объект читает / проверяет содержащийся код ошибки, возвращаемое значение уничтожается без уведомления. Но, если возвращенный код ошибки не был прочитан к тому моменту, когда возвращаемые значения выходят за пределы области видимости, он вызовет какое-то исключение, из его деструктора.
Я недавно попробовал ваш любимый вариант, и оказалось, что вам стоит это сделать нет. gotw.ca/gotw/047.htm
Здесь мы должны дифференцировать вместо того, чтобы слепо следовать совету Общее для случаев специфический.
Обратите внимание, что следующий игнорирует - проблема контейнеров объектов и что делать при наличии нескольких объектов внутри контейнеров. (И это можно частично игнорировать, поскольку некоторые объекты просто не подходят для размещения в контейнере.)
Вся проблема становится легче думать, когда мы разделяем классы на два типа. Класс dtor может иметь две разные обязанности:
Если мы рассмотрим вопрос таким образом, то я думаю, что можно утверждать, что семантика (R) никогда не должна вызывать исключение из dtor, поскольку нет: а) мы ничего не можем с этим поделать и б) многие операции с свободными ресурсами не вызывают даже предусмотреть проверку ошибок, например voidfree(void* p);.
Объекты с семантикой (C), такие как файловый объект, которому необходимо успешно очистить свои данные, или соединение с базой данных («защищенная область действия»), которое выполняет фиксацию в dtor, относятся к другому типу: мы может что-то делаем с ошибкой (на уровень приложения), и нам действительно не следует продолжать, как будто ничего не произошло.
Если мы будем следовать маршруту RAII и разрешить объекты, которые имеют семантику (C) в своих d'tors, я думаю, тогда мы также должны учесть нечетный случай, когда такие d'tors могут бросать. Отсюда следует, что вы не должны помещать такие объекты в контейнеры, а также следует, что программа все еще может terminate(), если commit-dtor выдает, пока активно другое исключение.
Что касается обработки ошибок (семантика фиксации / отката) и исключений, есть хороший разговор с одним Андрей Александреску: Обработка ошибок в C++ / декларативный поток управления (удерживаемым в НДЦ 2014)
В деталях он объясняет, как библиотека Folly реализует UncaughtExceptionCounter для своего инструментария ScopeGuard.
(Должен заметить, что у другие тоже были похожие идеи.)
Хотя разговор не фокусируется на метании от д'тора, он показывает инструмент, который можно использовать сегодня, чтобы избавиться от проблемы с тем, когда бросать от д'тора.
В будущеемай будет стандартной функцией для этого, см. N3614, и обсуждение этого.
Upd '17: стандартная функция C++ 17 для этого - std::uncaught_exceptions afaikt. Я быстро процитирую статью cppref:
Notes
An example where
int-returninguncaught_exceptionsis used is ... ... first creates a guard object and records the number of uncaught exceptions in its constructor. The output is performed by the guard object's destructor unless foo() throws (in which case the number of uncaught exceptions in the destructor is greater than what the constructor observed)
Полностью согласен. И добавляем еще одну семантическую (Ro) семантику отката. Обычно используется для защиты прицела. Как и в случае с моим проектом, где я определил макрос ON_SCOPE_EXIT. Дело в том, что семантика отката заключается в том, что здесь может произойти что угодно значимое. Так что нам действительно не следует игнорировать неудачу.
Мне кажется, что единственная причина, по которой у нас есть семантика фиксации в деструкторах, заключается в том, что C++ не поддерживает finally.
@Mehrdad: finallyявляется a dtor. Он всегда называется, несмотря ни на что. Для синтаксического приближения finally см. Различные реализации scope_guard. В настоящее время, когда имеется оборудование (даже в стандарте, это C++ 14?) Для определения того, разрешено ли запускать dtor, его можно даже сделать полностью безопасным.
@MartinBa: Я думаю, вы упустили суть моего комментария, что удивительно, поскольку я был согласен с вашим представлением о том, что (R) и (C) разные. Я пытался сказать, что dtor по своей сути является инструментом для (R), а finally по своей сути является инструментом для (C). Если вы не понимаете, почему: подумайте, почему допустимо создавать исключения друг над другом в блоках finally и почему то же самое нет для деструкторов. (В некотором смысле это вещь данные против контроля. Деструкторы предназначены для освобождения данных, finally - для освобождения управления. Они разные; к сожалению, C++ связывает их вместе.)
@MartinBa: Одной RAII-подобной альтернативой try / finally была бы поддержка C++ не только деструкторов, но и финализаторы. Тогда финализаторам будет вполне разрешено генерировать исключения поверх друг друга, потому что они не будут влиять на время жизни объектов (они управляются деструкторами, которые будут запускаться впоследствии, независимо от того, преуспели ли финализаторы, поскольку они не могут потерпеть неудачу ). Тот факт, что C++ рассматривает финализацию как уничтожение (или, если вы предпочитаете называть это, фиксация как релиз), является источником проблемы.
@Mehrdad - спасибо за уточнение. Я думаю, что недоразумение (если оно таковое) происходит из-за того, что вы, кажется, предполагаете, что запрет или же, позволяющий «накладывать исключения друг на друга», имеет четко определенное техническое обоснование. В то время как я предлагаю OTOH, что это не так. Есть аргументы в пользу того и другого, и все C++, C# и Java имеют слабые места в этой области. terminate в C++ - это я, но молча проглотить первое исключение (Java, не так ли?) - тоже нормально.
@MartinBa: Под этим я имел в виду выброс исключений, когда они уже распространяются. Почему бы не иметь технического обоснования? Каждый язык допускает это вне деструкторов (даже C++ позволяет генерировать исключение в блоке catch); это прекрасно и четко определено. Единственная проблема здесь - деструкторы; причина того, что C++ не допускает этого, заключается в том, что он мешает высвобождению ресурсов, что должно происходить даже в случае сбоя завершения. Я утверждаю, что выпуск и финализация должны обрабатываться отдельно, чтобы это больше не было проблемой.
@ Mehrdad: Слишком долго здесь. Если хотите, можете привести аргументы здесь: programmers.stackexchange.com/questions/304067/…. Спасибо.
@MartinBa, так что вы думаете о передаче ошибок во время разрушения функции обратного вызова? Похоже, что программисты на C++ редко вспоминают, что функции обратного вызова являются потенциальным решением всех видов проблем.
@MartinBa также благодарит за то, что четко сформулировал различие между семантикой выпуска и фиксации, чтобы остановить волну «но как вы могли бы даже обрабатывать любые ошибки во время уничтожения?» введите ответы.
@Mehrdad Бросок от ctor влияет на время жизни объекта, но не от dtor. Если нормально выполнить ошибку в блоке catch или, наконец, заблокировать, я не понимаю, почему это изначально плохо в dtor. Проблемы почти такие же.
@curiousguy: одно из обещаний C++ заключается в том, что автоматические переменные уничтожаются, когда они выходят за пределы области видимости. Если вы выбрасываете из конструктора, проблем нет, поскольку объект еще не построен, но его поля созданы, поэтому их можно уничтожить. Однако, если вы выбрасываете из деструктора, объект не будет уничтожен - то есть он все еще существует - в то время как его поля должны быть уничтожены. Это означает, что вы получите объект, который существует после того, как его компоненты будут уничтожены, что не имеет смысла, и в противном случае AFAIK не встречается на языке.
@Mehrdad "Однако если вы выберете из деструктора, объект не будет уничтожен" Почему это так?
@curiousguy: потому что деструктор не завершил бы свою работу, которая уничтожает объект?
@Mehrdad Если dtor не заботится о выполнении полной работы, это означает, что он не является потокобезопасным и не должен вызывать какую-либо функцию, которая может вызвать исключение, как и любая функция с побочными эффектами, которые нельзя оставить полдела сделано. Перераспределение вектора имеет такого рода проблемы: вы не можете оставить новый диапазон наполовину созданным, если один конструктор копирования не работает.
@curiousguy: Я не понимаю, какое отношение эти предложения имеют к вашим недоразумениям, которые вы пытались прояснить со мной в предыдущем обсуждении ... Я не предлагал вам написать плохой деструктор; Я пытался объяснить, почему семантически не имеет смысла разрешать выброс из деструктора в языке.
Позвольте нам продолжить обсуждение в чате.
Установите тревожное событие. Обычно тревожные события - лучшая форма уведомления о сбое при очистке объектов.
В отличие от конструкторов, в которых создание исключений может быть полезным способом указать, что создание объекта выполнено успешно, исключения не должны создаваться в деструкторах.
Проблема возникает, когда деструктор генерирует исключение во время процесса раскрутки стека. Если это произойдет, компилятор окажется в ситуации, когда он не знает, продолжать ли процесс раскрутки стека или обработать новое исключение. Конечным результатом является то, что ваша программа будет немедленно прекращена.
Следовательно, лучший способ действий - просто вообще воздерживаться от использования исключений в деструкторах. Вместо этого напишите сообщение в файл журнала.
Запись сообщения в файл журнала может вызвать исключение.
Мартин Ба (вверху) находится на правильном пути - вы по-разному проектируете логику RELEASE и COMMIT.
Тебе надо есть какие-то ошибки. Вы освобождаете память, закрываете соединения и т. д. Больше никто в системе не должен ВИДЕТЬ эти вещи снова, а вы возвращаете ресурсы ОС. Если похоже, что здесь вам нужна реальная обработка ошибок, это, вероятно, является следствием конструктивных недостатков вашей объектной модели.
Здесь вам нужны такие же объекты-оболочки RAII, которые такие вещи, как std :: lock_guard, предоставляют для мьютексов. С ними вы ВООБЩЕ не помещаете логику фиксации в dtor. У вас есть специальный API для этого, а затем объекты-оболочки, которые будут передавать его в СВОИх dtors и обрабатывать там ошибки. Помните, что вы можете просто ЛОВИТЬ исключения в деструкторе; их выдача смертельно опасна. Это также позволяет вам реализовать политику и различную обработку ошибок, просто создав другую оболочку (например, std :: unique_lock против std :: lock_guard), и гарантирует, что вы не забудете вызвать логику фиксации, которая является единственной половиной пути достойное обоснование для того, чтобы поставить его в дтор на 1 место.
Я нахожусь в группе, которая считает, что добавление паттерна «scoped guard» в деструктор полезно во многих ситуациях - особенно для модульных тестов. Однако имейте в виду, что в C++ 11 добавление деструктора приводит к вызову std::terminate, поскольку деструкторы неявно аннотируются с помощью noexcept.
Анджей Кшеменски написал отличный пост на тему деструкторов, которые бросают:
Он указывает, что в C++ 11 есть механизм для переопределения noexcept по умолчанию для деструкторов:
In C++11, a destructor is implicitly specified as
noexcept. Even if you add no specification and define your destructor like this:class MyType { public: ~MyType() { throw Exception(); } // ... };The compiler will still invisibly add specification
noexceptto your destructor. And this means that the moment your destructor throws an exception,std::terminatewill be called, even if there was no double-exception situation. If you are really determined to allow your destructors to throw, you will have to specify this explicitly; you have three options:
- Explicitly specify your destructor as
noexcept(false),- Inherit your class from another one that already specifies its destructor as
noexcept(false).- Put a non-static data member in your class that already specifies its destructor as
noexcept(false).
Наконец, если вы решите добавить деструктор, вы всегда должны помнить о риске двойного исключения (выброса во время раскрутки стека из-за исключения). Это вызовет вызов std::terminate, и это редко то, что вам нужно. Чтобы избежать такого поведения, вы можете просто проверить, существует ли уже исключение, прежде чем генерировать новое с помощью std::uncaught_exception().
So my question is this - if throwing from a destructor results in undefined behavior, how do you handle errors that occur during a destructor?
Основная проблема вот в чем: нельзя не потерпеть неудачу. В конце концов, что значит потерпеть неудачу? Если фиксация транзакции в базе данных не удалась, и она не завершилась неудачей (не удалось выполнить откат), что произойдет с целостностью наших данных?
Поскольку деструкторы вызываются как для нормального, так и для исключительного (сбойного) пути, они сами не могут дать сбой, иначе мы «не сработаем».
Это концептуально сложная проблема, но часто решение состоит в том, чтобы просто найти способ убедиться, что неудача не приведет к провалу. Например, база данных может записывать изменения до фиксации во внешней структуре данных или файле. Если транзакция не удалась, то файл / структуру данных можно выбросить. Все, что он должен затем гарантировать, это то, что фиксация изменений из этой внешней структуры / файла является атомарной транзакцией, которая не может потерпеть неудачу.
The pragmatic solution is perhaps just make sure that the chances of failing on failure are astronomically improbable, since making things impossible to fail to fail can be almost impossible in some cases.
Для меня наиболее подходящее решение - написать вашу логику, не связанную с очисткой, таким образом, чтобы логика очистки не могла дать сбой. Например, если вам хочется создать новую структуру данных, чтобы очистить существующую структуру данных, то, возможно, вы можете попытаться создать эту вспомогательную структуру заранее, чтобы нам больше не приходилось создавать ее внутри деструктора.
По общему признанию, все это гораздо легче сказать, чем сделать, но это единственный действительно правильный способ, как я вижу, как это сделать. Иногда я думаю, что должна быть возможность написать отдельную логику деструктора для нормальных путей выполнения, отличных от исключительных, поскольку иногда деструкторы чувствуют себя немного так, как будто они несут двойную ответственность, пытаясь обрабатывать оба (пример - охранники области видимости, которые требуют явного отклонения ; им бы это не потребовалось, если бы они могли отличать исключительные пути разрушения от неисключительных).
Тем не менее, основная проблема заключается в том, что мы не можем не потерпеть неудачу, и это сложная проблема концептуального дизайна, которую необходимо идеально решить во всех случаях. Это действительно станет проще, если вы не слишком увлечетесь сложными управляющими структурами с тоннами крошечных объектов, взаимодействующих друг с другом, а вместо этого смоделируете свои проекты немного более громоздким способом (пример: система частиц с деструктором для уничтожения всей частицы система, а не отдельный нетривиальный деструктор на частицу). Когда вы моделируете свои проекты на таком более грубом уровне, у вас есть меньше нетривиальных деструкторов, с которыми нужно иметь дело, а также часто можно позволить себе любые накладные расходы на память / обработку, необходимые для обеспечения того, чтобы ваши деструкторы не вышли из строя.
И это одно из самых простых решений, естественно, - реже использовать деструкторы. В приведенном выше примере частицы, возможно, после уничтожения / удаления частицы следует сделать некоторые действия, которые могут выйти из строя по какой-либо причине. В этом случае, вместо того, чтобы вызывать такую логику через dtor частицы, которая могла бы выполняться по исключительному пути, вы могли бы вместо этого сделать все это системой частиц, когда она удаляет является частицей. Удаление частицы всегда может выполняться неисключительным путем. Если система разрушена, возможно, она сможет просто очистить все частицы и не беспокоиться об этой логике удаления отдельных частиц, которая может дать сбой, в то время как логика, которая может дать сбой, выполняется только во время нормального выполнения системы частиц, когда она удаляет одну или несколько частиц.
Часто встречаются подобные решения, которые возникают, если вы избегаете работы с множеством мелких объектов с нетривиальными деструкторами. Где вы можете запутаться в беспорядке, где кажется почти невозможным обеспечить безопасность исключений, - это когда вы запутаетесь во множестве крошечных объектов, у всех которых есть нетривиальные dtors.
Это очень помогло бы, если бы nothrow / noexcept фактически транслировалось в ошибку компилятора, если что-либо, указывающее на нее (включая виртуальные функции, которые должны наследовать спецификацию noexcept своего базового класса), пыталось вызвать что-либо, что может вызвать ошибку. Таким образом, мы могли бы перехватить все это во время компиляции, если бы мы действительно случайно напишем деструктор, который может выбросить.
Разрушение - это неудача?
Я думаю, он имеет в виду, что деструкторы вызываются во время сбоя, чтобы очистить этот сбой. Таким образом, если деструктор вызывается во время активного исключения, то он не может очиститься от предыдущего сбоя.
«Два исключения сразу» - стандартный ответ, но не НАСТОЯЩАЯ причина. Настоящая причина в том, что исключение должно генерироваться тогда и только тогда, когда постусловия функции не могут быть выполнены. Постусловие деструктора состоит в том, что объект больше не существует. Этого не может быть. Любая подверженная сбоям операция с окончанием срока службы должна поэтому вызываться как отдельный метод до того, как объект выйдет за пределы области видимости (в любом случае разумные функции обычно имеют только один путь к успеху).