RAII против исключений
Чем больше мы используем RAII в C++, тем больше мы оказываемся с деструкторами, которые делают нетривиальное освобождение. Теперь освобождение (завершение, как бы вы его ни называли) может завершиться неудачей, и в этом случае исключения действительно единственный способ сообщить кому-либо наверху о нашей проблеме освобождения. Но опять же, метание деструкторов - плохая идея из-за возможности создания исключений при разматывании стека. std::uncaught_exception()
позволяет узнать, когда это произойдет, но не намного, так что кроме того, что вы можете записать сообщение до завершения, вы ничего не сможете сделать, если только вы не захотите оставить свою программу в неопределенном состоянии, где некоторые вещи освобождаются / завершаются а некоторые нет.
Один из подходов - иметь деструкторы без бросков. Но во многих случаях это просто скрывает настоящую ошибку. Наш деструктор может, например, закрывать некоторые управляемые RAII соединения с БД в результате генерирования некоторого исключения, и эти соединения с БД могут не закрыться. Это не обязательно означает, что мы согласны с завершением программы на этом этапе. С другой стороны, регистрация и отслеживание этих ошибок не является решением для каждого случая; иначе у нас не было бы необходимости в исключениях для начала. С деструкторами без бросков мы также сталкиваемся с необходимостью создавать функции "reset()", которые должны вызываться до уничтожения, но это просто наносит ущерб всей цели RAII.
Другой подход - просто позволить программе завершиться, поскольку это наиболее предсказуемая вещь, которую вы можете сделать.
Некоторые люди предлагают цепочку исключений, чтобы можно было обрабатывать более одной ошибки одновременно. Но я, честно говоря, никогда не видел, чтобы это было сделано в C++, и я понятия не имею, как реализовать такую вещь.
Так что это либо RAII, либо исключения. Не так ли? Я склоняюсь к деструкторам без бросков; главным образом потому, что это делает вещи простыми (r). Но я действительно надеюсь, что есть лучшее решение, потому что, как я уже сказал, чем больше мы используем RAII, тем больше мы используем dtors, которые делают нетривиальные вещи.
аппендикс
Я добавляю ссылки на интересные статьи и обсуждения по теме:
- Метательные деструкторы
- Stackru обсуждение проблем с SEH
- Обсуждение Stackru по метаниям-деструкторам (спасибо, Мартин Йорк)
- Джоэл об исключениях
- SEH считается вредным
- CLR Exception Handling, которая также касается цепочки исключений
- Херб Саттер на std::uncaught_exception и почему он не так полезен, как вы думаете
- Историческая дискуссия по этому вопросу с интересными участниками (долго!)
- Страуструп, объясняющий РАИИ
- Прицел Андрея Александреску
8 ответов
Вы НЕ ДОЛЖНЫ выбрасывать исключение из деструктора.
Примечание: обновлено, чтобы ссылаться на изменения в стандарте:
В С ++03
Если исключение уже распространяется, то приложение будет закрыто.
В С ++ 11
Если деструктор noexcept
(по умолчанию), тогда приложение будет закрыто.
Следующее основано на C++11
Если исключение ускользает от noexcept
Функция определяется реализацией, если стек даже размотан.
Следующее основано на C++03
Под прекращением я имею в виду немедленно прекратить. Разматывание стопки прекращается. Больше не называются деструкторы. Все плохие вещи. Смотрите обсуждение здесь.
выбрасывать исключения из деструктора
Я не следую (как и не согласен) с вашей логикой, что это делает деструктор более сложным.
При правильном использовании интеллектуальных указателей это фактически упрощает деструктор, поскольку теперь все становится автоматическим. Каждый класс подбирает свой маленький кусочек головоломки. Никаких операций на головном мозге или ракетостроении здесь. Еще одна большая победа RAII.
Что касается возможности std::uncaught_exception(), я указываю вам на статью Херба Саттерса о том, почему она не работает
Из оригинального вопроса:
Теперь освобождение (завершение, как бы вы его ни называли) может завершиться неудачей, и в этом случае исключения - единственный способ сообщить кому-либо наверху о нашей проблеме освобождения.
Неспособность очистить ресурс либо указывает:
Ошибка программиста, в этом случае вы должны регистрировать сбой, после чего уведомлять пользователя или завершать приложение, в зависимости от сценария приложения. Например, освобождение выделения, которое уже было освобождено.
Ошибка распределителя или недостаток дизайна. Обратитесь к документации. Скорее всего, ошибка, вероятно, поможет диагностировать ошибки программиста. Смотрите пункт 1 выше.
В противном случае неисправимые неблагоприятные условия, которые могут быть продолжены.
Например, в бесплатном хранилище C++ есть оператор delete без сбоев. Другие API (например, Win32) предоставляют коды ошибок, но не будут работать только из-за ошибки программиста или аппаратного сбоя, при этом ошибки указывают на такие условия, как повреждение кучи или двойное освобождение и т. Д.
Что касается неисправимых неблагоприятных условий, возьмите соединение с БД. Если закрытие соединения не удалось из-за сброса соединения - круто, все готово. Не бросай! Разорванное соединение (должно) приводит к закрытому соединению, поэтому больше не нужно ничего делать. Если что-нибудь, зарегистрируйте сообщение трассировки, чтобы помочь диагностировать проблемы использования. Пример:
class DBCon{
public:
DBCon() {
handle = fooOpenDBConnection();
}
~DBCon() {
int err = fooCloseDBConnection();
if(err){
if(err == E_fooConnectionDropped){
// do nothing. must have timed out
} else if(fooIsCriticalError(err)){
// critical errors aren't recoverable. log, save
// restart information, and die
std::clog << "critical DB error: " << err << "\n";
save_recovery_information();
std::terminate();
} else {
// log, in case we need to gather this info in the future,
// but continue normally.
std::clog << "non-critical DB error: " << err << "\n";
}
}
// done!
}
};
Ни одно из этих условий не оправдывает попытки второго вида отдыха. Либо программа может продолжаться в обычном режиме (включая откат исключения, если выполняется откат), либо она умирает здесь и сейчас.
Edit-Add
Если вы действительно хотите иметь возможность сохранять какие-то ссылки на те соединения с БД, которые не могут закрыться - возможно, они не закрылись из-за периодических условий, и вы хотите повторить попытку позже - тогда вы всегда можете отложить очистку:
vector<DBHandle> to_be_closed_later; // startup reserves space
DBCon::~DBCon(){
int err = fooCloseDBConnection();
if(err){
..
else if( fooIsRetryableError(err) ){
try{
to_be_closed.push_back(handle);
} catch (const bad_alloc&){
std::clog << "could not close connection, err " << err << "\n"
}
}
}
}
Очень не красиво, но это может сделать работу за вас.
Вы смотрите на две вещи:
- RAII, который гарантирует, что ресурсы очищаются при выходе из области действия.
- Завершение операции и выяснение того, удалось ли это или нет.
RAII обещает, что завершит операцию (освободит память, закроет файл, пытаясь его очистить, завершит транзакцию, пытаясь ее зафиксировать). Но поскольку это происходит автоматически, без необходимости что-либо делать программисту, он не сообщает программисту, были ли эти "попытки" успешными или нет.
Исключения являются одним из способов сообщить о том, что что-то не получилось, но, как вы говорите, у языка C++ есть ограничение, которое означает, что они не подходят для этого из деструктора [*]. Возвращаемые значения - это другой способ, но еще более очевидно, что деструкторы тоже не могут их использовать.
Итак, если вы хотите знать, были ли ваши данные записаны на диск, вы не можете использовать RAII для этого. Он не "опровергает всю цель RAII", поскольку RAII все равно будет пытаться записать его и освободит ресурсы, связанные с дескриптором файла (транзакция БД, что угодно). Он ограничивает возможности RAII - он не скажет вам, были ли записаны данные или нет, поэтому для этого вам нужно close()
функция, которая может возвращать значение и / или выдавать исключение.
[*] Это тоже вполне естественное ограничение, присутствующее на других языках. Если вы думаете, что деструкторы RAII должны генерировать исключения, чтобы сказать "что-то пошло не так!", Тогда что-то должно произойти, когда в полете уже есть исключение, то есть "что-то еще пошло не так даже раньше!". Языки, которые, как мне известно, используют исключения, не допускают двух исключений в полете одновременно - язык и синтаксис просто не допускают этого. Если RAII должен делать то, что вы хотите, то сами исключения должны быть переопределены так, чтобы в одном потоке имело смысл работать несколько ошибок одновременно, а два исключения распространялись наружу и вызывались два обработчика, по одному на каждого.
Другие языки позволяют второму исключению затемнять первое, например, если finally
броски блока в Java. C++ в значительной степени говорит, что второй должен быть подавлен, иначе terminate
называется (подавление обоих, в некотором смысле). Ни в том, ни в другом случае более высокие уровни стека не сообщаются обоим сбоям. Немного прискорбно то, что в C++ вы не можете точно сказать, является ли еще одно исключение слишком большим (uncaught_exception
это не говорит вам, это говорит вам что-то другое), так что вы даже не можете бросить в случае, когда в полете уже нет исключения. Но даже если бы вы могли сделать это в этом случае, вы все равно были бы забиты в случае, когда еще один - это слишком много.
Это напоминает мне вопрос от коллеги, когда я объяснил ему концепцию исключения /RAII: "Эй, какое исключение я могу выбросить, если компьютер выключен?"
Во всяком случае, я согласен с ответом Мартина Йорка RAII против исключений
Как обстоят дела с исключениями и деструкторами?
Многие функции C++ зависят от деструкторов, не генерирующих метание.
Фактически, вся концепция RAII и ее взаимодействие с ветвлением кода (возвраты, выбросы и т. Д.) Основаны на том факте, что освобождение не приведет к ошибке. Точно так же некоторые функции не должны давать сбои (например, std::swap), когда вы хотите предложить своим объектам гарантии высокого исключения.
Это не значит, что вы не можете создавать исключения через деструкторы. Просто этот язык даже не пытается поддерживать это поведение.
Что будет, если это будет разрешено?
Просто ради забавы я пытался это представить...
Если ваш деструктор не освободит ваш ресурс, что вы будете делать? Ваш объект, вероятно, наполовину разрушен, что бы вы сделали из "внешнего" улова с этой информацией? Попробуйте снова? (если да, то почему бы не попробовать снова изнутри деструктора?...)
То есть, если бы вы могли в любом случае получить доступ к своему полуразрушенному объекту: что, если ваш объект находится в стеке (что является основным способом работы RAII)? Как вы можете получить доступ к объекту за пределами его видимости?
Отправка ресурса внутри исключения?
Ваша единственная надежда будет состоять в том, чтобы отправить "дескриптор" ресурса внутри исключения и код надежды в улове, ну... попробуйте еще раз, чтобы освободить его (см. Выше)?
Теперь представьте что-нибудь смешное:
void doSomething()
{
try
{
MyResource A, B, C, D, E ;
// do something with A, B, C, D and E
// Now we quit the scope...
// destruction of E, then D, then C, then B and then A
}
catch(const MyResourceException & e)
{
// Do something with the exception...
}
}
Теперь давайте представим, что по какой-то причине деструктор D не может освободить ресурс. Вы закодировали его, чтобы отправить исключение, которое будет поймано уловом. Все идет хорошо: вы можете справиться с неудачей так, как вы хотите (как вы будете конструктивным образом все еще ускользать от меня, но тогда это не проблема сейчас).
Но...
Отправка НЕСКОЛЬКИХ ресурсов внутри НЕСКОЛЬКО исключений?
Теперь, если ~D может потерпеть неудачу, то ~C тоже может. а также ~B и ~A.
В этом простом примере у вас есть 4 деструктора, которые потерпели неудачу в тот же момент (выход из области видимости). Вам нужен не улов с одним исключением, а улов с массивом исключений (будем надеяться, что сгенерированный для этого код не... э... бросает).
catch(const std::vector<MyResourceException> & e)
{
// Do something with the vector of exceptions...
// Let's hope if was not caused by an out-of-memory problem
}
Давайте вернемся (мне нравится эта музыка...): Каждое выбрасываемое исключение отличается (потому что причина другая: помните, что в C++ исключения не обязательно происходят из std:: exception). Теперь вам нужно одновременно обработать четыре исключения. Как вы могли бы написать предложения catch, обрабатывающие четыре исключения по их типам и по порядку их выдачи?
А что, если у вас есть несколько исключений одного и того же типа, которые вызываются множественным неудачным освобождением? А что если при выделении памяти массивов исключений из массивов вашей программе не хватит памяти и, э-э... выбросить исключение из нехватки памяти?
Вы уверены, что хотите потратить время на решение этой проблемы, вместо того чтобы тратить время на выяснение причин неудачи или как по-другому отреагировать на нее?
По-видимому, дизайнеры C++ не увидели жизнеспособного решения, а просто сократили свои потери там.
Проблема не в RAII против исключений...
Нет, проблема в том, что иногда вещи могут потерпеть неудачу настолько, что ничего не поделаешь.
RAII хорошо работает с исключениями, если выполняются некоторые условия. Среди них: деструкторы не будут бросать. То, что вы видите как оппозицию, - это всего лишь угловой пример единого шаблона, объединяющего два "имени": исключение и RAII
В случае, если в деструкторе возникает проблема, мы должны принять поражение и спасти то, что можно спасти: "Не удалось освободить соединение с БД? Извините. По крайней мере, давайте избежим этой утечки памяти и закроем этот файл".
Хотя шаблон исключений является (как предполагается, основной) обработкой ошибок в C++, он не единственный. Вы должны обрабатывать исключительные (каламбур) случаи, когда исключения C++ не являются решением, используя другие механизмы ошибок / журналов.
Поскольку вы только что встретили стену на этом языке, стена, которую ни один другой язык, о котором я знаю или о котором я слышал, не прошла правильно, не обрушив дом (попытка C# была достойной, в то время как Java - это все еще шутка, которая ранит меня на стороне... Я даже не буду говорить о языках сценариев, которые будут молчать об одной и той же проблеме).
Но в конце концов, независимо от того, сколько кода вы напишите, пользователь не будет защищен выключением компьютера.
Лучшее, что вы можете сделать, вы уже написали это. Мои собственные предпочтения связаны с методом метания finalize, ресурсами очистки деструктора non-throwing, которые не были завершены вручную, и журналом / окном сообщений (если возможно) для предупреждения о сбое в деструкторе.
Возможно, вы не устраиваете правильный поединок. Вместо "RAII против исключения", это должно быть "Попытка высвободить ресурсы против ресурсов, которые абсолютно не хотят быть освобожденными, даже когда им угрожает разрушение".
:-)
Я бы хотел спросить, игнорируя вопрос о завершении и т. Д., Что, по вашему мнению, является правильным ответом, если ваша программа не может закрыть соединение с БД из-за обычного или исключительного уничтожения.
Похоже, вы исключаете "простую регистрацию" и не хотите заканчивать, так что, по вашему мнению, лучше всего сделать?
Я думаю, что если бы у нас был ответ на этот вопрос, у нас было бы лучшее представление о том, как действовать дальше.
Никакая стратегия не кажется мне особенно очевидной; кроме всего прочего, я действительно не знаю, что означает закрытие соединения с базой данных для выброса. Каково состояние соединения, если close() выбрасывает? Это закрыто, все еще открыто или неопределенно? И если это не определено, есть ли способ вернуть программу в известное состояние?
Отказ деструктора означает, что не было никакого способа отменить создание объекта; Единственный способ вернуть программу в известное (безопасное) состояние - это разорвать весь процесс и начать заново.
Каковы причины, по которым ваше разрушение может потерпеть неудачу? Почему бы не попытаться разобраться с ними до того, как уничтожить их?
Например, закрытие соединения с базой данных может быть вызвано тем, что:
- Транзакция в процессе. (Проверьте std::uncaught_exception() - если true, откат, еще фиксация - это наиболее вероятные желаемые действия, если у вас нет политики, которая говорит об обратном, до фактического закрытия соединения.)
- Соединение разорвано. (Обнаружение и игнорирование. Сервер будет выполнять откат автоматически.)
- Другая ошибка БД. (Запишите это, чтобы мы могли исследовать и, возможно, соответствующим образом обработать в будущем. Что может быть для обнаружения и игнорирования. Тем временем, попробуйте выполнить откат и снова отключиться и игнорировать все ошибки.)
Если я правильно понимаю RAII (чего я не могу), то все дело в его масштабах. Так что вы не хотите, чтобы транзакции длились дольше, чем объект в любом случае. Поэтому мне кажется разумным, что вы хотите обеспечить закрытие как можно лучше. RAII не делает это уникальным - даже без каких-либо объектов (скажем, в C) вы все равно пытаетесь перехватить все ошибки и справиться с ними как можно лучше (а иногда и игнорировать их). Все, что делает RAII, это заставляет вас размещать весь этот код в одном месте, независимо от того, сколько функций использует этот тип ресурса.
Вы можете узнать, есть ли в данный момент исключение в полете (например, мы находимся между блоком throw и catch, выполняющим разматывание стека, возможно, копирование объектов исключения или подобное), проверив
bool std::uncaught_exception()
Если он вернет true, бросок в этот момент приведет к завершению программы. Если нет, то выбросить безопасно (или, по крайней мере, так же безопасно, как и всегда). Это обсуждается в разделах 15.2 и 15.5.3 ISO 14882 (стандарт C++).
Это не отвечает на вопрос, что делать, когда вы ударили по ошибке при очистке исключения, но на самом деле нет хороших ответов на это. Но он позволяет вам различать нормальный выход и исключительный выход, если вы ожидаете сделать что-то другое (например, войти и игнорировать это) в последнем случае, а не просто паниковать.
Если действительно нужно исправить некоторые ошибки в процессе финализации , этого не следует делать в деструкторе. Вместо этого следует использовать отдельную функцию, которая возвращает код ошибки или может выдать ошибку. Чтобы повторно использовать код, вы можете вызвать эту функцию внутри деструктора, но вы не должны допускать утечки исключения.
Как упоминали некоторые люди, на самом деле это не освобождение ресурсов, а что-то вроде фиксации ресурса во время выхода. Как упоминали другие люди, что вы можете сделать, если сохранение не удается во время принудительного отключения питания? Вероятно, нет удовлетворительных ответов, но я бы предложил один из следующих подходов:
- Просто позвольте неудаче и потере случиться
- Сохраните несохраненную часть в другом месте и позвольте восстановлению произойти позже (см. Другой подход, если он тоже не работает).
Если вам не нравится ни один из этих подходов, сделайте так, чтобы ваш пользователь явно сохранил. Скажите им, чтобы они не полагались на автосохранение при отключении питания.