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

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

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

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

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

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

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

spraff 30.08.2011 14:13

@spraff Значит, любая функция с пустым постусловием может перехватывать и отбрасывать все исключения?

curiousguy 30.09.2011 03:29

Он должен перехватить и обрабатывать все исключения (сброс может быть приемлемым) ИЛИ он должен переписать свое постусловие, чтобы сказать: «если вход не удовлетворяет X, выход Y / undefined»

spraff 04.10.2011 12:07

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

spraff 04.10.2011 12:11

@spraff: Вы знаете, что то, что вы сказали, подразумевает «выбросить RAII»?

Kos 26.10.2011 17:30

На самом деле, как раз наоборот.

spraff 27.10.2011 11:52

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

Frunsi 08.08.2012 06:00

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

spraff 12.08.2012 01:11

> Если во время операции очистки возникает ошибка, вы ее просто игнорируете? Это верный вопрос. Упоминание «деструктора» вызывает только бесполезные автоматические ответы, такие как «деструктор не должен генерировать период». Единственно возможный ответ на такие общие вопросы - «как бывает».

curiousguy 30.09.2011 09:23

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

Nicholas Wilson 21.08.2013 13:44

Эта статья является более поздним продолжением обсуждения этой проблемы. cpp-next.com/archive/2012/08/evil-or-just-misunderstanding

ThomasMcLeod 24.12.2013 17:55

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

Jnana 15.11.2015 14:06

@Jnana Этот «аргумент» явно абсурден. Не каждый объект попадает в контейнер.

curiousguy 30.01.2017 05:00

@spraff "Нет, не поленитесь, напишите xyz () и очистите деструктор от логики, отличной от RAII." в блоке catch?

curiousguy 30.01.2017 05:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
271
14
119 333
16
Перейти к ответу Данный вопрос помечен как решенный

Ответы 16

Ответ принят как подходящий

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

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

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

Erik Forbes 25.09.2008 02:05

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

Martin York 25.09.2008 02:15

Когда приложение выходит из строя, ОС должна обрабатывать любые оставшиеся ресурсы.

Eclipse 03.10.2008 21:55

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

Martin York 03.10.2008 22:02

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

Tom 14.12.2008 01:06

@Martin: если приложение закрывает соединения с БД в деструкторе, возможно, оно неправильно спроектировано.

user195488 06.06.2011 22:51

@Eclipse "Операционная система должна обрабатывать любые оставшиеся ресурсы." некоторые ресурсы разделяются между процессами: Sys V IPC; POSIX mutex / sema ... тоже могут использоваться совместно. Или просто временные файлы. Существует множество ресурсов, которые операционная система не может безопасно очистить. Возможно, вы захотите иметь процесс-наблюдатель, который обнаруживает завершение вашего процесса и занимается этим.

curiousguy 30.09.2011 03:30

@ dog44wgm: исходный плакат ссылался на книгу Скотта Майерса «Эффективный C++». «Более эффективный C++» - это еще одна книга, и хотя она может говорить об одном и том же предмете, формулировка может быть совершенно иной.

Adrien Plisson 16.12.2011 19:17

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

doug65536 09.02.2014 19:41

@ doug65536: Ты хочешь сделать такое предположение (с твоим кораблем стоимостью 100 миллиардов долларов на пути к Марсу)?

Martin York 09.02.2014 21:33

@Deduplicator: Что это добавляет к текущему обсуждению.

Martin York 13.10.2014 04:52

@LokiAstari: Просто, если я правильно читаю стандарты C++ 11 и C++ 14, dtor будет иметь спецификацию noexcept(true) по умолчанию, что означает, что он не должен генерировать исключение ни при каких обстоятельствах. Поправьте меня если я ошибаюсь.

Deduplicator 13.10.2014 04:58

Я работаю над встраиваемыми системами. Я никогда не хочу, чтобы деструктор генерировал исключение ... весь бит о том, что "это задача ОС, которая должна обрабатывать очистку любых оставшихся ресурсов", НЕ МОЖЕТ полагаться в этом контексте. Я бы оставил все в плохом состоянии.

Erin 10.08.2015 23:51

@mangguo: Значит, у вас еще хуже с C++ 11. По умолчанию деструктор помечен как noexcept, что означает, что если программа выбрасывает и исключение из деструктора, приложение завершается без раскрутки стека и без вызова других деструкторов (вы даже не можете ожидать, что деструкторы между точкой выброса и деструктором будут называться либо).

Martin York 11.08.2015 01:22

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

Martin York 11.08.2015 01:22

@LokiAstari Я думал, что прекращение вызова было само собой разумеющимся;) Кроме того, мы находимся в процессе рефакторинга нашего использования исключений ... проблема, с которой я сейчас сталкиваюсь, заключается в том, что бывший коллега блестяще добавил что-то, что может вызвать исключение в деструктор без попытки / улова. Так что да, я согласен с вами. С помощью какой-то черной магии мы раньше не сталкивались с проблемами.

Erin 11.08.2015 01:42

@mangguo: Было бы еще лучше объявить деструктор, который отбрасывает исключения. ~myClass() noexcept(true | false | discard) :-) Примечание. В C++ 03 выдача исключения из деструктора не является проблемой и не вызывает вызова std::terminate() (если только исключение уже не распространяется).

Martin York 11.08.2015 01:44

@LokiAstari, это было бы неплохо ... но я не хочу оставлять оборудование в плохом состоянии. Или, может быть, так и сделаю, тогда я уйду с работы и оставлю какую-нибудь беднягу с кошмарной ситуацией, которую нужно отлаживать. Муахаха

Erin 11.08.2015 01:47

Небольшое примечание ... Поскольку деструкторы C++ 11 по умолчанию не имеют значения noexcept, поэтому для того, чтобы этот пример работал должным образом, нам нужно использовать ~Bad() noexcept(false) { throw 1; }.

Izaan 06.06.2017 17:24

@LokiAstari - это не остановка выполнения, сначала бросок в деструктор, а затем выход из блока try. почему идет во второй строке "бросить 2"?

EmptyData 14.09.2017 07:17

@LokiAstari - это не остановка выполнения, сначала бросок в деструктор, а затем выход из блока try. почему идет во второй строке "бросить 2"?

EmptyData 14.09.2017 07:20

@EmptyData: вы правы для публикации C++ 11 (действие деструктора по умолчанию - завершить бросок, поскольку деструкторы по умолчанию noexcept). Этот ответ был написан, когда C++ 03 был стандартом. Я обновлю, чтобы учесть изменение языка.

Martin York 14.09.2017 17:38

@EmptyData Теперь должно быть точно.

Martin York 14.09.2017 17:48

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

Sonic78 06.10.2017 10:26

@ 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)

Martin York 06.10.2017 20:54

Но деструктор может быть явно помечен как noexcept(false), что приводит к поведению C++ 03, если исключения, экранирующие деструктор, не вызывают terminate(). Вы также заметите, что я добавил подробные комментарии в приведенный выше пример, чтобы указать на это.

Martin York 06.10.2017 20:55

@Loki Astari: Извините, похоже, я использовал старый рабочий черновик и забыл добавить номер документа. Не существует еще в рабочем проекте под номером N4296 на странице 417 (Раздел 15.2, абзац 1). И да, это всего лишь примечание, но ИМХО хороший совет. (Как и ваше первое предложение «Выбрасывать исключение из деструктора опасно.»: Поэтому я поставил +1.) Тем временем я нашел хорошую запись в «блоге Анджея C++» (Деструкторы, которые выбрасывают) [akrzemi1.wordpress.com/2011/09/21/destructors-that-th‌ row /].

Sonic78 06.10.2017 22:03

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

Martin York 06.10.2017 23:20

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

Andy 20.02.2018 12:25

@MartinYork Я думаю, что здесь есть урок, что RAII, изначально предназначенный для управления локальными ресурсами, к сожалению, не совсем так идеален для многих других типов очистки, для которых он используется (хотя он по-прежнему превосходит ручную очистку во многих способов).

Andy 20.02.2018 12:29

«... что-нибудь опасное должно происходить в ...» ----------- Не правда ли, что каждая чертова строчка кода может вызвать исключение? Даже простое добавление может сделать это (Целочисленное переполнение).

Gravity 05.06.2020 14:06

@ Migrate2Lazarusseemyprofile Целочисленное переполнение не является исключением "C++". ЧТОБЫ получить исключение C++, некоторый код должен явно выполнить оператор throw. Таким образом, довольно легко понять, где и когда потенциально могут произойти исключения. 1) Оператор Throw 2) Все, что может содержать оператор throw => Функция / метод без маркировки noexcept

Martin York 05.06.2020 17:51

@MartinYork - Блин, я забыл, что этой функции (docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/…) нет в C++. Извиняюсь.

Gravity 05.06.2020 18:20

@ Migrate2Lazarusseemyprofile Это не имеет ничего общего с C++. Нигде в стандарте вы не найдете ничего подобного. Я предполагаю, что вы путаете эту документацию со стандартом C++. Это было бы прямым нарушением стандарта C++. Если вы собираетесь что-то процитировать, используйте стандарт. Здесь: stackoverflow.com/a/4653479/14065 Ссылка на текущую версию - n849

Martin York 05.06.2020 18:34

@MartinYork - это то, что я сказал в предыдущем комментарии: эта функция (вызывающая исключение EIntOverflow при нарушении диапазона) не существует в C++.

Gravity 05.06.2020 18:42

@ Migrate2Lazarusseemyprofile Зачем вообще упоминать об этом. Почему бы вам просто не удалить ошибочный комментарий.

Martin York 05.06.2020 18:43

@MartinYork - Слишком поздно. Информация "просочилась" :). Сообщите разработчикам C, что другие языки могут обрабатывать такие исключения / ошибки. Может быть, позже попросят реализовать его на C / ++.

Gravity 06.06.2020 19:59

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

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

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

Только что заметил ... бросание с дтора - это никогда Undefined Behavior. Конечно, он может вызвать terminate (), но это очень хорошо определенное поведение.

Martin Ba 26.12.2013 00:09

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

Andy 20.02.2018 12: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 не удается, как мне на самом деле удалить, гарантированно не потерпев неудачу? Черт возьми, это моя память!

Теперь рассмотрим, что в первом фрагменте кода объект автоматически удаляется, потому что он находится в стеке, а 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, включая перехват деструкторов, если вы захотите это сделать. По-прежнему не самый предпочтительный подход, но это действительно так.

tyree731 15.04.2019 16:59

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

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

Krishna Oza 24.01.2014 10:00

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

Marc van Leeuwen 06.06.2016 12:04

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

Например:

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 внутри деструктора?

Jason Liu 15.11.2019 09:53

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

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

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

Не неопределенное поведение, а немедленное прекращение.

Marc van Leeuwen 06.06.2016 12:10

В стандарте указано «неопределенное поведение». Такое поведение часто приводит к прекращению действия, но не всегда.

DJClayworth 06.06.2016 14:35

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

Marc van Leeuwen 06.06.2016 18:06

Из проекта 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 12.05.2009 13:19

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

lothar 12.05.2009 19:10

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

Krishna Oza 24.01.2014 11:12

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

... но я верю, что деструкторы для классов контейнерного типа, такие как вектор, не должны маскировать исключения, возникающие из классов, которые они содержат. В этом случае я фактически использую метод «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?

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

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

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

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

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

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

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

Я недавно попробовал ваш любимый вариант, и оказалось, что вам стоит это сделать нет. gotw.ca/gotw/047.htm

GManNickG 18.03.2010 18:03

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

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

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

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

Если мы рассмотрим вопрос таким образом, то я думаю, что можно утверждать, что семантика (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-returning uncaught_exceptions is 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. Дело в том, что семантика отката заключается в том, что здесь может произойти что угодно значимое. Так что нам действительно не следует игнорировать неудачу.

Weipeng L 23.08.2013 10:55

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

user541686 01.12.2015 08:22

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

Martin Ba 01.12.2015 14:06

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

user541686 01.12.2015 14:18

@MartinBa: Одной RAII-подобной альтернативой try / finally была бы поддержка C++ не только деструкторов, но и финализаторы. Тогда финализаторам будет вполне разрешено генерировать исключения поверх друг друга, потому что они не будут влиять на время жизни объектов (они управляются деструкторами, которые будут запускаться впоследствии, независимо от того, преуспели ли финализаторы, поскольку они не могут потерпеть неудачу ). Тот факт, что C++ рассматривает финализацию как уничтожение (или, если вы предпочитаете называть это, фиксация как релиз), является источником проблемы.

user541686 01.12.2015 14:25

@Mehrdad - спасибо за уточнение. Я думаю, что недоразумение (если оно таковое) происходит из-за того, что вы, кажется, предполагаете, что запрет или же, позволяющий «накладывать исключения друг на друга», имеет четко определенное техническое обоснование. В то время как я предлагаю OTOH, что это не так. Есть аргументы в пользу того и другого, и все C++, C# и Java имеют слабые места в этой области. terminate в C++ - это я, но молча проглотить первое исключение (Java, не так ли?) - тоже нормально.

Martin Ba 02.12.2015 00:29

@MartinBa: Под этим я имел в виду выброс исключений, когда они уже распространяются. Почему бы не иметь технического обоснования? Каждый язык допускает это вне деструкторов (даже C++ позволяет генерировать исключение в блоке catch); это прекрасно и четко определено. Единственная проблема здесь - деструкторы; причина того, что C++ не допускает этого, заключается в том, что он мешает высвобождению ресурсов, что должно происходить даже в случае сбоя завершения. Я утверждаю, что выпуск и финализация должны обрабатываться отдельно, чтобы это больше не было проблемой.

user541686 02.12.2015 02:43

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

Martin Ba 02.12.2015 12:29

@MartinBa, так что вы думаете о передаче ошибок во время разрушения функции обратного вызова? Похоже, что программисты на C++ редко вспоминают, что функции обратного вызова являются потенциальным решением всех видов проблем.

Andy 20.02.2018 12:05

@MartinBa также благодарит за то, что четко сформулировал различие между семантикой выпуска и фиксации, чтобы остановить волну «но как вы могли бы даже обрабатывать любые ошибки во время уничтожения?» введите ответы.

Andy 20.02.2018 12:11

@Mehrdad Бросок от ctor влияет на время жизни объекта, но не от dtor. Если нормально выполнить ошибку в блоке catch или, наконец, заблокировать, я не понимаю, почему это изначально плохо в dtor. Проблемы почти такие же.

curiousguy 15.01.2019 03:18

@curiousguy: одно из обещаний C++ заключается в том, что автоматические переменные уничтожаются, когда они выходят за пределы области видимости. Если вы выбрасываете из конструктора, проблем нет, поскольку объект еще не построен, но его поля созданы, поэтому их можно уничтожить. Однако, если вы выбрасываете из деструктора, объект не будет уничтожен - то есть он все еще существует - в то время как его поля должны быть уничтожены. Это означает, что вы получите объект, который существует после того, как его компоненты будут уничтожены, что не имеет смысла, и в противном случае AFAIK не встречается на языке.

user541686 15.01.2019 09:56

@Mehrdad "Однако если вы выберете из деструктора, объект не будет уничтожен" Почему это так?

curiousguy 15.01.2019 18:25

@curiousguy: потому что деструктор не завершил бы свою работу, которая уничтожает объект?

user541686 15.01.2019 19:56

@Mehrdad Если dtor не заботится о выполнении полной работы, это означает, что он не является потокобезопасным и не должен вызывать какую-либо функцию, которая может вызвать исключение, как и любая функция с побочными эффектами, которые нельзя оставить полдела сделано. Перераспределение вектора имеет такого рода проблемы: вы не можете оставить новый диапазон наполовину созданным, если один конструктор копирования не работает.

curiousguy 15.01.2019 23:02

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

user541686 16.01.2019 00:56

Позвольте нам продолжить обсуждение в чате.

curiousguy 16.01.2019 03:07

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

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

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

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

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

Konard 23.07.2019 19:25

Мартин Ба (вверху) находится на правильном пути - вы по-разному проектируете логику 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 noexcept to your destructor. And this means that the moment your destructor throws an exception, std::terminate will 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 своего базового класса), пыталось вызвать что-либо, что может вызвать ошибку. Таким образом, мы могли бы перехватить все это во время компиляции, если бы мы действительно случайно напишем деструктор, который может выбросить.

Разрушение - это неудача?

curiousguy 15.01.2019 03:16

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

user2445507 17.03.2019 03:41

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