В вашем вопросе утверждается, что «написание исключительного кода очень сложно». Сначала я отвечу на ваши вопросы, а затем отвечу на скрытый за ними вопрос.
Отвечая на вопросы
Вы действительно пишете код безопасности исключений?
Конечно, я делаю.
По этой причине Java потеряла свою привлекательность для меня как программиста C ++ (отсутствие семантики RAII), но я отвлекаюсь: это вопрос C ++.
Фактически это необходимо, когда вам нужно работать с кодом STL или Boost. Например, потоки C ++ ( boost::thread
или std::thread
) сгенерируют исключение для корректного выхода.
Вы уверены, что ваш последний «готовый к работе» код безопасен для исключений?
Можете ли вы быть уверены, что это так?
Написание кода, исключающего исключения, похоже на написание кода без ошибок.
Вы не можете быть на 100% уверены, что ваш код безопасен от исключений. Но затем вы стремитесь к этому, используя известные шаблоны и избегая известных анти-шаблонов.
Вы знаете и / или действительно используете альтернативы, которые работают?
В C ++ нет жизнеспособных альтернатив (т.е. вам нужно вернуться к C и избежать библиотек C ++, а также внешних неожиданностей, таких как Windows SEH).
Написание исключительного безопасного кода
Чтобы написать код безопасности исключений, вы должны сначала знать, каков уровень безопасности исключений для каждой написанной вами инструкции.
Например, a new
может выдать исключение, но назначение встроенного (например, int или указателя) не завершится неудачей. Своп никогда не потерпит неудачу (никогда не пишите своп броска), std::list::push_back
может бросить ...
Гарантия исключения
Первое, что нужно понять, это то, что вы должны быть в состоянии оценить гарантию исключения, предлагаемую всеми вашими функциями:
- none : ваш код никогда не должен предлагать это. Этот код пропустит все и сломается при первом же исключении.
- базовая : это гарантия, которую вы должны предоставить по меньшей мере, то есть, если выдается исключение, ресурсы не просачиваются, а все объекты остаются целыми
- сильный : обработка либо завершится успешно, либо сгенерирует исключение, но если он сгенерирует, то данные будут в том же состоянии, как если бы обработка вообще не началась (это дает транзакционную мощность C ++)
- nothrow / nofail : обработка будет успешной.
Пример кода
Следующий код кажется правильным C ++, но на самом деле предлагает гарантию «none», и, следовательно, он не корректен:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Я пишу весь свой код с учетом такого анализа.
Самая низкая предлагаемая гарантия - базовая, но при этом порядок каждой инструкции делает всю функцию «нулевой», потому что, если 3. throws, x утечет.
Первое, что нужно сделать, это сделать функцию «базовой», то есть поместить умный указатель в x, пока он не будет безопасно принадлежать списку:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Теперь наш код предлагает «базовую» гарантию. Ничего не протечет, и все объекты будут в правильном состоянии. Но мы могли бы предложить больше, то есть сильную гарантию. Вот где это может стать дорогостоящим, и поэтому не весь код C ++ является сильным. Давай попробуем:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Мы переупорядочили операции, сначала создав и установив X
их правильное значение. Если какая-либо операция завершается неудачно, то t
она не изменяется, поэтому операции с 1 по 3 можно считать «сильной»: если что-то выбрасывается, t
не изменяется и X
не будет течь, потому что принадлежит интеллектуальному указателю.
Затем мы создаем копию t2
из t
, и работать на этой копии от операции 4 до 7. Если что - то бросает, t2
модифицируется, но затем, по- t
прежнему является оригинальным. Мы все еще предлагаем сильную гарантию.
Затем мы переставляем t
и t2
. Операции подкачки должны быть nothrow в C ++, поэтому будем надеяться, что T
подкачка, для которой вы написали, - nothrow (если это не так, переписайте ее, чтобы она не стала выше).
Таким образом, если мы достигнем конца функции, все выполнится успешно (нет необходимости в возвращаемом типе) и будет t
иметь свое исключенное значение. Если это не удастся, значит t
, все еще имеет свое первоначальное значение.
Теперь предоставление надежной гарантии может быть довольно дорогостоящим, поэтому не пытайтесь предлагать надежную гарантию всему своему коду, но если вы можете сделать это без затрат (а встраивание C ++ и другая оптимизация могут сделать весь код выше бесплатным) , затем сделать его. Пользователь функции поблагодарит вас за это.
Вывод
Требуется некоторая привычка для написания безопасного кода. Вам нужно будет оценить гарантию, предлагаемую каждой инструкцией, которую вы будете использовать, а затем вам нужно будет оценить гарантию, предлагаемую списком инструкций.
Конечно, компилятор C ++ не будет резервировать гарантию (в моем коде я предлагаю гарантию в виде тега @warning doxygen), что довольно печально, но не должно мешать вам пытаться писать код, безопасный для исключений.
Нормальная ошибка против ошибки
Как программист может гарантировать, что безотказная функция всегда будет успешной? В конце концов, функция может иметь ошибку.
Это верно. Предполагается, что исключения гарантируются кодом без ошибок. Но тогда, на любом языке, вызов функции предполагает, что функция не содержит ошибок. Никакой вменяемый код не защищает себя от возможной ошибки. Напишите код как можно лучше, а затем предложите гарантию, предполагая, что он не содержит ошибок. И если есть ошибка, исправьте ее.
Исключения - исключительная ошибка обработки, а не ошибки кода.
Последние слова
Теперь вопрос: «Стоит ли это того?».
Конечно, это является. Наличие функции «nothrow / no-fail», зная, что функция не потерпит неудачу, является большим благом. То же самое можно сказать и о «сильной» функции, которая позволяет писать код с семантикой транзакций, такой как базы данных, с функциями фиксации / отката, когда коммит является нормальным выполнением кода, а исключения - откатом.
Тогда «базовая» - это минимальная гарантия, которую вы должны предложить. C ++ - очень сильный язык, с его областями действия, позволяющими избежать любых утечек ресурсов (то, что сборщику мусора будет трудно предложить для базы данных, соединения или файловых дескрипторов).
Итак, насколько я понимаю, это является стоит.
Редактировать 2010-01-29: О безбросовом свопе
nobar сделал комментарий, который, на мой взгляд, весьма актуален, поскольку он является частью «как вы пишете безопасный код исключения»:
- [меня] своп никогда не подведет (даже не пишите своп)
- [nobar] Это хорошая рекомендация для пользовательских
swap()
функций. Следует отметить, однако, что std::swap()
может произойти сбой на основе операций, которые он использует внутри
по умолчанию std::swap
будут создаваться копии и назначения, которые для некоторых объектов могут генерировать. Таким образом, подкачка по умолчанию может сгенерировать, либо для ваших классов, либо даже для классов STL. Что касается стандарта C ++, то операция swap для vector
, deque
и list
не будет выбрасываться, тогда как это возможно, map
если функтор сравнения может сгенерировать конструкцию копирования (см. Язык программирования C ++, Специальный выпуск, приложение E, E.4.3 Своп )
Если посмотреть на реализацию подкачки вектора в Visual C ++ 2008, то подкачка вектора не сработает, если два вектора имеют одинаковый распределитель (т. Е. Обычный случай), но будет делать копии, если они имеют разные распределители. И, таким образом, я предполагаю, что это может бросить в этом последнем случае.
Таким образом, исходный текст остается в силе: никогда не пишите своп-бросок, но необходимо запомнить комментарий Нобара: убедитесь, что объекты, которые вы меняете, имеют не-бросающий своп.
Редактировать 2011-11-06: Интересная статья
Дейв Абрахамс , который дал нам основные / сильные / nothrow гарантии , в своей статье описал свой опыт по обеспечению безопасности исключения STL:
http://www.boost.org/community/exception_safety.html
Посмотрите на 7-й пункт (Автоматизированное тестирование для исключительной безопасности), где он полагается на автоматическое модульное тестирование, чтобы убедиться, что каждый случай проверен. Думаю, эта часть является отличным ответом на вопрос автора « Можете ли вы быть уверены, что это так? ».
Изменить 2013-05-31: Комментарий от дионадар
t.integer += 1;
без гарантии, что переполнение не произойдет, не исключение безопасно, и на самом деле может технически вызвать UB! (Переполнением со знаком является UB: C ++ 11 5/4 «Если во время вычисления выражения результат не определен математически или не находится в диапазоне представляемых значений для его типа, поведение не определено.») Обратите внимание, что без знака целые числа не переполняются, но выполняют вычисления в классе эквивалентности по модулю 2 ^ # бит.
Dionadar ссылается на следующую строку, которая действительно имеет неопределенное поведение.
t.integer += 1 ; // 1. nothrow/nofail
Решение здесь состоит в том, чтобы проверить, является ли целое число уже в его максимальном значении (использование std::numeric_limits<T>::max()
) перед выполнением сложения.
Моя ошибка будет в разделе «Нормальная ошибка против ошибки», то есть ошибка. Это не лишает законной силы аргументацию и не означает, что безопасный код исключений бесполезен, потому что его невозможно достичь. Вы не можете защитить себя от выключения компьютера, ошибок компиляции, даже ваших ошибок или других ошибок. Вы не можете достичь совершенства, но вы можете попытаться приблизиться как можно ближе.
Я исправил код с учетом комментария Дионадара.