Коды возврата & исключения

    Замечательные статьи публиковались в последнее время, хотелось бы добавить ещё несколько абзацев по данной теме.

    Уважаемые авторы предыдущих топиков как-то упускали тот момент (или мне показалось? или это само-собой подразумевается?) что exceptions возникли как инструмент для решения весьма утилитарной задачи — передачи управления из места возникновения ошибки в то место, где она может быть обработана.

    Немного истории, чтобы понятно было, откуда такая задача возникла. В каждом более-менее нетривиальном программном обеспечении (сложнее, чем «Hello, world», да) всегда существуют точки, где нормальное выполнение не может продолжаться — I/O подсистема выдала отказ, памяти для алгоритма почему-то не хватило, входные параметры для функции ей не понравились и т.п. Как именно реагировать?

    Ситуация становится ещё печальней, если рассматривать создание какой-либо библиотеки, которая должна будет использоваться в других проектах. Мы не можем (как следствие) вызывать assert/abort или ещё какую-нибудь подобный обработчик — откуда мы знаем, что имеем право завершать работу всего приложения? К примеру, наша библиотека занимается сбором какой-то статистики входных данных, а из-за такого её поведения будет остановлена работа всего устройства. А пишем мы firmware для кардиостимулятора, конечно же.

    Ок, abort() — не годится. Мы не хотим создавать приложения, подобные воздушному шарику — в любом месте уколол, весь шарик умер. Мы хотим пользоваться технологией, которая позволит разделить место, где возникла ошибка, и место где принимается решение о том, что именно мы будем делать с этой ошибкой. Так как для одних применений реакцией будет запрет сбора этой статистики, для других — переинициализация библиотеки (например, с другими параметрами), где-то — просто игнорирование. Так как на более высоком уровне доступно гораздо больше информации о том, как реагировать на возникшую ошибку.

    Как ещё можно сигнализировать “наверх” о наших проблемах? Глобальными переменными типа errno в языке C? Не получится. Возвращаемым значением? Уже лучше, но возникают новые проблемы:
    • на программисте теперь лежит груз ответственности за проверку возвращаемого значения при каждом вызове подобных функций,
    • вся программа на всех уровнях теперь должна поддерживать этот подход (ибо ситуация, когда вызвали функцию из нашей библиотеки, она вернула ошибку, вызывающий это обнаружил и сам вернул ошибку, а на более высоком уровне её прошляпили — скажем так, нежелательна),
    • цепочка вызовов состоит из очень похожих блоков: вызвали функцию, проверили наличие ошибки, в случае её наличия — выходим сами с ошибочным признаком. А если это надо писать постоянно — почему же это не автоматизировано?

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

    Этот инструмент — exceptions. Он естественным образом получается как результат внесения в язык той концепции обработки ошибок, которая была описана выше. У этого инструмента есть свои плюсы и минусы (которые иногда заставляют полностью отказаться от их использования). Более того, «обработка ошибочных ситуаций» это общая концепция, а «обработка ошибочных ситуаций с использованием exceptions» — всего лишь один из примеров её реализации. Обработку ошибок можно делать и не задействуя механизм exceptions, разве что действий со стороны программиста несколько больше потребуется.

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

    PS: ещё было желание показать на примерах кода, какие именно минусы есть у exceptions. И как решалась задача «и ошибки удобно обрабатывать и exceptions при этом не использовать» (актуально для встроенных систем, когда «кардиостимуляторы» пишутся). Но это позже.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 53
      +5
      Насколько понимаю, эту самую мысль доводил Страуструп чуть ли не с самой первой своей книги!
        +5
        Насколько я понимаю механизм исключений появился не просто как средство сказать об ошибки, а скорее как средство принуждающего программиста-пользователя обработать ошибку! Ведь всем известно, что если функция возвращает значение какого-нить типа, то это еще не говорит что это значение обязательно проверят! А так хотя бы безусловный catch(...) поставят, что будет говорить о том, что программист хоть немного но осведомлено об ошибке!
          +4
          В C++ всё-таки механизм исключений, к сожалению, не слишком-то располагает программиста к использованию исключений, в отличии от, например, Java. Всё дело в том, что в Java есть такая вещь как проверяемые исключения.
            0
            Не писал на Java и ничего не скажу про нее. Но если не обработать исключение в коде на С++, то пользователь точно заметит. Вернее с определенной вероятностью в 99%
              +1
              Проверяемые исключения — полубесполезная вещь.
              Все дело в java.lang.RuntimeException от которого наследуется большое число важный исключений, которые в итоге не проверяются. В итоге да, какую-то часть ошибок нас заставят проверить, но RuntimeException это такая дырка, через которую обязательно пролезет что-то лишнее, что обязательно нужно было проверить.

              Не нужно было делать java.lang.RuntimeException непроверяемым или хотя бы запретить наследоваться от него не стандартным классам.
                +3
                Если бы все исключения были бы проверяемыми, Вам пришлось бы проверять во всем коде NPE, деление на ноль, передачу неверных параметров и т.д., даже там, где заведомо известно, что этого не может быть.
                Checked exceptions в JVM используются для того, чтобы сигнализировать об ошибках, возникновение которых мы не можем контролировать. В частности, ошибки ввода/вывода. И это не панацея.
                  0
                  Совершенно верно. Выброс непроверяемого исключения говорит о некорректной работе метода, либо некорректном его вызове (недопустимые параметры). И это является ошибкой программы, а не исключительной ситуацией, и решается элементарным тестированием.

                  Еще есть такая штука как контрактное программирование (http://code.google.com/p/cofoja/). Оно позволяет жестче контролировать корректность вызовов методов и обнаруживать ошибки на ранних стадиях. Каждый метод содержит дескриптор, где указываются ожидаемые значения аргументов и результатов. Там же указываются ВСЕ ожидаемые исключения от метода, как checked, так и unchecked.
                  Кстати, в javadoc для java api также как правило указываются все unchecked exceptions, которые может выдать метод.
                  0
                  Насчет наследования от RuntimeException.
                  Если у вас есть единая точка обработки какого-то вашего исключения, будет очень утомительно во всех методах ваших классов прописывать выкидывание этого исключения.
                  А если возможных исключений порядка 5 (что бывает намного чаще, чем хотелось бы)? Все прописывать в сигнатуры методов? Это был бы громадный шаг к потере читаемости.
                0
                Вообще, catch(...), если посмотреть на него с другой стороны,
                говорит не только о том, что программист осведомлен об ошибке, но и о том, что разработчик полностью
                не владеет ситуацией и не знает о «типовых» для своего кода исключительных ситуаций, затыкая их catch(...).
                Последнее, имхо, применимо в критических участках кода, в процессах, которые не должны никогда падать, а во всех других случаях можно допустить выход исключения в систему.
                По крайней мере, его будет проще дебажить, общаться с удаленным клиентом тоже будет проще, особенно если сравнить со случаем, когда в try catch (...) мы имеем, не культурно обернутый exception, а сообщение «а х** его знает, что там у меня»
                  +1
                  Ну в современном C++ коде почти все исключения все же наследуются от ::std::exception (не видел реальных примеров обратного), поэтому в среднестатистическом main-е большого проекта, использующего множество библиотек все же присутствует catch(exception& e). Перед ним может идти пару специфичных кетчей, но на деле все же именно этот общий будет ловить почти все ошибки, ибо так записывается логика «если вот такие ошибки, мы можем что то делать, в противном случае — записать в лог и умереть».

                  Так что catch(...) и catch(::std::exception&) это плохо только в теории, а так — иногда другого выхода просто нет.
                    0
                    >Ну в современном C++ коде почти все исключения все же наследуются от ::std::exception (не видел реальных примеров обратного)
                    Исключение boost::thread_interrupted. Его таким сделали, чтобы оно не ловилось случайно в user-code. Только самой библиотекой.
                    +2
                    Ну да, это специфический инструмент, и спользовать его надо в соответствующих ситуациях. К примеру — top-level обработчик, который залогирует (возможно — сложным образом) состояние приложения, возможно — выйдет с осмысленным кодом ошибки. А возможно всё же попытается более-менее корректно освободить ресурсы перед смертью.
                –1
                да, именно. в точку!
                  0
                  Эти размышления действительны для всех типов ошибок?
                  Я бы, наверное, назвал такие:
                  1)критические ошибки ресурсов/потов окружения {SIG_ABORT} или сбой чётности памяти, требуют остановить программу
                  2)некритические ошибки ресурсов/потоков, реакция варьируется между игнорированием, ожиданием, ветвлением/диалогом и пр.
                  3)ошибки аргументов к нашей функции, реакция зависит от контекста
                  4)ошибки аргументов к чужой функции {tan(PI/2)} реакция зависит от контекста
                    0
                    >Эти размышления действительны для всех типов ошибок?
                    В первом пункте — ошибки, которые вне модели runtime'а С++, поэтому на уровне С++ их не обработать (сбой чётности, например, — где гарантия, что наш код обработчика уже не испорчен?).

                    Остальные обрабатываются единообразно — либо на данном уровне ясно, что с ними делать, либо — неясно и решать будет предыдущий уровень.
                    +1
                    Копайте в истории дальше. Задача возникла не там. Исключения — это всего лишь способ передать управления на несколько уровней вверх. Все. Обработка ошибок — это частный случай.
                      +2
                      Использовать исключения для «очень удобного» выхода из функции — один из самых худших вариантов их использования.
                        –2
                        ну в плюсах — да
                          +3
                          Везде. И исключение по определению именно возникновение нештатной ситуации. Если проблема только в передаче на несколько уровней — фигня проблема, goto в подходящей инкарнации и все дела.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              Я же и сказал — goto в подходящей инкарнации. НЕШТАТНАЯ СИТУАЦИЯ — ситуация, при которой технологический процесс или состояние оборудования выходит за рамки нормального функционирования и может привести к аварии (БСЭ). Исключение возбуждается, если ввиду какой-то причины нормальное, запланированное поведение программы (выполнение алгоритма) становится невозможным. Нехватка памяти — как раз такая ситуация и ее обработка выходит за рамки самого алгоритма, потому что управлением исполнитель заниматься не должен (опять же, по определению), а должен тот, кто «заказал» выполнение этого действия.
                              Да, «рабочие» функции должны в этом случае бросать исключения. А концептуально они действительно не отличаются от работы с возвращаемыми значениями, это просто механизм более высокого уровня для выполнения той же задачи — и об этом автор пишет в своей статье.
                              > Нет концептуальной разницы между «нештатной» и «штатной» ситуацией
                              Есть для любого организованного технологического процесса. АПП Вам в руки.
                              Управление на основе исключений — дурной тон в любом языке программирования. Если оно необходимо — значит была допущена ошибка при проектировании системы.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  +1
                                  Вы вообще-то что-то крупнее «hello, world» программировали? Где есть больше одного модуля и надо разграничить области видимости и функциональные компоненты? Если я пишу библиотеку с заданным функционалом — я не могу в ее «рабочих» функциях держать «все время держать в голове архитектуру всей программы» — она мне просто неизвестна. Зато я могу сказать, что есть ряд возможных ситуаций, когда нормальное выполнение алгоритма невозможно. Это как раз и есть «нет записи в таблице», «невозможно подключиться к узлу», «недостаточно свободной памяти». Таких ситуаций всегда ограниченное количество и пользователь моей библиотеки узнает об их возникновении как раз отлавливая бросаемые исключения — сигналы о возникновении проблемы.
                                  > Ну и в качестве дополнения…
                                  Если Вы можете (архитектура допускает) — то ради бога, делайте все проверки и соответствующие действия в рамках одного блока программы. Исключения тут и не нужны. А если не можете? Функции f1 не хватило памяти для работы, всю память съела функция f2. Так что, будем организовывать очистку памяти f2 из f1? Наверное нет, мы сообщим «f1 не хватает памяти» тому, кто ее вызвал, а тот уже пусть принимает решение, освобождать занятую память или нет. Это не разработчика f1 задача. Его задача — сказать «я предвижу такую, такую и такую ситуацию, когда функция не сможет работать нормально».
                                  Управление исключениями — это когда без возникновения нештатной ситуации кидается исключение вместо возврата соответствующего результата функции. Это — дурной тон. Welcome to Real World.
                                    0
                                    Если кидается исключение, значит функция не может подготовить результат.
                                      0
                                      Это же очевидно, что вы хотели этим сказать?
                                        +1
                                        Что это не дурной тон по мнению alexkolzov
                                          +1
                                          Верно. Если функция не может выполнить свою задачу — кидается исключение, которое может быть обработано там, где это приемлемо с точки зрения архитектуры системы. Это не дурной тон, а прекрасная практика. Дурной тон — перекидывать управление с места на место путем возбуждения исключений там, где реальной исключительной ситуации нет.
                                            +1
                                            Можете привести пример, где дурно использовать исключение? Пример штатной ситуации.
                                              0
                                              Приведу слегка утрированный.

                                              def method(self, i_value) :
                                                  if isinteger(value) :
                                                      raise IntegerValue(i_value)
                                                  else :
                                                      raise NotIntegerValue(i_value)
                                              


                                              Выглядит довольно глупо, но что-то наподобие этого мне приходилось видеть неоднократно, когда пытались сделать разделение обработчиков различных результатов функции. Здесь нет никакой особенной, ошибочной ситуации а исключения используются как способ передачи управления. Вот это — дурной тон.
                                                +1
                                                Действительно, есть функции, которые знают о «нештатной» ситуации, сами обрабатывают её и выдают ожидаемый результат. Кидать исключение функция не должна. Например, функция фильтра значения, результат которой – отфильтрованное значение. Но на уровне выше бывает важно учесть, как функция отработала, например, учесть, что начальное значение было неверным. Здесь покажется, что удобней использовать коды возвраты, но их не будешь возвращать вместо значения. Тогда либо last_error() или через аргумент, переданный по ссылке (указатель). Но код ошибки мало информативен. На уровне выше нужно тогда знать все возможные коды ошибок. И здесь, я думаю, удобней возвратить вместо кода объект исключения (не кидая его), содержащий в себе всю информацию о «нештатной» ситуации. И тогда очень просто на уровне выше, либо строим логику, учитывая код/класс исключения, либо уже выкидываем это исключение.
                                                Что скажете?
                                                  +1
                                                  Извиняюсь, поправлю себя. На уровне выше кроме кода часто нужно знать дополнительную информацию об ошибке — сообщение, причину ошибки и др.
                                                    0
                                                    В таких ситуациях, конечно, можно соорудить объект-обертку возвращаемого значения, из которого можно получить информацию о протекании процесса его вычисления, в том числе возникшие проблемы. Я не считаю этот подход удачным, поскольку в функции происходит что-то вроде «сделаю так, как смогу, а там, выше, пусть разбираются, на сколько это правильно». То есть возможно проведение ненужных вычислений. Тут надо либо полностью доверять функции вычисление значения, либо сообщать об исключительной ситуации в момент ее возникновения и доверять принятие решения вызывающему эту функцию. Но такой подход имеет право на жизнь. И все-таки это не «бросание» исключений, не управление исключениями, а всего-лишь специфический тип возвращаемого значения.
                              0
                              Хм… А где — нет?

                              В том же python'е, к примеру, StopIteration используется для завершения цикла, но даже там прямо сказано «This is derived from Exception rather than StandardError, since this is not considered an error in its normal application». И уж точно он не используется для «передачи на несколько уровней вверх».
                              • НЛО прилетело и опубликовало эту надпись здесь
                          +3
                          Где-то год назад Страуструп проводил лекцию в Москве для разработчиков Parallels. Сам я на ней не был, поскольку к тому времени там уже не работал, но рассказывали бывшие коллеги. Ему задали вопрос, что больше всего ему не нравится в С++ (очевидцы, поправьте меня, если я перефразировал не корректно). Он дал лаконичный ответ: «Exceptions».

                          Что это означает и почему он так считает, я не знаю. За что купил… :-)
                            0
                            Он такого не мог сказать, по крайней мере весь материал строится на том, что нужно и полезно юзать исключения, а не коды ошибок в виде возвращаемого значений. Единственное что он говорит по поводу исключений в С++, что могло бы хоть как-то навести на мысль что он против С++ исключений, так это «некоторые программисты находят удобным разделение на logic_error, runtime_error, я нет»! Если бы он был против исключений, уверяю, его харизмы хватило бы на изменения в стандарте!
                              0
                              Не хватило бы. По причинам обратной совместимости
                                0
                                Ну выкидывание происходит не за один присест, а поэтапно! С надписями depricated и другими уведомлениями.
                                  0
                                  Это же не Java, где Oracle могут диктовать свои условия. В C++ такая фишка не прокатит. Может и планируется когда-нибудь в отдаленном будущем в десятом стандарте исключить исключения (простите за каламбур), но скорее всего их оставят как элемент языка даже если Бьерн будет против.
                                    0
                                    Да неужели, а ::std::auto_ptr уже стал deprecated.
                                      0
                                      Не путайте, пожалуйста, элемент библиотеки, пусть даже стандартной, с конструкцией самого языка. Это разные вещи. Первое вывести из эксплуатации гораздо проще
                                        0
                                        Ну хорошо, а изменение семантики ключевого слова auto?
                                          0
                                          Насколько я помню, auto как модификатор класса памяти стоял у всех локальных перемен по-умолчанию и на практике использовался крайне редко. Так что это вполне безболезненное изменение.
                              +3
                              Я думаю, что он всё-таки имел в виду не то, что ем не нравятся исключения как концепция, а то, как они реализованы в C++. В плюсах действительно механизм exceptions не очень логичный — в качестве исключений можно бросать объекты по значению, по ссылке, по указателю, типы объектов исключений тоже могут быть любыми. Всё это вместе означает, что для того, чтобы всё обработалось везде как нужно, чтобы не возникло потери информации в исключении (какие-нибудь «срезки» объектов), чтобы нигде не произошло утечки памяти от программиста требуется либо очень внимательное программирование, либо самоограничение в использовании доступных механизмов exception. Например, часто настоятельно советуют перехватывать exceptions исключительно по константой ссылке, считается, что это наиболее оптимальный вариант перехвата, но сам язык этого не диктует.
                                0
                                Нет, это он не имел в виду.
                                Этот вопрос ему в том году задавали, когда он выступал в Бауманке (просто помещение, так это была конференция за 5к) — дескать, почему не заставить всех наследовать исключения от ::std::exception. Ответ был универсальным — «зачем? хочется — введите coding standard, а так прелесть плюсов в том, что все можно и каждый сам задает свои ограничения».

                                К сожалению аналогичный ответ был и на вопрос «почему всего 5 типов итераторов и они не разделяются на только чтение, только запись», и на вопрос «почему с++ не заботится о переполнении стека, столько ж механизмов напридумывали — стандартизируйте уже наконец!».
                                  0
                                  Ну тогда вообще сложно сказать, что он имел в виду, потому что во всех интервью он говорит, что считает exceptions полезным инструментом.
                              0
                              Вам бы познакомиться с обработкой ошибок в эрланге =)
                                0
                                Не знаю почему минусуют. Но как раз в эрланге основной принцип: «не можешь сделать — умри» плюс гибкая система супервизоров и апгрейда на лету и есть более надёжная альтернатива «распространению ошибок» по-всему приложению. Вообще эрланг это скорее язык управления, а не вычислений. К сожалению в С/С++ такое применимо только в специфических приложениях.
                                0
                                Единственный важный момент про C++ и исключения, особенно в контексте библиотек:
                                Не везде исключения можно использовать. Например в исключения не должны пересекать границы Dll-ки почти никогда. Поскольку их будет возможно обработать только из плюсов.
                                Мне кажется разумным правило никогда не использовать исключения в случае если нужна бинарная совместимость с дргими языками/компиляторами и отдавать им предпочтение во всех остальных случаях.
                                  0
                                  В мире не только dll бывают, а это ещё веселее. Поэтому для кроссплатформенных прог есть шанс рано или поздно наткнуться на невозможность юзать исключения.
                                    0
                                    Вообще говоря, я согласен. Но это может быть связано только с поломанным компилятором. Я прямо сейчас колбашу кросплатформенный проект на более чем полтора десятка разных платформ и на более чем 5 архитектур процессора. И мы приняли решение перейти к исользованию исключений повсеместно.
                                    Поскольку у нас сложилось впечатление, что чинить поддержку исключений часто бывает намного проще, чем поддерживать коды возвратов для большОго количества подсистем и архитектур.
                                    Но это такое. Каждый выбирает инструмент на свой вкус. Главное что бы выбор был обоснован и следовали этому выбору все.
                                  0
                                  Очень интересное обсуждение по теме (eng): discuss.joelonsoftware.com/default.asp?joel.3.125056.21

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

                                Самое читаемое