Ответ 1
В вашем вопросе утверждается, что "Написание безопасного кода безопасности очень сложно". Сначала я отвечу на ваши вопросы, а затем отвечу на скрытый вопрос позади них.
Ответы на вопросы
Вы действительно пишете безопасный код исключения?
Конечно, да.
Это причина the. Java потерял много своей привлекательности для меня как программиста на С++ (отсутствие семантики RAII), но я отвлекаюсь: это вопрос на С++.
Это действительно необходимо, когда вам нужно работать с кодом STL или Boost. Например, потоки С++ (boost::thread
или std::thread
) будут генерировать исключение, чтобы выйти изящно.
Вы уверены, что ваш последний "готовый к производству" код безопасен?
Вы даже можете быть уверены, что это?
Написание исключающего кода кода похоже на создание кода без ошибок.
Вы не можете быть на 100% уверены, что ваш код безопасен. Но тогда вы стремитесь к этому, используя хорошо известные шаблоны и избегая известных анти-шаблонов.
Знаете ли вы и/или используете альтернативы, которые работают?
В С++ нет жизнеспособных альтернатив (т.е. вам нужно вернуться на C и избежать библиотек С++, а также внешних сюрпризов, таких как Windows SEH).
Написание кода безопасного исключения
Чтобы написать безопасный код исключения, вы должны знать first, какой уровень безопасности исключений для каждой команды, которую вы пишете.
Например, new
может генерировать исключение, но назначение встроенного (например, int или указателя) не приведет к сбою. Подкачка никогда не будет терпеть неудачу (никогда не пишите метательный обмен), std::list::push_back
может выбросить...
Гарантия исключения
Первое, что нужно понять, это то, что вы должны иметь возможность оценить гарантию исключения, предлагаемую всеми вашими функциями:
- none. Ваш код никогда не должен предлагать это. Этот код прольет все и разбивается при первом исключении.
- basic. Это гарантия, которую вы должны, по крайней мере, предлагать, то есть если исключение выбрано, ресурсы не просачиваются, а все объекты остаются целыми.
- strong. Обработка будет либо успешной, либо вызовет исключение, но если она будет выбрана, то данные будут в том же состоянии, что и обработка не началась вообще (это дает транзакционная мощность для С++)
- nothrow/nofail: обработка будет успешной.
Пример кода
Следующий код выглядит как правильный С++, но на самом деле предлагает гарантию "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 будет протекать.
Первое, что нужно сделать, это сделать функцию "basic", которая помещает 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
}
Теперь наш код предлагает "базовую" гарантию. Ничто не будет течь, и все объекты будут в правильном состоянии. Но мы могли бы предложить больше, то есть сильную гарантию. Здесь он может стать дорогостоящим, и именно поэтому сильный не все код С++. Попробуем:
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
of t
и работаем над этой копией из операции с 4 по 7. Если что-то бросает, t2
изменяется, но тогда t
по-прежнему является оригиналом. Мы по-прежнему предлагаем сильную гарантию.
Тогда мы поменяем t
и t2
. Операции свопинга должны быть не нарисованы на С++, поэтому давайте надеяться, что своп, который вы написали для t
, является nothrow (если это не так, перепишите его так, чтобы он не был).
Итак, если мы дойдем до конца функции, все сработало (нет необходимости в возвращаемом типе), а t
имеет свое значение. Если он терпит неудачу, то t
имеет все еще свое первоначальное значение.
Теперь предоставление сильной гарантии может быть довольно дорогостоящим, поэтому не стремись предоставить сильную гарантию ко всему вашему коду, но если вы можете сделать это без затрат (а вложение С++ и другая оптимизация могут сделать весь код выше безрезультатно), тогда сделайте это. Пользователь функции поблагодарит вас за это.
Заключение
Требуется некоторая привычка писать безопасный код. Вам нужно будет оценить гарантию, предлагаемую каждой инструкцией, которую вы будете использовать, а затем вам нужно будет оценить гарантию, предлагаемую списком инструкций.
Конечно, компилятор С++ не будет создавать резервную копию гарантии (в моем коде я предлагаю гарантию как тэг @warning doxygen), что печально, но это не должно мешать вам пытаться писать безопасные исключения код.
Нормальная ошибка с ошибкой
Как программист может гарантировать, что функция nofail всегда будет успешной? В конце концов, функция может иметь ошибку.
Это верно. Исключительные гарантии должны предлагаться без ошибок. Но тогда на любом языке вызов функции предполагает, что функция не содержит ошибок. Никакая здравомыслящий код не защищает себя от возможности наличия ошибки. Напишите код как можно лучше, а затем предложите гарантию с предположением, что это ошибка. И если есть ошибка, исправьте ее.
Исключения относятся к исключительной ошибке обработки, а не к ошибкам кода.
Последние слова
Теперь вопрос: "Стоит ли это?".
Конечно. Наличие функции "nothrow/nofail", зная, что функция не сработает, - отличное благо. То же самое можно сказать и о "сильной" функции, которая позволяет писать код с транзакционной семантикой, например, с базами данных, с функциями фиксации/отката, причем фиксация является нормальным выполнением кода, а исключения - откатом.
Тогда "основная" - это наименьшая гарантия, которую вы должны предложить. С++ - очень сильный язык там, с его областями, позволяющий избежать утечек ресурсов (что-то сборщику мусора будет трудно предложить для базы данных, подключения или файлов).
Итак, насколько мне известно, он стоит того.
Редактировать 2010-01-29: О не-метании swap
nobar сделал комментарий, который, я считаю, весьма уместен, потому что он является частью "как вы пишете безопасный код исключения":
- [me] Сделка никогда не потерпит неудачу (даже не пишите метательный обмен)
- [nobar] Это хорошая рекомендация для пользовательских функций
swap()
. Следует, однако, отметить, чтоstd::swap()
может завершиться неудачей на основе операций, которые он использует внутри
по умолчанию std::swap
создаст копии и назначения, которые для некоторых объектов могут быть выбрасываться. Таким образом, своп по умолчанию может использоваться, как для ваших классов, так и даже для классов STL. Что касается стандарта С++, операция свопинга для vector
, deque
и list
не будет выбрасываться, тогда как для map
, если функтор сравнения может бросить на построение копии (см. Язык программирования С++, специальное издание, приложение E, E.4.3.Swap).
Рассматривая реализацию векторного свопа Visual С++ 2008, векторный своп не будет бросать, если два вектора имеют один и тот же распределитель (т.е. обычный случай), но будут делать копии, если у них есть разные распределители. И, таким образом, я предполагаю, что это может забросить этот последний случай.
Итак, исходный текст по-прежнему сохраняется: никогда не пишите метапотный обмен, но нужно помнить о значении nobar: убедитесь, что объекты, которые вы меняете, имеют неперебрасываемый обмен.
Изменить 2011-11-06: Интересная статья
Дэйв Абрахамс, который дал нам основные/сильные/nothrow гарантии, описанный в статье, его опыт в том, чтобы сделать исключение STL безопасным:
http://www.boost.org/community/exception_safety.html
Посмотрите на 7-й пункт (автоматизированное тестирование для безопасности исключений), где он опирается на автоматическое тестирование модулей, чтобы убедиться, что каждый случай проверен. Я думаю, эта часть является отличным ответом автору вопроса " Вы даже можете быть уверены, что это?".
Редактировать 2013-05-31: Комментарий от dionadar
t.integer += 1;
без гарантии того, что переполнение не произойдет НЕ БЕЗОПАСНО БЕЗОПАСНО, и на самом деле может технически вызывать UB! (Подписанное переполнение - UB: С++ 11 5/4 "Если во время оценки выражения результат не определен математически или нет в диапазоне значений, представляемых для его типа, поведение undefined." ) Примечание. что целое число без знака не переполняется, а выполняет их вычисления в классе эквивалентности по модулю 2 ^ # бит.
Dionadar ссылается на следующую строку, которая действительно имеет поведение undefined.
t.integer += 1 ; // 1. nothrow/nofail
Решение состоит в том, чтобы проверить, действительно ли целое число уже имеет максимальное значение (используя std::numeric_limits<T>::max()
), прежде чем делать добавление.
Моя ошибка попадет в раздел "Нормальная ошибка с ошибкой", то есть ошибка. Это не делает недействительным аргументацию, и это не значит, что безопасный код исключений бесполезен, потому что его невозможно достичь. Вы не можете защитить себя от выключения компьютера или ошибок компилятора или даже ошибок или других ошибок. Вы не можете достичь совершенства, но можете попытаться приблизиться как можно ближе.
Я исправил код с комментарием Dionadar.