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

33

Я пытаюсь убедить руководство своей команды разрешить использование исключений в C ++ вместо возврата bool isSuccessfulили enum с кодом ошибки. Однако я не могу противостоять его критике.

Рассмотрим эту библиотеку:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Рассмотрим разработчика, вызывающего B()функцию. Он проверяет документацию и видит, что она не возвращает никаких исключений, поэтому он не пытается ничего поймать. Этот код может привести к сбою программы в процессе производства.

  2. Рассмотрим разработчика, вызывающего C()функцию. Он не проверяет документацию, поэтому не ловит никаких исключений. Вызов небезопасен и может привести к аварийному завершению работы программы.

Но если мы проверим ошибки таким образом:

void old_C(myenum &return_code);

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

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

DBedrenko
источник
4
@Sjoerd Основное преимущество заключается в том, что компилятор заставляет разработчика предоставлять переменную функции для хранения кода возврата; он осознает, что может быть ошибка, и он должен устранить ее. Это бесконечно лучше, чем исключения, которые вообще не проверяются во время компиляции.
Д.Бедренко
4
«он должен справиться с этим» - нет проверки, происходит ли это тоже.
Sjoerd
4
@ Sjoerd Это не обязательно. Достаточно хорошо, чтобы разработчик знал, что может произойти ошибка, и он должен проверить ее. Рецензенты тоже могут видеть, проверили ли они ошибку или нет.
Д.Бедренко
5
Возможно, у вас больше шансов использовать Either/ Resultmonad для возврата ошибки в безопасном для компоновки виде
Daenyth
5
@gardenhead Поскольку слишком много разработчиков не используют его должным образом, в конечном итоге получаются уродливые коды и продолжают обвинять эту функцию вместо себя. Функция проверенных исключений в Java - прекрасная вещь, но она получает плохой рэп, потому что некоторые люди просто не понимают этого.
AxiomaticNexus

Ответы:

47

Это законная критика исключений. Они часто менее заметны, чем простая обработка ошибок, такая как возврат кода. И нет простого способа обеспечить соблюдение «контракта». Отчасти дело в том, чтобы позволить вам перехватывать исключения на более высоком уровне (если вам нужно перехватывать каждое исключение на каждом уровне, насколько оно отличается от возврата кода ошибки, в любом случае?). А это значит, что ваш код может быть вызван другим кодом, который не обрабатывает его должным образом.

Исключения имеют свои недостатки; Вы должны сделать случай, основанный на экономической выгоде.
Я нашел эти две статьи полезными: необходимость исключений и все неправильно с исключениями. Кроме того, эта запись блога предлагает мнения многих экспертов об исключениях с упором на C ++ . Хотя мнение эксперта, похоже, склоняется в пользу исключений, оно далеко от четкого консенсуса.

Что касается убеждения лидера вашей команды, это не может быть правильным выбором. Особенно с устаревшим кодом. Как отмечено во второй ссылке выше:

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

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

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


источник
1
Спасибо за совет и ссылки. Это новый, а не устаревший проект. Мне интересно, почему исключения настолько распространены, когда имеют такой большой недостаток, что делает их использование небезопасным. Если вы перехватываете исключение на n уровней выше, то позже вы модифицируете код и перемещаете его, нет статической проверки, чтобы гарантировать, что исключение все еще перехватывается и обрабатывается (и это слишком много, чтобы просить рецензентов проверить все функции на глубине n уровней). ).
Д.Бедренко
3
@ См. Ссылки выше для некоторых аргументов в пользу исключений (я добавил дополнительную ссылку с более экспертным мнением).
6
Этот ответ на месте!
Док Браун
1
По своему опыту я могу сказать, что попытка добавить исключения в существующую кодовую базу - ДЕЙСТВИТЕЛЬНО плохая идея. Я работал с такой кодовой базой, и с ней ДЕЙСТВИТЕЛЬНО, ДЕЙСТВИТЕЛЬНО трудно работать, потому что вы никогда не знали, что может взорваться вам в лицо. Исключения следует использовать ТОЛЬКО в проектах, где вы планируете их заранее.
Майкл Кохн
26

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


Исключения не предназначены для повсеместного распространения.

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

В моем коде есть только несколько операторов catch - если они вообще есть - вне основного цикла. И это похоже на общий подход в современном C ++.


Исключения и коды возврата не являются взаимоисключающими.

Вы не должны делать это «все или ничего». Исключения следует использовать для исключительных ситуаций. Такие вещи, как «Config file not found», «Disk Full» или что-либо еще, что не может быть обработано локально.

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

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

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


Статическая проверка исключений бесполезна.

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

В Java есть статическая проверка, но обычно это считается неудачным экспериментом, и большинство языков, особенно C #, не имеют такого типа статической проверки. Это хорошее прочтение о причинах, почему в C # его нет.

По этим причинам C ++ устарел throw(exceptionA, exceptionB)в пользу noexcept(true). По умолчанию функция может выдавать, поэтому программисты должны ожидать этого, если в документации явно не указано иное.


Написание безопасного кода исключений не имеет ничего общего с написанием обработчиков исключений.

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

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

Сьерд
источник
3
« Пока есть общий обработчик исключений ... с вами обычно все в порядке ». Это противоречит большинству источников, которые подчеркивают, что очень сложно писать безопасный код исключений.
12
@ dan1111 «Написание безопасного кода исключений» имеет мало общего с написанием обработчиков исключений. Написание безопасного кода исключений - это использование RAII для инкапсуляции ресурсов, использование «сначала все делай локально, потом используй swap / move» и другие лучшие практики. Это сложно, когда ты к ним не привык. Но «написание обработчиков исключений» не является частью этих лучших практик.
Sjoerd
4
@AxiomaticNexus На некотором уровне вы можете объяснить каждое неудачное использование функции как «плохих разработчиков». Величайшие идеи - те, которые уменьшают плохое использование, потому что они делают правильную вещь простой. Статически проверенные исключения не попадают в эту категорию. Они создают работу, непропорциональную их стоимости, часто там, где написанному коду просто не нужно заботиться о них. У пользователей возникает соблазн заставить их замолчать неправильным образом, чтобы компилятор перестал их беспокоить. Даже если все сделано правильно, они приводят к большому беспорядку.
jpmc26
4
@AxiomaticNexus Причина, по которой он является непропорциональным, заключается в том, что он может легко распространяться почти на все функции всей вашей кодовой базы . Когда все функции в половине моих классов обращаются к базе данных, не каждая из них должна явно объявить, что может выдать исключение SQLException; и не каждый метод контроллера, который их вызывает. Такой шум на самом деле является отрицательным значением, независимо от того, насколько мал объем работы. Sjoerd бьет по голове: большая часть вашего кода не должна касаться исключений, потому что он все равно ничего не может с ними поделать .
jpmc26
3
@AxiomaticNexus Да, потому что лучшие разработчики настолько совершенны, что их код всегда будет отлично обрабатывать каждый возможный пользовательский ввод, и вы никогда и никогда не увидите такие исключения в prod. И если вы делаете, это означает, что вы неудачник в жизни. Серьезно, не давай мне этот мусор. Никто не настолько совершенен, даже не самый лучший. И ваше приложение должно вести себя разумно даже перед лицом этих ошибок, даже если «разумно» означает «остановить и вернуть половину приличной страницы / сообщения об ошибке». И угадайте, что: это то же самое, что вы делаете с 99% IOExceptions тоже . Проверенное деление произвольно и бесполезно.
jpmc26
8

Программисты C ++ не ищут спецификации исключений. Они ищут гарантии исключений.

Предположим, что фрагмент кода выдал исключение. Какие предположения может сделать программист, которые все еще будут действительны? Что написано в коде, что гарантирует код после исключения?

Или это возможно, что определенный кусок кода может гарантировать, что никогда не выбросит (то есть, ничего, кроме процесса ОС будет прекращено)?

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

Различные методы программирования на C ++ обеспечивают гарантии исключений. RAII (управление ресурсами на основе области действия) предоставляет механизм для выполнения кода очистки и обеспечивает высвобождение ресурсов как в обычных, так и в исключительных случаях. Создание копии данных перед выполнением изменений на объектах позволяет восстановить состояние этого объекта в случае сбоя операции. И так далее.

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

При разработке на C ++ (для производственного использования) никто не может позволить себе замаскировать детали. Кроме того, двоичный двоичный объект (не с открытым исходным кодом) - проклятие программистов на C ++. Если мне нужно вызвать какой-нибудь двоичный двоичный объект, и этот двоичный объект не работает, то обратный инжиниринг - это то, что программист C ++ сделает дальше.

Ссылка: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - см. В разделе «Безопасность исключений».

В C ++ не удалось реализовать спецификации исключений. Более поздний анализ на других языках говорит, что спецификации исключений просто не практичны.

Почему это неудачная попытка: для строгого соблюдения она должна быть частью системы типов. Но это не так. Компилятор не проверяет спецификацию исключений.

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

Ссылка: http://www.gotw.ca/publications/mill22.htm

Начиная с C ++ 17, этот [[nodiscard]]атрибут можно использовать для возвращаемых значений функции (см .: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value не может быть проигнорировано ).

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

Если вы принимаете аргументы, что программисты на C ++ ищут гарантии исключений вместо спецификаций исключений, тогда ответ таков: если условие сбоя нового типа не нарушает какие-либо исключения, гарантирует код, обещанный ранее, это не является критическим изменением.

rwong
источник
«Когда кто-то изменяет реализацию функции (например, ей нужно вызвать другую функцию, которая может вызвать исключение нового типа), строгое соблюдение спецификации исключений означает, что вам также необходимо обновить эту спецификацию» - Хорошо , как вы должны. Какой будет альтернатива? Игнорирование недавно введенного исключения?
AxiomaticNexus
@AxiomaticNexus На это также отвечает гарантия исключения. На самом деле я утверждаю, что спецификация никогда не может быть полной; гарантия - это то, что мы ищем. Часто мы сравниваем тип исключения в попытке выяснить, какая гарантия была нарушена - чтобы угадать, какая гарантия все еще действительна. В спецификации исключений перечислены некоторые типы, которые необходимо проверить, но помните, что в любой практической системе список не может быть полным. В большинстве случаев это заставляет людей использовать ярлык «поедания» типа исключения, заключая его в другое исключение, что затрудняет проверку типа исключения
rwong
@AxiomaticNexus Я не спорю за или против использования исключения. Типичное использование исключений предполагает наличие базового класса исключений и некоторых производных классов исключений. Между тем, нужно помнить, что возможны другие типы исключений, например, выброшенные из C ++ STL или других библиотек. Можно утверждать, что спецификация исключений может быть сделана для работы в этом случае: укажите, что std::exceptionбудут выброшены стандартные исключения ( ) и ваше собственное базовое исключение. Но когда применяется ко всему проекту, это просто означает, что каждая функция будет иметь такую ​​же спецификацию: шум.
Rwong
Я хотел бы отметить, что когда речь идет об исключениях, C ++ и Java / C # - это разные языки. Во-первых, C ++ не является типобезопасным в смысле разрешения выполнения произвольного кода или перезаписи данных, во-вторых, C ++ не выводит хорошую трассировку стека. Эти различия вынуждали программистов на C ++ делать разные выборы с точки зрения обработки ошибок и исключений. Это также означает, что некоторые проекты могут на законных основаниях утверждать, что C ++ не подходит для них. (Например, я лично считаю, что декодирование изображений TIFF не должно быть разрешено для реализации на C или C ++.)
rwong
1
@ rwong: Большая проблема с обработкой исключений в языках, которые следуют модели C ++, заключается в том, что catchона основана на типе объекта исключения, когда во многих случаях важна не столько прямая причина исключения, сколько то, что оно подразумевает в отношении состояние системы. К сожалению, тип исключения, как правило, ничего не говорит о том, вызвало ли оно прерывистый выход кода из кода таким образом, чтобы объект оставался в частично обновленном (и, следовательно, недействительном) состоянии.
суперкат
4

Рассмотрим разработчика, вызывающего функцию C (). Он не проверяет документацию, поэтому не ловит никаких исключений. Звонок небезопасен

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

DeadMG
источник
Это небезопасно, поскольку он не ожидает появления исключения C()и, следовательно, не защищает от кода после пропуска вызова. Если этот код требуется для гарантии того, что некоторая инвариантность остается верной, эта гарантия теряет силу при первом появлении исключения C(). Не существует такого понятия, как совершенно безопасное для исключений программное обеспечение, и, скорее всего, не там, где исключений никогда не ожидали.
мастер
1
@cmaster Его не ожидая, чтоC() возникнет исключение - большая ошибка. По умолчанию в C ++ любая функция может выдавать - для этого требуется явное указание noexcept(true)компилятору иначе. В отсутствие этого обещания программист должен предположить, что функция может сгенерировать.
Sjoerd
1
@cmaster Это его собственная глупая ошибка и ничего общего с автором C (). Исключительная безопасность - вещь, которую он должен изучить и следовать ей. Если он вызывает функцию, которую он не знает, это не исключение, он должен чертовски хорошо справиться с тем, что случится, если она сработает Если ему не нужно ничего, кроме него, он должен найти реализацию, которая есть.
DeadMG
@Sjoerd и DeadMG: хотя стандарт позволяет любой функции генерировать исключение, это не означает, что проекты должны разрешать исключения в своем коде. Если вы запрещаете исключения из своего кода, как это делает команда OP, вам не нужно заниматься безопасным программированием. И если вы попытаетесь убедить руководителя группы разрешить исключения в кодовую базу, которая ранее была бесплатной, то будет множество C()функций, которые прерываются при наличии исключений. Это действительно очень неразумная вещь, она обречена на массу проблем.
Мастер
@cmaster Это новый проект - см. первый комментарий к принятому ответу. Таким образом, нет существующих функций, которые сломаются, так как нет существующих функций.
Sjoerd
3

Если вы создаете критическую систему, подумайте о том, чтобы следовать советам руководителя группы и не использовать исключения. Это правило AV 208 в стандартах кодирования C ++ компании Lockheed Martin, разработанных компанией Joint Strike Fighter . С другой стороны, в руководствах MISRA C ++ есть очень конкретные правила, касающиеся того, когда исключения могут и не могут использоваться, если вы создаете MISRA-совместимую программную систему.

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

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

Томас Оуэнс
источник