Pull to refresh

Comments 159

думал, что сперва в статье вы запоёте очередную песню про «полезность» кодов ошибок, но прочитав увидел мнение с котором сам согласен. В ООП нужно пользоваться только исключениями. Кстати, мне сказали, что исключения тормозят систему… я решил проверить скорость обработки 10000 подряд вызванных исключений vs кодов ошибок. конечно коды работают раз в 100 быстрее(на .net пробовал), но вот показатель 0,0001с vs 0,01с для 10000 брошенных подряд случайно сгенерированных исключений — это весомый аргумент за использование последних без оглядки на «упавшую» производительность.
Я в целом придерживаюсь мнения, что оптимизировать стоит после того как сделан прототип и профайлер определил место где у нас тормозит. Если тормозит. Большая же часть кода который мы пишем не очень нуждается в оптимизациях и даже переживает сложность O(n**2) :).
>> Кстати, мне сказали, что исключения тормозят систему…

очередная экономия на спичках!
Исключения в С++ не бесплатны. В случае, когда они не выбрасываются разница в производительности незаметна, но если они будут выбрасываться очень часто это может сказаться на производительности. Поэтому есть хорошее правило — исключения должны выбрасываться в исключительных случаях.

Проверено на опыте кстати. Xerces-C использует исключения для сигнализации об ошибках, так вот у нас парсились немного невалидные xmlки. Xerces-C парсер бросал исключение и сам же его ловил на пару фреймов ниже по стеку. Парсились они очень часто и после того, как xmlку подправили, кусок стал выполняться в разы быстрее, даже на глаз было заметно, без всяких профайлеров.
Может этот парсер сам по себе кучеряво написан? Вы меня уж извените но что бы тормозить при парсинге xml файлов, это надо постараться…
Ну можно сказать, что и кучеряво. Вообще-то сам по себе такой частый парсинг xmlок это не очень хорошо, но в итоге получилось то, что получилось. Использовались бы там коды ошибок всё бы летало и на невалидных xmlках.
у нас такое было в нашем же коде — функция возвращала точку пересечения двух прямых. Исключение кидалось если они не пересекались. Код вызывался для прорисовки разных объектов на экране — и пока там были исключения тормозил все дело так, что водить мышкой было неприятно ( ощутимые задержки ). Коды возврата полностью убрали тормоза.
С тех пор очень настороженно отношусь к исключеням.
Так про это же и говорят — начинайте оптимизировать, если видите проблему, а пока проблемы нет пользуйтесь исключениями. Сколько можно одно и тоже-то повторять…
А не будет ли слишком дорого избавиться от исключений в своем проекте, когда проблема все-таки возникнет? Может быть, если видно, что код должен быть быстрым — сразу писать без них?
Вы сделали выброс исключения частью логики своей программы. Они же должны обрабатывать только ошибки (исключительные ситуации).
Удивительно, но наличие в C#-функции блока try/catch замедлило скорость работы этой функции в 4 раза — притом, что сам блок находился в неисполнявшейся части кода. Я сравнивал функции


int f1(int k){
  if(k==0){
    try{ File.Delete("tmp.tmp"); }catch{}
  }
  return 2*k;
}

int f2(int k){
  if(k==0) File.Delete("tmp.tmp");
  return 2*k;
}

Передавались ненулевые значения параметра.

Но главный недостаток использования исключений в «штатном» выполнении программы — они очень мешают отладке. Сейчас мне отладчик сразу показывает, где объект оказался null, а когда в программе использовались исключения, то отладчик на них все время спотыкался.
Действительно, а как можно прямым способом обнаружить источник исключения? То есть без построения предположений о том откуда оно могло взяться, а именно точно знать откуда. Особенно, если мы его не сами выбрасываем и точку останова нельзя поставить.
Что-то в дебаггере Visual Studio 2010 Express я этого не находил.
Насчет Express не знаю. В Visual Studio 2008 делается так: Debug — Exceptions — ставим галку в Common Language Runtime Exceptions/Thrown. Остановится в точке нашего кода, в которой находится вызов или операция, вызвавшая исключение.
> В ООП нужно пользоваться только исключениями.

Не всегда и не везде:
today.java.net/article/2006/04/04/exception-handling-antipatterns#antipatterns
stackoverflow.com/questions/2870701/risking-the-exception-anti-pattern-with-some-modifications
www.rockstarprogrammer.org/post/2007/jun/09/java-exception-antipatterns/
etc

> Кстати, мне сказали, что исключения тормозят систему…

В целом, они отрабатывают медленнее. Но, стоит ли гнаться за этими миллисекундами, если bottleneck в другом месте?
Не всегда и не везде:
today.java.net/article/2006/04/04/exception-handling-antipatterns#antipatterns
stackoverflow.com/questions/2870701/risking-the-exception-anti-pattern-with-some-modifications
www.rockstarprogrammer.org/post/2007/jun/09/java-exception-antipatterns/
etc

По ссылкам 1 и 3 — описание, что не стоит делать, при использовании исключений, а не примеры, почему исключениями иногда не стоит пользоваться. 2 — оно как бы не совсем про ООП.
> По ссылкам 1 и 3 — описание, что не стоит делать, при использовании исключений, а не примеры, почему исключениями иногда не стоит пользоваться.

Там есть замечательные примеры с пробросом исключений.

> 2 — оно как бы не совсем про ООП.

Оно легко адаптируется к использованию с ООП. Не нашел толкового примера на C++, но думаю мысль и из этого понятна.
Я к тому веду — что не стоит пихать исключения везде, часто можно (и правильнее) обойтись и без них.
Есть очень простой универсальный критерий, выводящийся из самого смысла понятия «исключительная ситуация» — используйте исключения только для сбоев.
Исключениями удобно делать что-то такое:

public Person authPerson(int id) throws NoSuchPersonException {
Person person = new Person(id); // бросаем NoSuchPersonException если пользователя с таким id нет
person.applySomeRulesOrSomething();
return person.auth();
}

Возвращаем объект Person если все ОК, и null если не прошла авторизация (в т.ч. у пользователя не хватает пермишенов).
Врядли это можно назвать сбоем, а вот исключительной ситуацией — вполне.
По мне — это именно сбой. По крайней мере на том уровне, где идет проверка всего необходимого для авторизации. Так как если у пользователя нет права на авторизацию — то в идеале и доступ к попыткам авторизоваться должен быть недоступен.
Общий принцип: если нажатие на кнопку ведет к посылу на предмет нехватки прав, то кнопка должна быть неактивной.
Имхо, сбой — это нештатная ситуация. Деление на ноль, ошибка преобразования строки в число, ошибка соединения клиентской части с серверной, итд.
В данном случае отбой при недостатке прав — это ожидаемая, штатная ситуация.
Деление на ноль, ошибка преобразования строки в число — вполне себе штатные ситуации при разборе пользовательского ввода и использовании его для вычислений.
Отбой при недостатке прав как штатную ситуацию я бы посчитал недостатком проектирования — возможностью послать заведомо невыполнимую команду. Например, в вашем случае добавил бы функцию безопасной проверки наличия права на авторизацию, по которой давал бы пользователю доступ собственно к команде «Авторизовать». Или счел бы сбоем организационного уровня — а почему это вдруг пользоваетль с недостаточными правами хочет странного?
> Например, в вашем случае добавил бы функцию безопасной проверки наличия права на авторизацию, по которой давал бы пользователю доступ собственно к команде «Авторизовать».

Следует заметить, что во-первых, пример был взят исключительно, что называется, с потолка.
И во-вторых — мысль была такая —
— Есть id некоего пользователя.
— Проверить, имеет ли этот пользователь доступ к неким «продвинутым» фичам.
— Если имеет — авторизовываем в вызвавшей подсистеме.

Авторизация — это собственно и есть способ получить данные пользователя, проверить их валидность и определить что пользователь может а что не может.
Ну и в третьих — это имеет весьма косвенное отношение к обсуждаемому вопросу.
Собственно, тут можно обойтись и без исключений, с обычными кодами возврата. Но это, на мой взгляд не так удобно.
В динамически-типизированом ООП код возврата типа «никакой объект» более, чем вполне уместен (если невозможность вернуть объект — «ожидаемая» ошибка в логике той функции, котрая возвращает null)
Ну это я отвечал на «В ООП нужно пользоваться только исключениями».
Но так возбудилсо, что забыл поставить цитату :)
По моему опыту использовать в таких случаях null — явный антипаттерн, приводящий к гекатомбам лишних проверок с одной стороны и ошибкам периорда выполнения в местах, далеких от ошибок кода — с другой. Можешь не вернуть результат — пиши if (TryGetResult(Result))… — и всем все будет ясно уже по сингнатуре. Не зря Хоар само введение null считал одним из самых дорогих решений в IT.
> 0,01с
Вообще-то 10мс это дофига, даже для .net. Вы что-то неправильно намерили.
А теперь представьте что штатное боевое время работы вашей программы — суток так 3-4.
Если 10000 исключений вызываются за 0,01 с, то представьте сколько исключений может вызваться за суток так 3-4.
Что если по большей части программа будет вызывать исключения и обрабатывать их (будет абсолютно правильно с точки зрения логики) вместо того чтобы возвращать коды возврата. Тогда время работы 3-4 дня превратится в 4+ дня.
Думаю время работы не совсем правильный аргумент.
Конечно это не относится к программам которые работают 3-4 дня из-за того, что по минуте ждут ответа от сервиса.
Перепроверил измерения, у меня получилось 20-25мс на выброс и отлов 1000 исключений.
ИМХО конечно, но это слишком много. Это ведь не просто 25 мс, а полная загрузка процессора(если быть точным то 1го ядра) и в этом время ничего не происходит.
То есть если взять средненагруженный сервер где сидит хотя бы сотня пользователей одновременно и представить что программисты кидают исключения в любой нестандартной ситуации — на мой взгляд пользоваться этим сервисом будет попросту невозможно.

:) Первый пост
А откуда у вас 1000 исключений при штатной работе сервиса?
А при слегка нештатной значит можно уйти в бесконечный вис? :)

А если серьёзно — разговор в топике вроде был о полном отказе от кодов возврата и как следствие от гибридной схемы тоже. Значит многие операции будут возвращать не null/false — а простые исключения. Я уверен что при переходе на схему чистых исключений, оных будут сотни и тысячи.

Как частный пример приведу Dictionary.TryGetValue() или Int32.TryParse() которые возвращают коды ошибок(bool). Для них есть варианты с исключениями — для Dictionary — индексатор, а для Int32 — Parse(). Вот вторые и породят тысячи исключений.

Если мне не изменяет память микрософт в гайдах для .NET рекомендует использовать исключения только в ИСКЛЮЧИТЕЛЬНЫХ ситуациях(по первой просьбе найду пруфлинк).

Я сам дотнетчик, поэтому говорю только за него.
А что значит «слегка»?
Полный отказ от кодов возврата <> полный отказ от кодов ошибок.
Возвращать false ИМХО можно и нужно, если это часть штатной логики работы программы и надо бросать исключение, если это сбой.
Поэтому GetValue и TryGetValue нисколько друг другу не противоречат, а только дополняют: для второй функции неудачный разбор сбоем не является!
Вы зря дискутируете со мной насчёт кодов возврата :) Я ведь с вами согласен что исключения нужны кидать в ситуации сбоя.

Это идея топикстартера о том что приложение не должно пользоваться кодами возврата, а только использовать исключения для ситуаций когда метод не смог выполнить задачу.
Я так понимаю что случий с Int32.Parse как раз то чего желает топикстартер. Я же никогда им не пользуюсь, и предпочитаю TryParse.
я думаю, что вы не правильно поняли топикстаретра.
Коды возврата — это не тоже самое, что возврат null или true/false.
Пример: если строка в подстроке не найдена, то это false, а не исключение.
А если в на вход функции пришёл null вместо строки которую будут искать, то это исключение, а не null или false. И уже тем более не "-1"

Пример2: из базы получили ВСЕ объекты из списка ID. ( Select * from...where id IN {1,2,3,4} )
Так вот, если метод не нашёл объекты, то он возвращает пустой список. А если он нашёл только 3 объекта из 4, то он возвращает исключение, т.к. это не штатная ситуация — в базе нет объекта, id которого у вас каким-то образом появился. Для нормальной бизнес-логики это не нормально. Если бы тоже самое было не по ID по stateId или Amount, то исключение кидать не стоит, а просто вернуть 3 объекта.
Полагаю, что TryParse ничуть не противоречит идее топикстартера. Как видно из самой сигнатуры, задача метода попробовать разобрать строку и вернуть явный флаг — ее он успешно и выполняет. Ошибкой неудача разбора здесь не является, а значит, исключений, по идее автора, использовать не нужно. Пример — ошибка компиляции не является в общем случае ошибкой компилятора.
Не в тему:
Напишите пожалуйста пост про константность, ту что делится на «физическую» и «логическую». Потому что это скользкий и очень важный момент при разработке на С++. Только прошу написать более развернутый пост, чем на rsdn.ru
Это чудовищно сложный вопрос :(.
Но за идею спасибо. У меня есть ряд наработок, как раз недавно обнаружил что на примере implicitly shared контейнеров в Qt достаточно просто показать зачем на самом деле нужно ключевое слово const.
Я попробую написать, но сами понимаете — это далеко не на один день развлечение. Эта статья, например, писалась три дня О_О.
>>на примере implicitly shared контейнеров в Qt достаточно просто показать зачем на самом деле нужно ключевое слово const.
Это же реальный опыт!!! Это же самое интересное! Именно такие обсуждения и следует вести на хабре. Очень надеюсь, что вы найдете время на подобный материал!
Первые реализации исключений, особенно в C++, были с фатальными изъянами. Выбрасывание исключения в конструкторе приводило к тому, что деструктор не вызывался.

А что, не в «первых» реализациях деструктор вызывается? И в чем изьян? Что вы собираетесь разрушать?
UFO just landed and posted this here
И что? Пусть генерируются. Что вы собираетесь разрушать? Несконструированный объект в data members которого похозяйничал великий рандом? И разве этот «недостаток» исправили? Или кто-то тут ошибся, или та бабочка, которую я раздавил во время вчерашней экскурсии в прошлое, все таки, на что-то повлияла.
UFO just landed and posted this here
Как я уже написал выше, это все правильно с точки зрения логической целостности языка — но неудобно для программиста, если исключение застало нас посередине конструктора, когда мы уже выделили ряд ресурсов.
Для этого в современном C++ есть все средства для RAII
UFO just landed and posted this here
Отвечаю по порядку.
1. Нет, не вызывается. Чего не скажешь о реализациях в других языках, например herbsutter.com/2008/07/25/constructor-exceptions-in-c-c-and-java/
2. Изъян в том, что требует написания большого количества «инфраструктурного» кода, никаки н связанного с логикой приложения а требуемого только на поддержание целостности архитектуры.
3. Если у нас сложный класс, объект которого в конструкторе инициализирует ряд ресурсов — то мы хотим, чтобы в случае исключения в середине инициализации мы могли относительно легко разрушить уже созданное. А в C++ нам приходится вместо деструктора использовать «умные» указатели и RAII — для полей класса деструкторы все же вызываются :).
Язык такой. Или автоматическая инициализация нулями потребуется, потому что вы не сможете в деструкторе определить что удалять. Но это противоречит идеологии С++ — ты не платишь за то, что не используешь Плюс надо будет удалить из языка ссылочные типы, которые не могут быть неинициализированными. В общем, в С++ очень хорошая для С++ реализация исключений, разве только finally забыли.
Я в целом не спорю, что для своего времени реализация была очень неплохая. Но, как я уже писал выше — не без… хммм… косяков :). Программисту же что надо — ему надо чтобы было удобно. А как это «удобно» пересекается с синтаксисом и концепциями языка для программиста дело десятое. И нет ему дела до того, что исключение во время исключения вызывает логическую неопределенность — у него клиенты на телефоне с программой которая «просто выходит и все».
Не-не, об этом можно говорить только если рассматривать сферические исключения в вакууме. Есть язык, в который интегрировали фичу и притом достаточно хорошо и красиво. Все очень даже неплохо. Вы же не станете наезжать на С++ за то, что он не функциональный или за то, что он не интерпретируемый. Смысл в таких наездах? Ровно так же упреки насчет фатальных изъянов смотрится. В чем фатальность? Проблемы решаются довльно просто: вы уже упомнянули RAII, а семантика игнора — это catch(expectable&){}. На мой взгляд такой винегрет получился как раз из-за втрого пункта — из-за самих разработчиков, которым не объяснили (правда, кто должен был это делать?) и из-за legacy кода.
Убедили. Поменял формулировку на более нейтральную. А catch(expectable&){} работать не будет, потому что разработчикам забыли сказать про то, что нужно выделить expectable :). В Java это сделано, на мой взгляд, удобнее с прописыванием что метод может кидать в каких случаях.
Да, в других языках есть много удобных фишек, которые делают использование исключений и более приятным, и вообще не игнорируемым.
Насчет expectable. Я немного другое имел в виду. Не то, что надо какой-то expectable вводить, врядли при написании функции всегда можно оценить что есть expectable, да и не совсем это правильно смотрится. Просто в вызывающем контексте таким образом можно игнорить некоторые ошибки или группы ошибок с общим предком.
Проблема в том, что для C++ это потребует писать немаленькую конструкицю try + catch в каждом таком участке, что собственно мы и видим в большом количестве кода. А в случае с возвращаемым значением мы можем просто ничего не писать (это для примера, я на самом деле не знаю как можно хорошо реализовать подобную семантику).
Есть такое. Я как-то испробовал другой подход — практически бинарный — есть ошибка, нет ошибки. Т.е. функция может либо вернуть что-то, с чем можно дальше работать, либо все пропало. Вы знаете, очень неплохо получилось и вполне себе функицонировало. Но, наверное, не для всех классов задач такое подходит.
UFO just landed and posted this here
Тут немного некорректный код :). По феншую должно быть вот так:
Foo() :
  data( 0 )
{
  // ... развлекаемся
UFO just landed and posted this here
Обнулять примитивные типы в конструкторе — это не усложнение, это базовая практика для C++ :)
UFO just landed and posted this here
А в чем проблема-то? delete на 0 в C++ работает корректно, насколько я помню — ничего не делает.
UFO just landed and posted this here
Да, сначала вызовется у file, затем у data, но проблемы в приведенном коде нет. Если File::File выбросит исключение, то Foo::~Foo вызван не будет, соответственно delete не вызовется с каким-то мусором, если вы об этом.
UFO just landed and posted this here
Никто не спорит что оно логичное. Оно — неудобное :)
Не получится. Современные компиляторы на попытку задекларировать в одном порядке а в конструкторе инитиализировать в другом скажут варнинг ^_^.
Так не пишет ни один вменяемый C++ программист, к чему приводить надуманные примеры?
UFO just landed and posted this here
Ну мы же тут говорим о хороших практиках :) Я тоже много всякого повидал, но писать то можно более безопасно и красиво. Тем более, в С++, безопасность и красота кода идут рука об руку
UFO just landed and posted this here
Ничего. :) Деструктор не вызовется.
struct Foo
{
  boost::scoped_array<char> data;

  Foo()
  {
    File file("somefile");
    data.reset(new char[file.length()]);
    file.read(data.get(), file.length());
  }
};

Хотя бы так. И деструктор не нужен.
UFO just landed and posted this here
Да, компилятор создаст тривиальный деструктор, в котором будут вызваны деструкторы всех членов класса и базового класса. Но я не понимаю, какое это имеет отношение к приведенному примеру кода — в нём, даже если File::File бросит исключение, всё останется в валидном состоянии и память нигде не утечёт.

Про вызов деструкторов несконструированных мемберов вы, мягко говоря, неправду сказали.
UFO just landed and posted this here
Это было бы удобно. А костыль прикрутили бы в другом, не так часто используемом месте. В любом случае, я согласился что формулировка чуток резковата и исправил ее на более нейтральную :).
Теперь понятно. Я почему-то думал, что вы утверждаете, что деструкторы будут вызываться, а вы просто привели пример показывающий, почему их вызывать не надо. Или я опять вас неправильно понял?
Но разве правильно запрашивать ресурсы прямо в конструкторе? Если просто логически разделить конструкцию класса и его инициализацию — проблема исчезнет.
Правильно в том случае если классу эти ресурсы необходимы для работы. Например если у нас класс файл — то логично в конструкторе открыть этот самый файл, а если класс сокет — то создать объект сокет.
Так мы гарантируем — что если объект класса создан — то он рабочий. Иначе же придется каждый раз писать
Socket s;
if(s.is_valid())...


что сводит С++ к Си с классами. Можно конечно писать и так — но зачем если есть более удобные и логичные способы.
Ну и ради бога — вызывайте инициализацию сразу после конструктора, дождитесь её конца (как вы до этого собирались ждать окончания инициализации в конструкторе) и пользуйтесь безо всяких is_valid().
Про плюсы говорить не буду, но в Delphi работает так:
1. При создании память объекта заполняется нулями.
2. При выбросе исключения в конструкторе автоматически вызывается деструктор.
3. Деструктор умеет уничтожать недосозданный объект (благодаря пункту 1).
В результате бросать исключение в конструкторе можно без всяких ограничений.
>>деление на ноль… все это может случиться по разным причинам, начиная от вышедшего из строя железа и заканчивая вирусами в системе, но как правило от нас и нашей программы это не очень зависит.

Уж деление на ноль зависит от программиста напрямую, он просто обязан это предугадывать. По крайней мере именно этому нас учили с первого курса университета
Оно может произойти из-за срыва стека, битой памяти, в чужй библиотеке :). Обртите внимание, что я эти шибки рекомендую классифицировать как неожиданные и при встрече с ними в production честно падать. Потому как это будет либо ошибка в нашей логике либо что-то из вышеперичсленного, продолжать работать мы после такого не хотим.
Я могу сказать почему коды выжили в PHP. Потому что нет плагина для IDE, который может статически проанализировать код и ругнуться, что ты мол не поймал такой-то Exception. А плагина в свою очередь нет, так как это очень не тривиальная задача проанализировать интерпретируемый код с нестрогой типизацией.
Для питона тоже такого IDE нету.
Однако исключения используются весьма широко и ловко (например, для обозначения конца списка в итераторе)
Если для питона разработан набор методик, то это хорошо. В принципе в PHP мы тоже используем локальные исключения, но прокидывать их далеко опасаемся.

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

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

Разработчик библиотеки должен как минимум документировать те исключения которые кидает его код.
А при использовании третьих библиотек прочитать в документации какие исключения могут вывалиться оттуда (и подумать что с ними можно сделать).

Если так не сделано, то не стоит называть это библиотекой и темболее куда-то интегрировать.
А автор таки либо разгильдяй, либо не умеет читать.
>> Если так не сделано, то не стоит называть это библиотекой и темболее куда-то интегрировать.
Очень правильные пафосные слова, но…
Мы не живём в идеальном мире. Мало что хорошо документировано. Даже nginx, например, так что не использовать его теперь что-ли.
Неужто в мире php настолько мало хорошего кода, что приходится юзать поделки и изучать их методом тыка?

А что в nginx недокументировано?
>> Неужто в мире php настолько мало хорошего кода, что приходится юзать поделки и изучать их методом тыка?
Отнюдь. Наиболее часто сюрпризы с Exception выдаёт ZendFramework, а это далеко не поделка.
Я просто пытаюсь объяснить, что отказ от эксепшенов чаще проще, чем их использование.

>> А что в nginx недокументировано?
Плохая английская документация, как минимум. Плюс навигация по документации не очень. Плохо структурировано.
Слышал, что google против исключений в C++. Почитал мануал — оказалось что они, в целом, за исключения, но не используют их т.к. не хотят вставлять обработку исключений в свой старый код.
Не могу сказать, что это хороший пример. Вся выгода — только в том, что не нужно явно писать try/catch (один раз!), да сам принцип подходит только для случая, когда метод ничего возвращает. Более того, в данном коде при вызове copyRange(...) внутри getReadme(...) произошло неявное глушение исключения, что трудно назвать здравой идеей. В общем, не убедили.
Судя по всему, ваш браузер не поддерживает тэг «сарказм». Рекомендую обновиться до последней версии или перейти на более современный.
Всегда пологал, что пользоваться кодами возврата в высокоуровневых языках для обработки ошибок — больше похоже на «концепцию magic numbers». Сам использую исключения (в общих чертах) примерно так:

# все ожидаемые ошибки
class AppError < StdError
end

# более точное описание
class IntegrationError < AppError
end

Возможно, автор может написать небольшой обзор конвенции, к которой он пришел и пользуется. Интересно услышать мнение. Спасибо.
Автор старается использовать что лучше подходит для решаемой задачи :). Автору приходится решать много разнообразных задач, начиная от драйверов на чистом C и заканчивая DSL на ruby :(
Тем более есть о чем написать. На примерах.
Кстати говоря, например, на рельсах при сохранении в AR есть такая отличная возможность выбирать метод save или save!, что дает возможность выбрать вариант возвращения ошибки. Если нужно обрабатывать возможно неудачный результат работы, то предпочитаю save, а если результат сохранения будет ошибочным только в экстренном случае, то использую save! и в таком случае уже обрабатываю как исключение и заношу в таблицу исключений в БД.
Это еще один хороший пример того как можно комбинировать и то и другое.
В реализации исключений на С++ есть другой фатальный недостаток — это нечёткая спецификация ислучений в функциях, вот корень всех зол. Если у функции специфицированы исключения, то это не даёт никаких гарантий что она бросает только их.
Если у функции специфицированы исключения, то это не даёт никаких гарантий что она бросает только их.

Вообще-то по стандарту даёт (15.4/9), просто большинство компиляторов этот пункт стандарта не реализует к сожалению.
На мой взгляд с сиключениями две проблемы, из за которых их осознано или не очень избегают:
1. Они дают рваный поток выполнения. То есть по коду (даже в статически типизированных языках) нельзя сказать куда улетит брошенное нами исключение и что может вывалится из того, что мы вызвали. Код оказывается между молотом из исключений которые бросили в него и наковальней из интерфейса который он должен поддержать.
2. Даже в статически типизированных языках с поддержкой checked exceptions язык практически не помогает в какой-то спецификации обработки ошибок. Дело в том что реальный путь исключения по стэку (с.м. п. 1) определяется не статическим описанием программы (сигнатуры методов с исключениями) а её динамической структурой (кто какие реализации вызывает).
3. Предыдущие два пункта дают совсем прохой синергетический эффект — по коду трудно понять начальный замысел автора по обработке ошибок. А если чего-то не понимаешь, то это и легко разрушить.
В общем по-моему история распространения исключений по разным языкам — это ещё один пример того, как добро выдернутое из контекста обращается во зло.
Блин увлёкся. Это я к чему начинал… В сообществе функциональщиков есть интересные наработки ([1], [2]) по обращению с ошибками, счетающие в себе явность потока управления и возможность не думать об ошибке пока не захочется. И кажется эти наработки потенциально портабельны в промышленные языки и применимы.

P.S. Я знаю ссылки ужасны. Поверьте, это всё можно изложить ещё в 10 раз менее понятно.
Когда начинал изучать Яву, показалось, что там слишком много исключений — там, где можно было бы обойтись кодами возврата (открытие несуществующего файла — к примеру). По прошествии… пяти? лет — все еще считаю, что исключений слишком много ;) Хотя сам активно их использую…
В Java своя особая проблема — checked exceptions — когда компилятор заставляет программиста вставлять блоки обработки исключений в принудительном порядке. Сама по себе идея не такая уж и страшная, но очень уж много методов в стандартной библиотеке языка злоупотребляют ими. В результате такая простая задача, как скопировать содержимое файла в другой, превращается в огромную простыню catch-блоков, в порядке которых и уровнях их вложенности очень легко запутаться неподготовленному человеку.
Да, согласен. Было бы хорошо, если бы можно было как-то коротко и явно «глушить» (превращать в unchecked) некоторые исключения при вызове конкретных методов. Аннотациями, например.

В 7-й Java добавили наконец-то возможность в одном catch ловить сразу несколько исключений, так что теперь простыни должны стать чуть меньше.
ну, тут есть одна хитрость — которая иногда помогает ;) можно написать отдельный catch на те эксепшены, которые надо перехватывать — а на остальные повесить просто catch(Exception) — или вообще, Throwable. к примеру, FileNotFoundException extends IOException — так что достаточно одного catch(IOException) в блоке обработки… но в целом, соглашусь с Вами — многие checked можно было бы пускать через RuntimeException.
За перехват ВСЕХ исключений обычно больно бьют, в лучшем случае на ревью.
если это «тупой перехват» — с целью не возиться, тогда я бы сам вдарил ;) а если известно, что логика для всех возможных исключений — одна и та же, и они все наследуются от общего предка — тогда зачем на каждый класс писать одинаковый обработчик? в Яве, к примеру (кто про что, а шелудивый — про баню ;) от Exception наследуются ожидаемые исключения (которые требуют объявления в интерфейсе метода), а неожиданные — это RuntimeException, которым объявление не требуются. так вот, если я знаю, что у меня в коде могут возникнуть три вида ожидаемых исключений — и на все их я буду одинаково реагировать, я просто перехватываю Exception — а переполнение стека или что-нибудь еще такое вылетит у меня по RuntimeException ;) всегда мы упираемся в вопрос проектирования — в значительной степени, это касается базовых классов и библиотек. которые, к сожалению, слишком часто пишутся такими же людьми, как мы сами ;)
Понял, извиняюсь. На Delphi ожидаемые исключения на уровне языка не выделены, а потому просто подниматься до общего предка при анализе нельзя категорически.
Верно написано, но при использовании friend-функций имхо все таки удобнее пользоваться кодами.
Коды возврата выжили совсем по другим причинам.

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

2. Исключениями большинство нормально не умеет пользоваться. Попробуйте объяснить человеку, что это не механизм возврата кодов ошибок, а именно механизм исключительных ситуаций, которые разработчик алгоритма просто не может предусмотреть (например, он пишет обобщённый список). И исключения не должны быть частью API библиотеки необобщённых алгоритмов (шаблоны там всякие) вообще. И вследствии этого, всё выглядит красиво на маленьких примерах, а в большой программе исключения — это полный вынос мозга.

3. Ну. И вообще, исключения — это вообще-то очень древний и корявый способ. Человечество как бы уже додумалось до сопроцедур (coroutines), до продолжений (continuations) и прочих механизмов, которые дают понятную логику с возвратом значений, на далёкий уровень стека вызовов: основное важное качество исключений.
В целом с этим тоже соглашусь. В частности добавлю:

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


На мой взгляд, большая часть кода, который мы пишем, не требует какой-то особой эффективности :). Задачи, требующие эффективности в целом — например, ядро linux, как правило пишется на специально предназначенных для этого языках — например, C :).

И вообще, исключения — это вообще-то очень древний и корявый способ. Человечество как бы уже додумалось до сопроцедур (coroutines), до продолжений (continuations) и прочих механизмов, которые дают понятную логику с возвратом значений, на далёкий уровень стека вызовов: основное важное качество исключений


Оно сейчас, увы, не в мейнстриме :(.
1) а Си++ менее эффективен чем просто Си: требует дополнительной памяти для RTTI, не говоря уже о дополнительных расходах при вызове функций — членов класса.
2) классами большинство нормально не умеет пользоваться. период.

))
1. Ну… Си++ — монструазная штука и хорошо оптимизируемая (куча народа над этим работает). Поэтому, например, при помощи шаблонов Си обогнать вполне реально. Другое дело, что Си++ — это такой Си++, там всё себе вывихнешь, пока нормальный по скорости код получишь.

2. Поддерживаю.
не говоря уже о дополнительных расходах при вызове функций — членов класса.
виртуальных функций.
Как раз в больших программах исключения много лучше кодов ошибок — так как выдают место ошибки вкупе со стеком. Коды ошибок что-то говорят только маааааленькому ксуочку вызывающего кода, в котором тоже могут быть ошибки. В результате большая программа падает в месте, от которого до места ошибки как до Китая пешком.
Меньшая эффективность имеет смысл, только если исключительных ситуаций у вас много, то тогда проблема явно не в исключениях, а в логике на них.
Так что единственный недостаток исключений в прикладном программировании — это неумение ими пользоваться.
Только соппроцедурами, продолжениями и т.п. умеют пользоваться еще меньше народу.
Всё, конечно, зависит от контекста задачи. Для большой и сложной системы более применимы исключения. Для лёгкой и требовательной к производительности задаче коды ошибок скорее всего будут наилучшим выбором. Поэтому для меня будет странным увидеть документ «описания кодов ошибок» в проекте на C# с кучей классов, но в программе управления ДВС ракеты-носителя я бы скорее удивился обратному.

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

Вообще, по-моему, тут не стоит вопрос что использовать, тут стоит ответ на вопрос «какая основная цель у Вашей системы — расширяемость и поддерживаемость или риалтамовость?»
Да ну. В большой и сложной системе исключения, повторюсь, это — вынос мозга. С ними невозможно будет программировать. А вот в realtime задаче нечто похожее на исключения может быть вполне выгодным, потому что в случае ошибки, управление будет передано на обработчик, возможно, быстрее (зависит от реализации).
А у меня противоположное мнение: в большой системе только с исключениями и можно прожить. По опыту полумиллионострокового проекта на Delphi.
Да ну. В большой и сложной системе исключения, повторюсь, это — вынос мозга.
Мягко говоря, спорное утверждение. Уж не знаю про какой язык речь, но в большинстве известных мне всё как раз наоборот, имхо. Если про ООП говорить, то тем более. Исключения как раз гораздо более подходят для полноценной абстракции, чем коды возврата. Например, в java это и просто не принято и неудобно совершенно. Возврат null в некоторых случаях допустим, например, когда отсутствие объекта само по себе не ошибка ну и т. д., от реализации зависит.
Надо их уметь готовить. Я, собственно, и статью написал дабы обсудить с сообществом способы приготовления. А то вот недавно смотрел код серьезного open source проекта, так там данные по сети отправлялись в деструкторе по срабатыванию исключения (это у них был штатный способ отправлять данные). Смотришь на такое и понимаешь что зря, зря они селедку в шоколаде запекали :).
За логику в деструкторе надо пороть до просветления.
Ну… Не знаю. Согласен, что утверждение спорное. Просто самый замечательный экспириенс у меня был с исключениями, когда руководитель проекта сказал: никаких исключений в интерфейсах, за оные буду лишать части зарплаты. И всё так сразу стало красиво и понятно. Даже внутренняя логика модулей от этого сильно выиграла, там было, в основном, try-catch в теле реализации метода интерфейсного класса, и все знали, что дальше оно не вылетит, и не лепили поэтому try-catch где-то внутри.

Всё было легко и просто. Кажется, кстати, в Go принята такая же политика обработки исключений.

Так что, по-моему, коды возврата — это благо для больших систем, когда модули явно друг другу говорят, что с ними не так происходит. API операционок и, скажем, POSIX-овская модель работы с процессами, которая весьма удобна, тому подтверждение: внутри процесс может по своему стеку хоть на ассемблере гонять, но наружу он выдаёт код возврата, и это удобно для программирования.
В Go есть другие средства аналогичной мощности вроде defer.
Коды ошибок хороши только при заведомо слабом рантайме — например, при межмодульной коммуникации. Но и там есть способ протащить исключения для тех, кто их понимает (те, кто не понимают — увидят конвенционный код ошибки).
С ними невозможно программировать, простите, только тем, кто исключениями пользоваться не умеет.
Основная беда С++ в отсутствии finally, я считаю. Да, можно написать костыль, но это будет уже не то. Поэтому в плюсах активно юзать исключения — нецелесообразно.
Зачем в С++ finally, если в C++ есть RAII?
Не всегда удобно засовывать освобождение ресурсов в деструкторы. Пример — подтверждение или откат транзакции БД.
Собссно я к тому и клоню, что в С++ без исключений можно вполне прожить. Только надо принять решение — используем или не используем вообще.

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

Ситуация — звоним на телефон и проигрываем файл
public PlayFileResult PlayFile(string file) {… }

Пусть будет три состояния результата операции:
1) Оборудование сдохло
2) На другом конце провода факс
3) Ответил человек

Очевидно, что в первом случае надо бросить ошибку (вызвать исключение), во вторых двух надо вернуть какой-то код.
Вопрос сложный, но интересный. Я бы сказал так:
1. В зависимости от приложение сдохшее оборудование может быть как неожиданной ошибкой (у нас пользовательское приложение которое работает со штатной звонилкой системы) так и ожидаемой (у нас сервис на АТС, от нее постоянно что-то отваливается). Соответственно, кидаем разные исключения.
2. Факс / человек на другом конце провода — это вообще не ошибка, это результат работы функции :). И мы его естественно возвращаем. Использовать исключение для результата было бы… хмм… не по феншую.
знаешь, а меня вот как пользователя жутко бесит когда один отвалившийся модуль программы рушит всю программу. а чтобы этого не произошло — приходится каждое обращение к модулю заворачивать в try-catch-и. и чем более модульная система, тем страшнее получается код.
Зависит от программы. Если у нас Photoshop и в модуле не сработала API функция CreateEvent() — мы, конечно, можем это завернуть и попытаться продолжить работу. Но тут есть тонкий момент — неожиданные ошибки они на то и неожиданные, что при нормальной работе программы они возникать не должны. А если возникли — значит что-то пошло не так. И продолжить работу может быть не лучшим вариантом — если не сработал CreateEvent() то скорее всего у системы пушной зверь с ресурсами и она скоро рухнет. Дадим пользователю продолжить работу — он захочет сохранить файл — и следующая ошибка такого рода может привести к его повреждению. А мы совсем не хотим чтобы у пользователя повредились данные :). Вот по этому и приходится падать с громким стуком O_O.
и терять несохранённые данные? классное решение. а чтобы данные не повреждались нужно использовать транзакции. записали, проверили, стёрли предыдущую версию.

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

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


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

_как правило_, ага.
Приложение, которому настолько критично сохранение данных — вообще не должно иметь несохраненных. Классический пример — СУБД Oracle. Ее лог транзакций пишется синхронно с операцией и позволяет восстановить состояние базы даже после полного песца в рамках системы. Коды ошибок тут скорее вредны чем бесполезны.
ну, всегда надо искать балланс между сохранностью данных и скоростью работы. только вот отказываться от сохранения не потому что это невозможно, а потому что это _может_ не получиться — глупо. надо хотябы попытаться. а сбой сохранения всегда может произойти и надо быть к этому готовым и позаботиться о бэкапах/журналах заранее.
Почему это всё очевидно? Если вы предусматриваете все эти состояния, ну сделайте код возврата, да и всё. Не нужно тут никакого исключения. Исключение нужно тогда, когда вы никакого иного интерфейса не можете предложить. Вот если бы было нечто такое:

public playfileresult playfile(musicfeed feed) {… }

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

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

Исключения они для того и нужны чтобы организовать непредусмотренный (по разным причинам, иногда невозможно предусмотреть) авторами кода возврат из него. То есть, это вообще механизм для обобщённого программирования, а не для обработки ошибок. Страуструп вообще должен жевать свои тапочки за то, что настаивает на использовании исключений всегда и везде.
Механизм исключений как раз очень удобен, чтобы не городить всевозможных Result'ов. При нормальном функционировании метод возвращает свое успешное значение, а если что-то пошло не так — конкретное исключение, прописанное в заголовке метода (это я про java говорю). Таким образом логика нормального фукнционирования не усложняется лишними проверками if (result!=ERROR), а обработка ошибок вынесена в отдельный блок.
Это когда пишется необольшое приложение для себя — это да, логика не усложняется. Но если это сложное приложение с кучей библиотек — не будет в нём всё так красиво. Не будет никаких общих блоков обработки ошибок, потому что это не возможно, потому что в большинстве случаев отказ какого-нибудь компонента не должен приводеть к прерыванию всей цепочки работы (чего-нибудь клиент-серверное к примеру).

Кроме того, а почему Вы считаете, что 100500 кодов возвратов будут хуже, чем 100500 значений для исключения?
По очень простой причине — исключение можно перехватить и залогировать, а не перехваченное всплывет и бахнет. Позабытый код ошибки может создать видимость нормальный работы с реальными повреждениями в рандомных местах.
Считаю, что исключения стоит использовать действительно в исключительных ситуациях. Более того, разумно использовать исключение только в том случае — если знаешь как его обработать. Сам по себе блок catch (Exception ex) не имеет смысла, мы всего лишь знаем, что возникло исключение, а чем оно вызвано и что делать дальше… Другое дело, например, catch (FileNotFoundException ex) — сразу ясно что за ошибка и чем она вызвана. Считаю, что прежде чем прибегать к использованию исключений — можно выполнить те или иные проверки, которые позволят избежать возникновения исключительных ситуаций (например, проверить наличие файла, прежде чем пытаться прочесть его). Лучший вариант, на мой взгляд, использовать коды ошибок+исключения. Естественно, если для проверки валидности той или иной операции потребуются 15 вложенных if-оф, логичнее обернуть блок кода в try-catch, но лепить их везде, уповая на высокую производительность машин — неправильно, имхо.
Где найти пример правильного использования исключений в C++?

Я искренне не понимаю следующие моменты:

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

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

Если кто-то добавляет новое исключение в низкоуровневую функцию, то как это исключение нужно обработать? А что если эта функция используется в нескольких проектах?
Как программировать и сопровождать код с исключениями?

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


По феншую, мы ловим один тип исключения — «ожидаемая ошибка». Соответственно, если нам не нужна детализация (а это 99% случаев), то исключение ожидаемой ошибки будет корнем иерархии исключений ожидаемых ошибок. Ловим мы его как правило выше вызовов функций, в достаточно большом логическом блоке на который мы хотим среагировать — наприимер, показать пользователю сообщение.

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

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


Иерархия исключений должна быть спроектирована таким образом, что ожидаемые и неожиданные ошибки находятся в коре иерархии — их и ловим, если не нужна детализация. Увы, не все проектировщики языков и API об этом знают :(.

Если кто-то добавляет новое исключение в низкоуровневую функцию, то как это исключение нужно обработать? А что если эта функция используется в нескольких проектах?


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

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


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

А еще как убедить программистов не прятать свои же ошибки с использованием конструкции catch(...)?


Поставить какую-нибудь утилиту на сервер чтобы она не допускала коммиты с таким кодом :). Дешего и сердито.
то есть степень ожидаемости исключений целиком и полностью определяется не программистом, а автором языка?
Ну не то чтобы языка, скорее фреймфорков и API.
Спасибо за ответ, многое прояснилось и с виду вроде логично (не хватает кармы чтобы поставить плюс).

Еще хотелось бы уточнить:
Существуют ли стандартные иерархии исключений в C++, или в каждом проекте нужно придумывать свою иерархию классов, стоит ли наследовать ее от чего-нибудь вроде std::exception?
Попробую потроллить :-)

Топик на засыпку: «Реализация механизма исключений средствами аспектно-ориентированного программирования».
Аккаунт на хабре я тебе дал — флаг в длани, пиши :)
на самом деле, мне кажется, выбор между исключениями и кодами возврата заключается в ответе на вопрос: где будет обрабатываться ошибка(нестандартная ситуация)? если в той же части кода, что и вызывает код, генерирующий нестандартную ситуацию — тогда код возврата может быть проще. если же перехватчик может лежать гораздо выше в стеке вызова — тогда передача кодов возврата наверх может быть слишком громоздкой, поскольку потребуется проверка на каждом уровне. в моем представлении, «file not found» — это для кода возврата, а read error — это уже исключение (хотя зависит от контекста)
Тогда император, отец ребенка, обнародовал указ, предписывающий всем его подданным под страхом строгого наказания разбивать яйца с острого конца. Этот закон до такой степени озлобил население, что, по словам наших летописей, был причиной шести восстаний, во время которых один император потерял жизнь, а другой — корону.

…Насчитывают до одиннадцати тысяч фанатиков, которые в течение этого времени пошли на казнь, лишь бы не разбивать яйца с острого конца. Были напечатаны сотни огромных томов, посвящённых этой полемике, но книги Тупоконечников давно запрещены, и вся партия лишена законом права занимать государственные должности. В течение этих смут императоры Блефуску часто через своих посланников делали нам предостережения, обвиняя нас в церковном расколе путём нарушения основного догмата великого нашего пророка Люстрога, изложенного в пятьдесят четвёртой главе Блундекраля (являющегося их Алькораном). Между тем это просто насильственное толкование текста, подлинные слова которого гласят: Все истинно верующие да разбивают яйца с того конца, с какого удобнее.

(с) Свифт.
На C# исключения необходимо использовать только при возникновении ошибок («исключительных ситуаций»), которые невозможно предугадать на этапе написания кода. Все остальные пути поведения программы необходимо предусмотреть на этапе проектирования.
Sign up to leave a comment.

Articles