Вы (действительно) пишете безопасный код исключения?

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

Хотя большинство людей, похоже, игнорируют это или просто принимают его, EH имеет некоторые огромные недостатки: исключения невидимы для кода, и он создает много и много возможных точек выхода. Joel on software написал статью об этом. Сравнение с goto идеально подходит, это заставило меня снова подумать о EH.

Я стараюсь избегать EH и просто использовать возвращаемые значения, обратные вызовы или что-то подходящее для этой цели. Но , когда вам нужно писать надежный код, вы просто не можете игнорировать EH в наши дни: он начинается с new, который может вызывать исключение вместо того, чтобы просто возвращать 0 (как в старом дней). Это делает любую строку кода С++ уязвимой для исключения. И тогда больше мест в исходном коде С++ генерируют исключения... std lib делает это и т.д.

Это ощущение, что идет по шатким основаниям. Итак, теперь мы вынуждены заботиться об исключениях!

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

EH заменил старый чистый детерминированный подход (возвращаемые значения..), у которого было только несколько, но понятных и легко решаемых недостатков с подходом, который создает много возможных точек выхода в вашем коде, и если вы начнете писать код, который ловит исключения (что вам нужно сделать в какой-то момент), тогда он даже создает множество путей через ваш код (код в блоках catch, подумайте о серверной программе, где вам нужны средства ведения журнала, отличные от std:: cerr..). EH имеет преимущества, но это не главное.

Мои актуальные вопросы:

  • Вы действительно пишете безопасный код исключения?
  • Вы уверены, что ваш последний "готовый к производству" код является безопасным для исключения?
  • Вы даже можете быть уверены, что это?
  • Знаете ли вы и/или используете альтернативы, которые работают?

Ответ 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.

Ответ 2

Написание исключающего кода в С++ заключается не столько в использовании большого количества блоков try {} catch {}. Это о документировании того, какие гарантии предоставляет ваш код.

Я рекомендую прочитать статью Herb Sutter The Guru Of The Week", в частности рассрочки 59, 60 и 61.

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

  • Основной: когда ваш код генерирует исключение, ваш код не теряет ресурсы, а объекты остаются разрушаемыми.
  • Сильный: когда ваш код генерирует исключение, он оставляет неизменным состояние приложения.
  • Без броска: ваш код никогда не выдает исключений.

Лично я обнаружил эти статьи довольно поздно, поэтому мой код на С++ определенно не исключен.

Ответ 3

Некоторые из нас используют исключение более 20 лет. PL/у меня есть, например. Посылка о том, что они являются новой и опасной технологией, кажется мне сомнительной.

Ответ 4

Прежде всего (как заявил Нил), SEH - это обработка структурированных исключений Microsoft. Он аналогичен, но не идентичен обработке исключений на С++. Фактически, вы должны включить С++ Exception Handling, если вы хотите этого в Visual Studio - поведение по умолчанию не гарантирует, что локальные объекты будут уничтожены во всех случаи! В любом случае обработка исключений не является более сложной, она просто другая.

Теперь для ваших реальных вопросов.

Вы действительно пишете безопасный код исключения?

Да. Во всех случаях я стараюсь исключить безопасный код. Я евангелизирую с использованием методов RAII для доступа к ресурсам (например, boost::shared_ptr для памяти, boost::lock_guard для блокировки). В общем, последовательное использование RAII и методов защиты области сделать безопасный код исключения намного проще для записи. Трюк заключается в том, чтобы узнать, что существует и как его применять.

Вы уверены, что ваш последний "готовый к производству" код безопасен?

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

Вы даже можете быть уверены, что это?

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

Знаете ли вы и/или используете альтернативы, которые работают?

Я пробовал несколько вариаций на протяжении многих лет, таких как состояния кодирования в верхних битах (ala HRESULT s) или что ужасный setjmp() ... longjmp() взломать. Оба они разрушаются на практике, хотя совершенно по-разному.


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

  • Вы хотите видеть только try/catch, когда вы можете что-то сделать с конкретным исключением.
  • Вы почти никогда не хотите видеть raw new или delete в коде
  • Eschew std::sprintf, snprintf и массивы вообще - используйте std::ostringstream для форматирования и замены массивов с помощью std::vector и std::string
  • Если вы сомневаетесь, ищите функциональность в Boost или STL, прежде чем запускать свои собственные.

Я могу только рекомендовать вам научиться правильно использовать исключения и забыть о кодах результатов, если вы планируете писать на С++. Если вы хотите избежать исключений, вам может потребоваться написать на другом языке, который не имеет их или делает их безопасными. Если вы хотите научиться полностью использовать С++, прочитайте несколько книг из Herb Sutter, Nicolai Josuttis, и Скотт Майерс.

Ответ 5

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

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

Ответ 6

  • Вы действительно пишете безопасный код исключения?

Ну, я, конечно, намерен.

  • Вы уверены, что ваш последний "готовый к производству" код является безопасным для исключения?

Я уверен, что мои серверы 24/7, построенные с использованием исключений, запускаются 24/7 и не пропускают память.

  • Вы даже можете быть уверены, что это?

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

  • Знаете ли вы и/или используете альтернативы, которые работают?

Нет. Использование исключений чище и проще, чем любые альтернативы, которые я использовал за последние 30 лет в программировании.

Ответ 7

Оставляя в стороне путаницу между исключениями SEH и С++, вы должны знать, что исключения могут быть выброшены в любое время, и напишите свой код с учетом этого. Необходимость безопасности исключений в значительной степени обуславливает использование RAII, интеллектуальных указателей и других современных методов С++.

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

Ответ 8

EH хорош, как правило. Но реализация С++ не очень дружелюбна, так как очень сложно сказать, насколько хорош ваш охват исключения. Java, например, делает это проще, компилятор будет терпеть неудачу, если вы не будете обрабатывать возможные исключения.

Ответ 9

Мне очень нравится работать с Eclipse и Java, хотя (новичок в Java), потому что он выдает ошибки в редакторе, если вам не хватает обработчика EH. Это делает вещи LOT труднее забыть обработать исключение...

Кроме того, с помощью средств IDE он автоматически добавляет блок try/catch или другой блок catch.

Ответ 10

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

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

Можно ли написать эту операцию, чтобы обеспечить надежную гарантию исключения? Должен ли я согласиться на основной? Какие строки могут генерировать исключения, и как я могу убедиться, что если они это сделают, они не повреждают объект?

Ответ 11

  • Вы действительно пишете безопасный код исключения? [Нет такой вещи. Исключения составляют бумажный экран для ошибок, если у вас нет управляемой среды. Это относится к первым трем вопросам.]

  • Знаете ли вы и/или используете альтернативы, которые работают? [Альтернатива чему? Проблема в том, что люди не отделяют фактические ошибки от нормальной работы программы. Если это нормальная работа программы (т.е. Файл не найден), это не значит, что обработка ошибок. Если это фактическая ошибка, невозможно "обработать" ее или это не фактическая ошибка. Ваша цель состоит в том, чтобы выяснить, что пошло не так, либо остановить электронную таблицу, и зарегистрировать ошибку, перезапустить драйвер к вашему тостеру или просто молиться, чтобы реактивный самолет продолжал летать, даже когда это программное обеспечение искажает и надеется на лучшее.]

Ответ 12

Некоторые из нас предпочитают такие языки, как Java, которые заставляют нас объявлять все исключения, создаваемые методами, вместо того, чтобы сделать их невидимыми, как в С++ и С#.

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

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

По моему опыту было сложно написать чистый код обработки исключений на С++. Я в конечном итоге использую new(nothrow) много.

Ответ 13

Многие (я бы сказал, большинство) люди делают.

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

Вам нужно активно совершать ошибки в обработчиках, чтобы получить что-то небезопасное, и только catch (...) {} будет сравнивать с игнорированием кода ошибки.