Pull to refresh

Comments 190

Для с++/delphi… исключения могут приводить к memory leak, даже в хорошем коде. Пример:

try
a = A.Create;

try 
  a := A.Create;
  try
     a.Foo;
  except
     //do Something
  end
finally
  a.free;
end;

где a.Foo

self.p := P.Create;
p.Foo; // exception;
p.free; 

В этом случае p не будет освобожден.
Иными словами, если вы в одном месте написали try-except, вам нужно будет его писать и во вложенных функциях, иначе вы не можете рассчитывать, что try-except освободит вас от ошибок.
Прошу прощения, не

try 
  a := A.Create;
а
a := A.Create;
  try 


Кстати, в некоторых случаях правильнее засунуть вызов конструктора именно внутрь try. Знаете когда?
Нет, не знаю. Давайте пример…
В случаях, когда требуется сконструировать более одного объекта, поработать с ними, а потом разрушить:
a := nil;
b := nil;
c := nil;
try
  a := A.Create;
  b := B.Create;
  c := C.Create;
// Some code with a, b and c objects
finally
  c.Free;
  b.Free;
  a.Free;
end;



Да, можно было сделать 3 вложенных try..finally, но это будет жутковато выглядеть.
Если в a.Create — exception, будет вызван free для a,b,c, которые еще не созданны. Access Violation. Или я чего-то не догнал?
Не догнали. Смотрим исходники:

procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;


Осюда вытекает важнейшее свойство — у нулевых объектов можно безопасно вызывать .Free, но не .Destroy
А еще лучше — вызывать FreeAndNil
Жаль, что собственному комменту нельзя поставить минус. Знал же про free для нулевых…
Не переживайте так, добрые люди наставят Вам минусов.
Интересно… Вы используете эксепшены в первом случае:
try
a.Foo;
except
//do Something
end

но не используете во втором:
p.Foo; // exception;
p.free;

На мой взгляд — плохой пример, который, кстати, без отработки эксепшенов работать не будет.
> Иными словами, если вы в одном месте написали try-except, вам нужно будет его писать и во вложенных функциях…

Без except он будет крешиться, что заставит программиста чинить p.Foo;
И где тут хороший код?
Во-первых, вызов конструктора обычно выносят за блок try, т.к. в случае ошибки в конструкторе деструктор будет вызван автоматически.
Второй пример тем более нельзя назвать хорошим кодом, т.к. любой делфист должен знать, что вызов деструктора всегда должен быть в finally. Это же азбука.
Ну и в третьих обработка исключений в С++ и Delphi сильно отличается, хотя бы в том, что в стандарте С++ нет finally, поэтому в С++ лучше избегать исключений в том количестве, в каком они традиционно используются в Delphi.
> конструктора обычно выносят за блок try
выпил кофе, увидел, исправил.

> вызов деструктора всегда должен быть в finally. Это же азбука.
Это азбука delphi для модели обработки ошибок способом try-except, что никогда не было способом по умолчанию, загляните, например, в исходники VCL.
>>загляните, например, в исходники VCL.
Заглянул. И вам советую. Практически везде .Free внутри finally.
function TMenuStack.Pop: TCustomActionMenuBar;
begin
  Result := PopItem;
  if csDesigning in FMenu.ComponentState then
    FreeAndNil(Result)
  else
  if Assigned(Result.ActionClient) then
  begin
    Result.ActionClient.ChildActionBar := nil;
    Result.FChildMenu := nil;
    Result.ActionClient := nil;
    if (Count = 1) and not (FMenu is TCustomActionPopupMenu) then
      Peek.FCachedMenu := Result as TCustomActionPopupMenu
    else
      FreeAndNil(Result);
  end;
end;


destructor TInternalMDIAction.Destroy;
begin
  if Assigned(FClientItem) then
    FClientItem.Free;
  inherited;
end;


Мда, посмотрел подробней. Наткнулся и на try-finally, но не почти везде, а в редких случаях. В общем, не по умолчанию оно.
В дуструкторах то понятно члены разрушаются без всяких finally. Там где мы создаем и разрушаем объект внутри одной процедуры, там try..finally везде. Загляните в controls.pas, например.
В деструкторе да, ошибся, не посмотрел. Да я уже вижу, в разных файлах по разному…
Код в A.Foo — плохой. Очень.
1. Если один и тот же ресурс выделяется и освобождается в пределах одного метода, то его не следует делать полем объекта.
2. Освобождение поля объекта следует делать в деструкторе, а локальной переменной — в блоке finally.

try..except при нормальной работе нужен всего в трех случаях:
1. Предотвращение утечек в фабричных методах.
2. Повторный выброс исключения на границах абстракции (превращение «Не могу открыть файл» в «Не могу записать в лог»).
3. Собственно разбор полетов, там где нужно восстановление, повтор, логирование и т.п.

В остальных местах вполне хватает try..finally
1. Вот такой синтаксис для Delphi означает присваивание полю или свойству объекта:
self.p := P.Create;

2. Вы говорите, простите, ересь. Того, кто освобождает ресурсы вне блоков finally или деструкторов (деструктор в Delphi — аналог finally для объектов) — больно и обидно бьют. За утечки при выбросе исключений.
В моей проге, самое сильное звено — веб-сервер (на WinAPI), в нем нет try-except. Нет memory leak.

Уверяю вас, огромная прога на Delphi может жить без try, except, finally, raise. И ей от этого станет легче).
Видел я такие проги — легче от этого не было никому. Куча защитного кода, увидеть за которым логику программы не может даже автор через пару месяцев.
Кстати, WinAPI тоже может бросать исключения.
А писать на голом Win32 — это для мазохистов
Там self по ошибке, виновен.
Еще есть такие указатели, умные называются… хотя тут нужны не столько умные, сколько простые, типа scoped_ptr.
А еще есть GС. Но статья не об этом.
В хорошем коде на Delphi — никогда. За плюсы, полагаю, тоже самое ответят плюсисты.
Я читал про проблемы плюсовых исключений, но так как сам на них не пишу — не имею своего мнения :) В Delphi исключения вкусны и полезны при правильном их использовании — обозначении явных редких сбоев.
На них некоторые даже бизнес-логику строят, что конечно уже явный перебор. В таких программах пользователя может весьма развлечь стандартное окно ошибки с надписью «Ошибок нет», сам такое видел.
За бизнес-логику на исключениях — жестоко караю на ревью.
Так теряются все плюсы исключений:
1. Код чистой бизнес-логики перестает хорошо читаться.
2. Производительность идет лесом.
3. Лог засоряется исключениями, которые не являются сбоями.
Знаю, плохо, но сам иногда пользовался. Например, весьма удобно завести исключение ECancel, вызываемое на пользовательскую отмену какого-либо действия. Им можно легко вернуть любую глубоко вложенную бизнес-логику с формами и диалогами на место вызова. Да а залогировать заодно. Вроде плохо, но почему, затруднюсь ответить.
Во-во! Только я делал свой лисапед вместо EAbort, что сути не меняет.
Я думаю самая главная причина по которой в Гугле не используют исключения обозначена там в описании:
Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in a new project.

Что можно читать как: мы бы использовали исключения если бы не большое количество уже написанного кода без исключений.
Это не верно для современного C++ с повсеместным использованием RAII и кросс-тредовым пробросом исключений. Для современного C++ исключения есть нормальный способ уведомления о нестандартной ситуации. Коды ошибок уходят в прошлое и остаются уделом C. Вы можете считать иначе, но после выхода очередных книг от Саттер\Майерса, где будут петь дифирамбы исключениям, новые поколения программистов будут их использовать. Ровно как и люди, которые прислушиваются к мнению этих авторитетов. Ну это окромя тех, кто сам видит очевидные плюсы исключений. Парадигма языка меняется, не стоит цепляться за старое; надо менять свое отношение.
Рассмотрим пример на основе функции получения точки. Предположим, она считывает очередную точку из некоторой очереди. Вот как выглядит код без использования исключений:

while (!queue.isEmpty()) {
Point point = queue.getPoint();
if (point != null) {
...
}
}


В этом примере проверка на null будет выполнятся всегда, при каждой итерации.


Проверка будет выполняться всегда и при использовании исключений. Только переедет она в функцию getPoint(). Надо же нам проверять бросать исключение или нет.
Не совсем так. Раньше же в getPoint() была проверка, возращать null или валидную точку. И еще проверка на null в вызывающем коде. С исключениями результат первый проверки замениться на исключение, а вторая станет не нужна
В общем случае с точки зрения быстродействия лучше результат инициализировать невалидным значением, и при возможности (при нахождении точки) присвоить ему валидное.

Point getPoint()
{
Point result(std::numeric_limits::quiet_NaN(),std::numeric_limits::quiet_NaN());
/// Действия по поиску точки.
return result;
}
От задачи зависит. Ничто не мешает сделать как-то так:

Point getPoint() {
for (Point p: points) {
if (pointMatches(p)) {
return p;
}
}
throw NewPointNotFoundException
}
Извиняюсь что код не подсвечен, мне мои грехи в прошлой жизни не дают теги использовать
Никакие грехи не отнимут у вас &nbsp; хотя бы для форматирования.
Кстати, а ведь никуда проверка даже и не переедет. Она здесь же и останется. Ни один компилятор вот это никогда в линию не развернет:
try {
  while (!queue.isEmpty()) {
    Point point = queue.readPoint();
    ...
  }
} catch(PointNotFoudException ex) {
  throw new QueueReadingException(ex);
}

А значит на каждом витке цикла у нас будет та самая проверка об избавлении от которой автор поспешил нам рассказать. Только она завуалировна, скрыта синтаксисом исключения: а надо ли нам переходить на следующий виток цикла или было брошено исключение и нужно идти в блок catch.
А после какой именно инструкции внутри try {} она выполняется?
Вообще, по идее, после вызова каждой функции теоретически способной кинуть исключение. После каждой такой функции должно быть ветвление: или будет выполнена следующая после функции операция или будет переход к блоку catch.
Насколько я себе это представляю, каждый try формирует дополняет стек вызовов, отчасти параллельный стеку вызовов функций, а каждый raise просто переводит цепочку выполнения на возврат по другому стеку. Так что небольшие накладные расходы на try и никаких if-ов.
А можно где-нибудь про это почитать? Всегда интересовала реализация языка на низком уровне.
CPU window в Delphi — первое место, где бы лично я стал про это искать подробности. Но пока не искал.
>> Например, затраты на одно сравнение равны единице производительности. Инициация исключения 5 единиц.
Что-то из серии: «Давайте считать, что слон — это три енота. Тогда, влияние пятен на солнце, на мозг, значительно уменьшается!»

Откуда взято это отношение — 1:5?
С точки зрения бюджета зоопарка на прокорм зверей, 1(Слон) == Х(Енот).

Автор топика, АФАИУ, хочет сказать, что вопрос о использовании исключения зависит в основном от статистики: Экономия = (Количество_нормальных_выполнений * экономию_от_отсутствия_проверки_результата) — (Количество_исключений * затраты_на_выкидывание).
Я понимаю мысль, но не понимаю, на основе чего автор приводит именно такое отношение — 1:5.
Интуиция? Я после сегодняшних размышлений буду нагруженные места просто гонять тестами, чтоб вычислить эти самые экономию и затраты для каждого на практике, а потом прикидывать, при каком соотношении исключений к нормальным отработками становится выгодней их выбрасывать.
Числа взяты с потолка. Явно затраты на создание объекта выше, чем на сравнение. При чем, затраты на создание объекта исключения еще выше. Разница зависит от компилятора, виртуальной машины, железа, глубины стека. Поэтому четких цифр приведено не было. Основная цель как раз показать, что при увеличении числа итераций, количество проверок снижает производительность существеннее, чем использование исключений.
Код станет более элегантным, и на производительности это сильно не скажется.


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

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

1. Дать по рукам тому, кто комплектовал программу, если это программный файл.
2. Сообщить пользователю о нехватке файла, если это важный файл данных.
3. Выдать в консоль справку по программе, если в программу не передали необходимые данные.
4. Это какой-то другой случай, и тут возможны варианты.

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

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

Но в посте throw-catch используются не для обработки исключений, а для обработки ошибок в логике.
«Предположим, что вместо очереди у нас некоторый поток, который приходит из сети. При передаче могут возникать ошибки.» — это явно не ошибки в логике.
Сообщить пользователю о нехватке файла или выдать в консоль справку — это и есть обработка исключения. Кэп.
Я в курсе. Не в курсе автор поста. Он обрабатывает ошибку в логике, а не исключительную ситуацию.
И где тут ошибка в логике?
>>Исключение = признание, что в коде могут возникать ошибки

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

Я говорил о локальном коде вокруг авторского Point'а. Оборачивая такой код в исключение, мы заранее готовимся к тому, что он упадет… когда-нибудь. И это — вместо обычного ASSERT, который уже на этапе разработки покажет узкие места. Засада с исключениями в том, что они вызывают привыкание, и новички в программировании уже готовы лепить их там, где по-хорошему нужно бы изменить архитектуру. Исключения просто-таки просят, чтобы их вставляли «во избежание». И это происходит. Я сам через это прошел, но научился распознавать ситуации, когда нужно делать throw, а когда — нет.
Судя по профилю вы пишите в основном на С++. По исключениям плюсах я с вами соглашусь, на джаве, шарпе и делфи — нет.

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

PS: дело не в совершенстве, а хотя бы в том, что на любой самой идеальной строчке кода может выпасть out of memory или еще что, неподвластное этому коду.
Такого быть не должно. Просто потому, что не должно. Либо где-то есть проблемы с архитектурой, либо с быдлокодом, либо с тем, что используется в коде. Разумная программа должна обладать разумным (предсказуемым) поведением, даже если она выполняется в ограниченном пространстве, а не в идеальном мире, где бесконечные ресурсы и все нижележащие слои могут быть кривыми. Вопрос тогда нужно будет задавать к нижестоящим слоям, а не к программе. А иначе мы так и будем переносить ответственность за свои косяки на других.
Пардон,
* не могут быть кривыми.
>>Такого быть не должно
Оно есть, независимо от нашего желания.

Подключая чужие библиотечные функции вы же выясняете какие коды ошибок они могут возвращать? Точно также в языках с активным использованием исключений вы заранее выясняете, какие исключения могут возникнуть при выполнении той или иной функции. Принципиальной разницы никакой нет.
Ну как же? Подключая чужие библиотеки, я жду от них, чтобы они не выкидывали throw, а возвращали невалидные данные / коды ошибок. А иначе я не могу быть уверенным в библиотеке?
Исключение — это не просто падение программы. В Java, Python, etc они бросаются для того, чтобы их перехватывали
ошибки заставляют вышележащие системы обрабатывать себя (говорю о явовских чекнутых эксепшнах) или как прокидывать дальше, но в любом случае сказать что не предупреждали человек не может. Коды\невалидные данные оставляются на совести девелопера который может банально забыть про них.
А где гарантия, что кодер не забудет обработать исключение? Или попросту на него не забъет? Распространенный случай, между прочим.
В Java например у него нет возможности забить. Он должен или обработать выброшенное исключению сразу же, или перебросить исключение выше, иначе код просто не скомпилируется.
Положим это зависит от типа исключения в Java — checked / unchecked
поэтому про явовские чекнутые и написал. Непомню чтоб нечекнутые приходилось создавать.

А рантаймы на подобии out of memory это такая бяка когда действительно непонятно что делать но и тут есть возможность их ловить, а с кодами такое не пройдет.

Так что и гибкость и строгость присутвуют — вполне элегантно.
unchecked — это немного уже из другой области. Если брошено Unchecked exception — это означает что код некорректен (вызов метода у null, закончилась память, завалился assert). И адекватно продолжить работу после такого исключения мы скорей всего не сможем. Ближайший аналог unchecked exception в C это SIGABRT.
Checked-исключения же могут возникать и при корректном коде, но некорректном окружении(отсутствует файл, соединение с сервером и т.п.). Это как раз аналог error-кодов и невалидных данных в C и подобных языках. Во многих случаях мы можем сообщить об ошибке пользователю и продолжить работу. И такие ошибки в хорошем софте должны обрабатыватся всегда.
Не совсем соглашусь с вами. Взять тот же Spring framework — org.springframework.dao.DataIntegrityViolationException
Это unchecked исключение, тем не менее его ловить считаю вполне корректным, потому что это не некорректность кода, а попытка запихать в БД некорректные данные и об этом вполне разумно сообщить явно, а не говорить, что просто возникла ошибка.

Или же org.springframework.dao.ConcurrencyFailureException
Та же ситуация, в случае проблем с optimistic locking это все вполне успешно обрабатывается.

Так что имхо unchecked исключение исключению рознь.
В принципе да, я написал как оно должно было бы быть в идеальном случае, в жизни все остается на совести разработчиков
Есть другая гарантия — необработанное исключение вылезет наверх по стеку и больно покусает. Непроверенный код ошибки тихонько нагадит и, мерзко хихикая, будет провоцировать сбои в самых неожиданных местах.
А что будет, если вы правильно не обработаете коды ошибок? Да то же самое что и пропущенные исключения. Только исключения немедленно вылезут наверх, а необработанные коды могут вылезти в неожиданном месте.

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

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

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

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

> А с каких это щей вы не обрабатываете документированные исключения? Вы что, стремитесь завалить свою программу?

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

Кстати, коды ошибок я не использую. Использую невалидные данные, как это и принято в библиотеке Qt.
>> Кстати, коды ошибок я не использую. Использую невалидные данные, как это и принято в библиотеке Qt.

Если вы действительно пишите на Qt, наверное заметили, что каждый мало-мальски сложный класс имеет метод lastError() который и расшифровывает, почему вы получили инвалидные данные, т.к. одной проверки на инвалидность часто недостаточно. Это не что иное как resultCode, только расширенное.

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

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

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

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

Про lastError — неправда. Он есть только у классов, взаимодействующих с внешним миром, да и то не у всех. Другие сколь угодно сложные классы этой функции нет имеют и не могут.

> Но прошу вас, не суйтесь со своими понятиями в другие языки

Я пишу на Haskell. И у меня, по крайней мере, три видения ситуации с исключениями: со стороны Haskell, со стороны Qt и со стороны Delphi/Builder. По последнему смотрите, например, Текстовый анлизатор. В нем try...catch используются ровно 2 раза, — да и то потому, что во всем остальном коде они были не нужны, достаточно было отладить его с ASSERT'ами. А в Haskell вообще сделано интересно: там исключения — это что-то совершенно инородное и ненужное. Там все по-другому.

> Так и скажите — не осилил исключения, вот и всё. Некоторые до сих пор точно так же ООП не могут принять, и пишут на той же джаве процедурный код, как привыкли.

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


Про lastError — неправда. Он есть только у классов, взаимодействующих с внешним миром, да и то не у всех
Если вы пишите сферического коня в вакууме, то конечно плевать вы хотели на коды возврата и исключения, но в своих программах вы же используете внешние библиотеки?

Не скатывайтесь в троллизм, пожалуйста
Кроме того, что исключения — уродство, непотребство и неожиданное (для вас) поведение — вы никаких аргументов не привели. отсюда я делаю вывод, что просто не осилили. Тем более если вы писали на С++ и Хаскеле — откуда вы могли этому научиться. Там исключения действительно чужеродны. Но, вы утверждаете, что:
Delphi/Builder. По последнему смотрите, например, Текстовый анлизатор. В нем try...catch используются ровно 2 раза, — да и то потому, что во всем остальном коде они были не нужны, достаточно было отладить его с ASSERT'ами
А я не поленился скачать ваши исходники. В них try..catch встречается, внимание, 21 раз! Зачем же так бессовестно врать на людях?
> А я не поленился скачать ваши исходники. В них try..catch встречается, внимание, 21 раз! Зачем же так бессовестно врать на людях?

А теперь докажите мне, что я не пользуюсь исключениями, потому что не умею.
И к чему эти пафосные заявления были? Стесняетесь что ли?

И объясните (если умеете) что это за вундервафельный код(Project1.cpp — название модуля кстати намекает):

        try
        {
                 Application->Initialize();
		...
        }
        catch (Exception &exception)
        {
                 Application->ShowException(&exception);
        }
        catch (...) 
        {
                 // WTF ??? 0_o
                 try
                 {
                         throw Exception("");
                 }
                 catch (Exception &exception)
                 {
                         Application->ShowException(&exception);
                 }
        }
Боюсь, здесь вы промахнулись. Это автогенерируемый код, сразу же появляется после создания проекта в Builder'е. Зачем именно так — я не разбирался.

Вы глубже загляните, глубже. Там много чего интересного. И концепций там хватает, — даже тех, о которых я ничего не знал, но использовал. Например, «Получение ресурса — это инициализация». Комбинаторные парсеры. DSL. Я уж молчу про шаблоны проектирования, которые не вставлены специально, а появлялись из нужд разработки.
Да я не спорю, без исключений можно написать шикарный код. Ну и пишите без исключений, если не умеете их готовить:
TBool TTextResoundingAnalyser::LoadText(const TTextString & tFileName)
    {
       try
       {
       TVCLFileDataLoader      _VCLFDL;
       
             TRawDataItem rdi(&_VCLFDL, tFileName);
             TUInt index = _RawDataContainer.AddItem(rdi);
            _RawDataContainer.UnvirtualizeAll();

            _Alias = _RawDataContainer[index].Alias();

            _CreateMapCovers();
            _CalculateMaps();
       }
       catch(...)  { return false; }
    return true;
    };

Просто схавали все исключения и вернули false. Отличный подход! Или это тоже автогенератор нагадил?
Нет, здесь, действительно, кривой код. Еще бы: из трехлетней разработки эта часть разрабатывалась первой. Я тогда учился на 3-м курсе университета, еще мало что знал. Впрочем, не исключено, что и дальше будут не очень правильные try. Особенно в конце, ближе к защите диплома, когда нужно было писать очень быстро, и отлов исключений был самым простым способом, чтобы оградить плохой код от хорошего.
1) на яве часто программу пишет большое количество людей с неизвестным уровнем программирования и общения между собой. Явный контракт основаный на эксепшенах легче поддерживать чем лазать по исходникам 5-летней давности в поиске кодов исключения.
2)Программа в которой бросание эксепшна является из ряда выходящим уродством представляется мне очень далеким идеалом.
По пункту 1 я не могу объективно рассуждать, поскольку яву не знаю.

А вот пункт 2 — это программа на Haskell.
рискну предположить что дело не только в языке но и в области применения — по сравнению с чтением\записью данных в базу ресурсы затрачиваемые на try\catch пренибрежительно малы также как и мыл эфорт со стороны программиста по сравнению с уменьшением проблем с поддержкой всей этой прелести что зовется кодом.
Про хаскель чистая правда — там коды ошибок благодаря алгебраическим типам и do-нотации делают исключения скорее вредными чем бесполезными.
> С каких это щей я буду неправильно обрабатывать коды ошибок?
Вы — жалкий смертный. Или этого мало?
С точностью до наоборот — подключая чужую библиотеку, я жду от нее, что она или вернет мне валидные данные или явно обозначит сбой выбросом исключения. Иначе я не смогу доверять библиотеке и буду вынужден писать тонны защитного кода с проверками, затрудняющими понимание моих собственных алгоритмов.
Вы путаете исключения для отладки, — они обычно реализуются статическими ассертами, — и исключительные ситуации вроде отсутствия файла. Вам стоит разделить эти совершенно разные понятия и строить свои утверждения на чем-то одном. Да, библиотека может выбросить исключение на доступ к элементу массива по невалидному адресу. Но это нельзя оборачивать в try...catch, а надо лезть в свой код и исправлять логическую ошибку.
А я где-то говорил про перехват всего? Я говорил что жду от библиотеки валидных данных или исключений. Первое позволит мне работать без лишних проверок, второе — демаскирует сбой и я смогу его быстро найти и понять, что является его причиной.
UFO just landed and posted this here
Да блин! Проблемы есть! И были и будут. С этим ничего не поделать. Архитектура и соглашения нарушаются, быдлокод в проектах есть, в проектах заводятся странные зависимости по прихоти вашей системы управления зависимостями.

Смиритесь с тем, что мир устроен хреново и ваши программы станут гораздо лучше ему соответствовать.
> Смиритесь с тем, что мир устроен хреново и ваши программы станут гораздо лучше ему соответствовать.

Меня еще никогда не призывали делать свой код хреновым.

Извините, не хочу.
Я призываю лишь внимательно читать, прежде чем писать ответ.
Последний абзац — серьезное заблуждение. Внешний мир для любой программной единицы несовершенен.
Функцию МОЖНО вызывать с невалидными значениями аргументов.
Файл МОЖЕТ оказать заблокирован другим процессом.
Сервер БД МОЖЕТ оказаться недоступным.
Это все — сбои вне вашего кода, сообщить о которых в большинстве языков можно двумя способами — кодом ошибок или исключениями. Второе много лучше по следующим причинам:
1. Исключение автоматически всплывает по стеку и больно бьет — код ошибок без сознательных усилий глотается
2. Исключения в ООП-языках можно наследовать — коды ошибок только добавлять новые.
3. «Бархатный путь» (основной алгоритм без сбоев) при использовании исключений читается гораздо лучше
Если вы вызываете функцию с невалидными параметрами — это одно. Это не исключительная ситуация, это исключительно кривые руки. Сервер БД недоступен? Это — другое. И тут возможны варианты, в том числе, и проброс исключений. Заблуждаетесь таки вы, сводя к одному и тому же обе ситуации.

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

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

Я вам сочувствую, потому что это уже не быдлокод, — это г. Большое и страшное Г. В моих программах такого не бывает. Либо код валиден, и он таким остается, либо невалиден и приводится к нормальному виду. В этом очень помогают ASSERTS, юнит-тесты, отладочная печать и следования всяким принципам вроде SOLID. Правда, я не специально следую им, а так получается из процесса разработки.
Так, а это что такое? А ну не расслабляемся, троллим тонко, вкусом, не портим топик.
«Это не исключительная ситуация, это исключительно кривые руки.»
Это исключительная ситуация «невалидный параметр», причиной которой таки да, являются кривые руки :) Такие исключения очень даже можно бросать, но нельзя перехватывать.
«И пихать их везде в логику, как делает автор поста, не нужно и даже вредно.»
Где вы увидели «везде» и «в логику»? Автор привел в качестве примера функции, которые по явно указанной им спецификации могут не вернуть валидный результат.
«Заблуждаетесь таки вы, сводя к одному и тому же обе ситуации.»
Откуда вы вообще это взяли?
«Но вы слишком категоричны, следовательно, ошибаетесь.»
Это вряд ли. Я уж точно не писал ничего подобного безапелляционному
«Никогда еще от исключений код не становился элегантным.»
А указывал, в каких случаях по моему мнению целесообразно использовать исключения в Delphi. Вы же обобщили свой плюсовый опыт на все программирование разом в виде безусловного догмата.
4. Объект исключения может содержать в себе массу полезной информации, в отличие от кода ошибки.
Вы не правы. Исключение= исключительная ситуация. Предположим вы пишите какой-нить скачивальщик файлов из сети. очевидно, вы не хотите делать connection сheck-и 5 раз в секунду. В таком случае отлавливание исключение о потере connection-а вполне предсказуемая функциональность.
Красота использования исключений состоит в том, разработчик учитывает возможные исключительные ситуации, а не пеняет на 3rd party библиотеки или кривизну операционной системы. По хорошему возможные исключения должны быть оговорены документации/описании к функции.
Если пишите код для себя, то возможно исключения не нужны, если же делаете библиотеку, которой БУДУТ пользоваться, исключения нужно использовать, так же как assert-ы. Исключения и assert-ы хорошо разграничивают ошибки. Первые отвечают за устойчивость, а вторые за корректность. Мне поэтому кажется, что пример в топике не совсем подходит, размыта грань между этими внешними факторами качества ПО.

На всякий случай определения.
Корректность — это способность ПО выполнять точные задачи так, как они определены их спецификацией.
Устойчивость — это способность ПО соответствующим образом реагировать на аварийные ситуации.

Про ассерты соглашусь. Техника важная и полезная.
Раз у нас примеры на java, то давайте смотреть на байткод. В первом случае у нас простое сравнение и условный переход. Во втором случае у нас такиеже сравнение и переход, но внутри метода (проверка на необходимость выбросить исключение)
и безусловный переход в вызывающем метод коде(чтобы обойти метод catch). В плане экономия команд я бы с вами не согласился.
А разве у вас в первом случае внутри метода не будет того же условия, чтобы вернуть NULL? А также проверка там, где метод вызывается, что пришло не NULL. 2 сравнения против 1, не?
Вообще, исключения ИМХО нужно кидать только при возникновение таких ситуаций, которые невозможно исключить (такой вот каламбур) на этапе отладки. Не смогли открыть файл — исключение, нету соединения по сети — исключение, закончилась память — исключение. Все остальные ошибки отсеиваются на этапе отладки с помощью assert'ов.

Нужно ли кидать исключение в функции getPoint() и т.п.? Нет, нет и ещё раз нет! Если возврат пустой точки — штатная ситуация, то и обрабатываться она должна штатно, именно через if. Если это нештатная ситуация и такого, согласно алгоритму, просто не может быть — значит этого не должно быть, и вы зря тратите ресурсы, проверяя бросать или нет исключение? А затраты могут быть просто огромными: падение производительности в несколько раз. Вы никогда не задумывались почему std::vector::operator[] не бросает out_of_range? Любая нештатная ситуация которую можете создать вы и только вы, как разработчик приложения, и проверяться должна только в вашей отладочной версии программы.
Полностью с вами согласен.
Все-таки есть две основных позиции программистов: одни за использование исключений в качестве управления потоком работы приложения (как в примерах, приведенных автором), другие против. Копий уже до этого топика сломано было много. Смысл снова возрождать холивар? Все останутся при своих и будут писать как раньше писали.
Как же «зачем?» Одна из этих позиций — богоугодная истина, а другая — ересь!
UFO just landed and posted this here
Еще можно добавить, что для того чтобы проверить, используются ли исключения в логике программы/алгоритма, можно просто попытаться их выкинуть (мысленно или реально) и если при это код остается работоспособным в идеальном окружении (абсолютно черном вакууме, где все файлы существуют, соединения создаются и ь.д.), значит все исключения использованы по назначению. Если этого сделать нельзя — нужно пересмотреть дизайн системы.
по моим наблюдениям, резко против исключений опытные матёрые С/С++ программисты. По одной простой причине — в стандарте нет finally а без него пользоваться исключениями стоит действительно только в исключительных ситуациях. Просьба к вам уважаемые коллеги: переходя с С++ на другой язык не тащите свой устав в чужой монастырь.
Я думаю, насчет причины вы все же ошибаетесь. Причина — именно в «матерности» и опыте.
Когда я писал на Delphi я пользовался исключениями, без них там никуда, там это естественно, на них построен VCL. Сейчас я пишу на C++/Qt и я ими не пользуюсь, потому что в С++ это действительно не естественно.
VCL в плане внутренней архитектуры не идет ни в какое сравнение с Qt. В немалой степени это обусловлено непонятно падающим кодом и исключениями в неисключительных ситуациях. Я верю, что на Delphi, не используя VCL, можно писать хорошие программы, но без throw на каждом шагу. Потому что это вопрос архитектуры и соблюдения принципов разработки, а не вопрос языка.

(Обсуждать С# не могу, ибо не обладаю знаниями.)
Я уже неплохо изучил Qt, и первый восторг уже сменился более взвешенным видением. Да, VCL конечно устарел уже лет на 10, сравнивать их бесперспективно (VCL можно сравнить со своим ровесником MFC, который наголову проигрывает)
Но и в Qt не всё так гладко, как хотелось бы, к сожалению. Если хотите могу указать кривые моменты.

PS: Что касается «падучести» VCL — то это от кривых рук его применяющих. 8 лет я на нём писал, и не helloworld'ы, а приложения в сотни тысяч строк кода, продающиеся тысячами копий, за отличные деньги.
>>Если хотите могу указать кривые моменты.
Укажите, пожалуйста. Еще с универа Qt изучаю и пока что слишком мало таких моментов заметил. Хочется получить более объективное виденье.
1. QSqlDatabase построен таким образом, что полностью инкапсулирует где-то внутри своих синглтонов созданные соединения. С одной стороны вроде бы удобно, можно использовать в моделях и запросах БД по-умолчанию, с другой — теряется контроль над соединением, что особенно актуально для многопоточных приложений с коннектом к нескольким БД.
1.а Также, практически отсутствуют механизмы управления транзакциями. Как, например, запустить одновременно две разные транзакции я пока не придумал. Да, всё это конечно решаемо, но осадочек остался.
1.б Также, пока не нашел, как выполнить запрос типа «CREATE DATABASE ». Чтобы выполнить запрос нужно подключиться к БД, а её еще нет.
2. Графика виджетов основана на вычислениях с плавающей точкой. Из-за чего на платформах без математического сопроцессора (ARM, например) интерфейс здорово подтормаживает.
3. В наследуемых от QObject классах нельзя использовать множественное наследование. Без этого конечно тоже можно прожить, но иногда очень бы пригодилось, тем более что в С++ нет Интерфейсов.

Был бы очень рад, если какой-нибудь гуру Qt написал бы, что я глубоко заблуждаюсь и всё это решается так и эдак :)

В целом же, Qt — уникальный фреймворк. Не ошибусь, если скажу что по ряду свойств ему просто нет равных в настоящее время.
1.б Также, пока не нашел, как выполнить запрос типа «CREATE DATABASE ». Чтобы выполнить запрос нужно подключиться к БД, а её еще нет.

Я работал в основном с SQLite и там на этапе открытия БД, она создавалась.

2. Графика виджетов основана на вычислениях с плавающей точкой. Из-за чего на платформах без математического сопроцессора (ARM, например) интерфейс здорово подтормаживает.

А это вряд ли. Они используют видео ускоритель для отрисовки графики, или по крайней мере нативное api (где это возможно). А с Qt 5 появляется требование к наличию видеоускорителя на конечной платформе. Можно поддерживать старье (телефоны без видеоускорителя в скором времени перестанут существовать) и быть неудобным, а можно предоставлять удобство и идти в ногу со временем. А подтормаживает оно у вас, видимо на телефонах на платформе Symbian, без поддержки аппаратного ускорения, так там не ARM там все остальное тоже тормозит.

3. В наследуемых от QObject классах нельзя использовать множественное наследование.

Как же так. Множественное наследование можно использовать, например QDeclarativeItem
doc.qt.nokia.com/4.7-snapshot/qdeclarativeitem.html наследуется от QGraphicsObject(который наследуется от QObject) и QDeclarativeParserStatus. А вот наследоваться от двух классов, каждый из которых имеет базовый класс QObject нельзя, это да.
1б) на SQLIte да, создается автоматом, если файл не найден. Но не могу сказать, что это хорошая практика.
2) Мы пишем проекты в том числе под встраиваемые системы на ARM'ах с WinCE. Поменять платформу не в наших силах, к сожалению. Недавно измеряли производительность на Marvell 1Ghz (модель не помню) — довольно печально всё. Ждём от Qt решения этой проблемы.
3)да, тут вы наверное правы. И ЕМНИП нельзя никак использовать множественное наследование если указали в классе макрос Q_OBJECT.
4) кстати под WinCE Qt не полностью переносится. Нет поддержки QPrinter и QXmlPatterns хотя бы в виде заглушек.
И ЕМНИП нельзя никак использовать множественное наследование если указали в классе макрос Q_OBJECT.

Ну что вы) с этим как раз все хорошо)
вот:
class MainWindow : public QMainWindow, private Ui::MainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
};

этот код, кстати, был сгенерирован Qt Creator (при выбранной настройке Embedding UI Class: multiple inheritance)
Действительно, ваша правда! Последний раз когда пытался это реализовать, помню что moc ругался. Видимо оба класса были QObject'ами.
Не встречал на Delphi непонятно падающего кода: даже в самых сложных случаях успешно отлавливал косяки в сторонних библиотеках, которые при использовании кодов ошибок хрен бы нашел.
Полагаю, что причина скорее в кривости именно плюсовых исключений.
чем
try { } finally { finallyCode(); }
отличается от
try { } catch(...) { } finallyCode();
?
Кроме синтаксического удобства и необходимости писать дополнительный код в случае, если надо пробросить исключение дальше.
Тем что в первом случае я могу вызвать return внутри try и все равно попаду в finally, а во втором — нет.
Во втором случае исключение будет «съедено» в catch, а в первом — пойдёт гулять дальше после выполнения finally
> Но эти затраты становятся актуальными только при возникновении ошибок.

Нет же. Каждый try — это служебный код, выполняющийся всегда.
try..finally работает очень быстро, если не было выброшено исключение. На нем экономить бессмысленно.
Опять мимо. Код-то может быть и небольшим, но в цикле может приводить к замедлению. Особенно из-за cache miss.

А так я руками и ногами за исключения.
Если у вас try..finally в большом цикле — значит имеется кривость с многократным выделением-освобождением одного и того же ресурса вместо переиспользования.
Именно! Автор смотрит на исключения через розовые очки :)

А ещё становится страшно, когда обвязки из try/catch обволакивают весь код и обработанное на верхнем уровне исключение даёт бесконечно малое представление о настоящей проблеме, возникшей где-то в глубине и многократно обработанной и перепроброшенной в изменённой до неузнаваемости форме.
1. Изменять до неузнаваемости незачем — есть хорошая техника цепных исключений.
2. Можно логировать выброшенное исключение сразу — до начала раскрутки стека.
А нечего перехватывать catch'ем исключения, которые должны вылезти наружу в первозданном виде.
Впрочем, неверно обработанный код ошибки вообще вам не даст никакого представления о проблеме. Вы может и не узнаете о ней до поры до времени.
с точки зрения «оптимизации обработки» ошибок мне больше по-сердцу контракты.

С точки зрения процесса разработки — написание контрактов заставляет человека отдельно подумать, что каждая функция получает и что возвращает. Уже само это заметно уменьшает количество ошибок.
И записывается это в простом и адекватном виде логического выражения.

С точки зрения сопровождения — львиную часть неожиданных проблем поймает код, который добавляет компилятор в местах контрактов. На входе, на выходе. Вместо километровых (ни разу не видел адекватных стеков явовских приложений, видимо я очень везучий) traceback'ов — получаешь практически всю информацию необходимую для анализа ситуации.
жаль, во многих языках их нету :(
Сколько раз в процессе штатной работы будет инициировано исключение QueueReadingException? В коде без ошибок — ни разу! Где же тут затраты?

Про ресурсы, дополнительный код и проверки для раскрутки исключений? В C++ они добавляются при компиляции. Если же исключения отключены, то раскрутка стека гарантированно не производится, поэтому оптимизируются инструкции на создание стека (информация для раскрутки не сохраняется, созданный стек будет принципиально нераскручиваевым).
Я уже давно не в теме, но когда-то проверял влияние разных конструкций C++ на итоговую производительность. Помню, что добавление try-catch очень сильно просаживало время выполнения (десяти процентов), даже если никаких исключенний не выбрасоалось и не ловилось. Так что я от них в тот раз воздержался. Разумеется есть куча областей, где эти проценты останутся незамеченными на общем фоне неустранимых затрат.
Ребята, развели холивар на пустом месте. Вы всерьез пытаетесь мерить производительность кода с if-ом и без? Кто вас этому учил? Не сказано ли, имеешь проблему с производительностью — найди узкое место и оптимизируй его? А до тех пор, пока такой проблемы нет идем писать наиболее ясный и читаемый код. А исключений для обработки ошибок лучше еще не придумали в плане читаемости, по крайней мере, для Java.
запятую забыл после «пока такой проблемы нет».
Так в данной конструкции if и является заведомо узким местом.
while (!queue.isEmpty()) {
  Point point = queue.readPoint();
  if (point != null) {
    ...
  }
}

Просто для справки. Я как-то ради интереса в своей боевой программе, активно использующей контейнеры, добавил проверку на out_of_range. Всего один if дал почти трехкратное падение скорости.
У Реймонда Чена тоже есть мнение по поводу использования ислючений, и я в чем-то с ним согласен. Правда, он смотрит не с точки зрения производительности, а с точки зрения поддержки кода.
Мнение Реймонда Чена относится к голому механизму исключений Windows — а там даже не плюсы, а чистый Си и исключение — не объект, а структура.
И что это принципиально меняет — скажем, в его примере с CreateNotifyIcon()? Не нужна будет перестановка присвоений icon.Icon и icon.Visible например? Или то, что эта перестановка не сделана, будет как-то более заметна с точки зрения человека, поддерживающего код? Поясните свою мысль, пожалуйста.
1. Таки не нужна.
2. На Delphi будет заметно нарушение паттерна создающей функции.
Result := TX.Create;
try
  Result.Y := Z;
except
  FreeAndNil(Result);
  raise;
end;


На той же Delphi будет заметен косяк уже дизайна с присваиванием свойству Icon — такие вещи либо создаются внутри объекта, либо он ими не владеет.
1. Причем тут Delphi? Код в примере на C++.
2. Это ничем не лучше варианта без исключений, с простой проверкой на код ошибки/NULL. Основное достоинство исключений в том, что они могут быть обработаны где-то выше в стеке вызовов. Если на каждый чих писать try/catch с чисткой «по месту», то с точки зрения функционала это будет неотличимо от проверки кода возврата. Соответственно основное достоинство исключений как бы пропадает втуне.
1. При том, что в топике идет речь об исключениях без привязки к языку. Если в плюсах они, возможно, кривые, то в Delphi не использовать исключения нет никаких оснований.
2. «Простая проверка» нужна всегда и везде, из-за нее основной алгоритм читается отвратительно, пропуск «простой проверки» может дать видимый косяк совсем не в том месте, где реально допущена ошибка. Исключение же сразу даст в лоб и точно обозначит место.
На каждый чих в Delphi ничего писать не надо: например, выброс исключения в конструкторе автоматически вызовет деструктор, который освободит все выделенные ресурсы. Для использования try..except есть ровно три основания, я их перечислил в одном из комментариев выше.
1. Ну на Delphi я, если честно, писал давно, ничего не могу сказать на эту тему.
2. try/catch на каждый чих читается не менее отвратительно. Пропуск try/catch и соответствующей «чистки по месту» может дать видимый косяк совсем не в том месте, где реально допущена ошибка — например, в примере Чена это может вызвать попытку обращения к несуществующей иконке невесть где, возможно, даже в сторонней библиотеке. И выяснить, почему приложение падает, будет очень сложно. Исключение само по себе тут ничем не поможет — оно выбрасывается, и даже, возможно, обрабатывается где-то выше, но при отсутствии чистки icon.Visible это бесполезно, а код выше может даже не знать о том, что такое icon, и что у него есть свойство Visible.

Чен и не говорит, что исключения — это плохо. Он говорит, что заметить ошибку в обработке исключений, особенно если они обрабатываются не немедленно, а где-то выше по стеку, и когда все очень сильно зависит от порядка выполнения действий (а с исключениями это очень важно, и, скажем так, менее очевидно, чем в случае обработки кодов возврата, когда ты прямо-таки ВЫНУЖДЕН чистить данные «по месту» в случае ошибки), очень сложно. В этом плане я с ним согласен.
Итак, еще раз.
try..except на каждый чих писать НЕ НАДО, в отличие от обработки кодов ошибок. Более того, при использовании исключений чистый алгоритм и обработку ошибок можно разнести по разным программным единицам, что, кстати, для Java советуется прямо.
Про видимый косяк вы ошибаетесь — в Delphi исключение можно залогировать вместе со стеком в момент выброса.
Вдобавок все, что может сделать пропущенное исключение — это всплыть строго наверх, демаскировав ошибку. Пропущенный код ошибки может всплыть в любом коде впоследствии.
«например, в примере Чена это может вызвать попытку обращения к несуществующей иконке невесть где, возможно, даже в сторонней библиотеке.»
Это уже косяк дизайна. Если создаваемый объект без иконки невалиден — то он не должен конструироваться без нее. А если валиден — то код обязан учитывать этот случай. Исключения тут вообще не при чем.
«но при отсутствии чистки icon»
Отсутствие такой чистки есть косяк библиотечного кода, исключения тут снова не при чем. Если же создаваемый класс иконкой не владеет — то весь приведенный пример есть ошибка создания объекта, которым никто не владеет.
Чен говорит про сложности, которых в Delphi просто нет. Советую посмотреть последнее предложение в статье:
«Yes, there are programming models like RAII and transactions, but rarely do you see sample code that uses either.» — так вот, в Delphi оно так и есть изначально. Любой конструктор либо создает валидный объект, либо бросает исключение, автоматически вызывая деструктор, очищая все захваченные в конструкторе перед сбоем ресурсы.
try..except на каждый чих писать НЕ НАДО, в отличие от обработки кодов ошибок. Более того, при использовании исключений чистый алгоритм и обработку ошибок можно разнести по разным программным единицам, что, кстати, для Java советуется прямо.

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

Про видимый косяк вы ошибаетесь — в Delphi исключение можно залогировать вместе со стеком в момент выброса.

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

И вы опять почему-то ссылаетесь на особенности Delphi. Исключения есть много где кроме него, и места, где могут создаваться и модифицироваться объекты (в широком смысле) не ограничены конструкторами.
«Если вы пишете с проверкой кодов возврата, то, увидев вызов без последующей проверки, вы сразу задумаетесь: «а почему? Нет ли здесь возможной ошибки?».»
Ага — необработанные ошибки видны на ревью. Зато что делает код, когда все идет по плану — не видно от слова «совсем». В случае, если ревью таки пропустит косяк — при выполнении коды ошибок замаскируют его надежнее чем хамелеона. Исключение же заявит о себе прямо и недвусмысленно.
В результате:
1. Коды ошибок облегчают контроль кода на предмет обработки особых случаев.
2. Коды ошибок ухудшают контроль кода на предмет выполнения основной задачи.
3. Во время выполнения коды ошибок маскируют сбои.
«Толку от этого? Исключение было штатно обработано, а ошибка при отображении этого icon может возникнуть за тысячу миль от этого исключения»
Вы умеете читать то, что написано? Я этот случай разобрал комментарием выше. Ваш пример — ошибка дизайна.
Вот именно, что я пишу про особенности Delphi, которые позволяют эффективно использовать исключения, а вы — про особенности C++, которые этому мешают. Только я, в отличие от вас, кривости конкретной реализации на всю концепцию не натягиваю.
1. Коды ошибок облегчают контроль кода на предмет обработки особых случаев.

Согласен.

2. Коды ошибок ухудшают контроль кода на предмет выполнения основной задачи.
3. Во время выполнения коды ошибок маскируют сбои.


Это вы с чего придумали? Проверка кодов ошибок никогда ничего не «ухудшала» и не «маскировала». Вот конструкции типа:

try {
[… some shit from another module ...]
} catch (Exception) {
// Ignored or incorrectly handled
}

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

Вы умеете читать то, что написано? Я этот случай разобрал комментарием выше. Ваш пример — ошибка дизайна.

А вы поддерживаете только тот код, что сдизайнен вами лично? Если вам не нравится дизайн сторонней библиотеки, вы ее тут же переписываете? Тогда я искренне рад, что у вас на это есть время.
«Проверка кодов ошибок никогда ничего не «ухудшала» и не «маскировала».»
+ 3 строки кода и + 1 отступ на каждую проверку кода ошибки внутри реализации алгоритма никак не ухудшают читабельность кода только в фантастике.
Пропуск хотя бы одной проверки кода ошибок на ревью будет наглухо замаскирован при выполнении. Идеальные ревьюеры, которые выловят все-все-все — снова только в фантастике.
«А вы поддерживаете только тот код, что сдизайнен вами лично?»
Плохо сдизайненый чужой код оборачивается в хорошо сдизайненый свой. После чего везде используется обертка.
+ 3 строки кода и + 1 отступ на каждую проверку кода ошибки внутри реализации алгоритма никак не ухудшают читабельность кода только в фантастике.

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

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

Опять-таки, на уровне обертки не может быть 100% четкого понимания ни внутренних структур оборачиваемого кода, ни особенностей используемых алгоритмов. Возвращаясь к примеру с icon — ну передали в вашу обертку структуру с icon.Visible = true и icon.Icon, указывающим «куда-то». И что? Будете пытаться отвалидировать адрес Icon? Не всякий аллокатор это позволит. Будете пытаться сами обратиться по этому адресу? Если адрес невалиден, может прокатить сейчас, но обломаться через миллисекунду — опять-таки зависит от тонкостей работы аллокатора. В общем, тухлая идея на мой взгляд.
«Не ухудшают, представьте себе.»
Не представляю — разве что в качетве толстого троллинга. Исключения были введены в структурное программирование именно как средство избегать многоступенчатого защитного кода, в котором непонятно, что собственно защищают.
«где уже нет четкого представления ни о внутренних структурах, ни об особенностях кода.»
Выбрасываемые исключения описываются на уровне интерфейса, внутренние структуры и особенности кода там без разницы. Там, где само исключение теряет прежний смысл — его ловят и перевыбрасывают в новом значении. Только вот это надо куда реже, чем «простая проверка». И цепочку исключений отследить куда проще, чем цепочку проверок.
«Опять-таки, на уровне обертки не может быть 100% четкого понимания ни внутренних структур оборачиваемого кода, ни особенностей используемых алгоритмов.»
Обертка затем и нужна, чтобы инкапсулировать в себе изъяны дизайна того, что оборачивается, а снаружи мне особенности чужого кода даром не сдались. Мне задачи надо решать, а не мучиться с инструментом, городя гекатомбы проверок на каждый чих.
Исключения были введены в структурное программирование именно как средство избегать многоступенчатого защитного кода, в котором непонятно, что собственно защищают.

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

Выбрасываемые исключения описываются на уровне интерфейса, внутренние структуры и особенности кода там без разницы.

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

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

Набор слов. Как вы можете «инкапсулировать изъяны дизайна», если вы не знакомы с этими изъянами? Если вы не можете даже понять, валидны передаваемые вашей оберткой в библиотеку данные или нет? Напоминаю, что библиотека сторонняя, написана на C, и не сможет выбросить исключение в ответ на внутреннюю ошибку работы с данными. Она просто упадет по SIGSEGV, и привет.

Ладно, в общем, у меня больше вопросов нет. Кому надо, тот, прочитав нашу увлекательную дискуссию, вынесет для себя что-то полезное.
Спасибо.
Если я правильно понял, то вы полагаете, что ваш пример говорит сам за себя.
Давайте разберем его.
Мелкие хитрости вроде записи if в одну строчку оставим за кадром.
Крупный косяк — версия на исключениях почему сама возвращает невалидные данные, т.е. написана в стиле кодов ошибок.
Еще более крупный косяк — не указано, является ли загрузка из дополнительных источников аварийным вариантом или полноценной альтернативой, а также является ли невозможность загрузки из всех трех мест сбоем.
Если все это — штатные альтернативы, а не сбой, то я бы написал так:
function TryGetConfig(out AConfig: IXML): Boolean;
begin
  Result := 
    TryReadXmlFromFile( userProfile.go( 'config.xml' ), AConfig) or
    TryReadXmlFromFile( defaultProfile.go( 'config.xml' ), AConfig) or
    TryDomParse( '<config/>', AConfig);
end;

Чем это лучше варианта по ссылке?
1. Сама сигнатура функции явно говорит о возможности не вернуть результат.
2. Если результат возвращается — он всегда валиден.
3. Внешний код делает вызов и проверку одним оператором if.
4. Никаких неявных условий про 0 ни в коде функции, ни в сигнатуре.

Если же неудачная из первичного источника — сбой, то в этом случае буду писать на исключениях:
function GetConfig: IXML;
begin
  try
    Result := ReadXmlFromFile(userProfile.go( 'config.xml' );
  except
    on E: XMLError do
    try
      Result := ReadXmlFromFile(defaultProfile.go('config.xml'));
    except
      on E: XMLError do
      begin
        Result := DomParse('<config/>', AConfig);
      end;
    end;
  end;
end;

Чем это лучше варианта по ссылке?
Обрабатывается только конкретный класс ошибок.
Чем это лучше варианта с кодами ошибок?
1. Явно виден бархатный путь — чтение основного конфига.
2. Сбои можно сразу фиксировать в логе со стеками.
3. Диагностика ошибок времени исполнения и выбранная ветка легко делаются на основе логов.
Как его можно улучшить?
Перевыбрасывать исключение EConfigError, если ошибки XML вызывающему коду неинтересны.
бархатный путь простой:
прочитать конфиг юзера
если его нет, он битый или упал метеорит — прочитать дефолтный конфиг
если метеорит и дефолтный задел — создать пустой.

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

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

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

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

var obj= readXmlFile( xmlFile )
if( !obj ) throw Exception( 'не создался объект потому что' + readXmlFile.lastError )

наоборот же преобразовать более накладно:
try {
var obj= readXmlFile( xmlFile )
} catch( e ){
logError( e )
obj= null
}

null вместо объекта — вполне себе такой стандартный флаг означающий «нет данных», но разумеется не расшифровывающий «почему их нет». если нужны подробности — всегда можно сделать проверки до или после запроса.
Ваши два примера кода нерелевантны, так как оба работают в стиле кодов ошибок, используя разные вспомогательные процедуры, и показывают не преимущества кодов ошибок, а издержки использования библиотек с исключениями в программах на кодах ошибок. Мои два примера ведут себя ПО-РАЗНОМУ, сосредотачиваясь на требованиях к функции — один всегда возвращает валидные данные или ничего, второй — валидные данные или сообщение о сбое в виде исключения. Во втором случае возможна подробная оперативная диагностика и логирование, в первом — клиент явно поставлен в известность, что данных может не быть.
На каждый чих никаких двух вариантов не нужно — бизнес-логика и особые случаи разделяются еще при проектировании решения задачи. В некоторых ситуациях такое бывает (например, дотнетные Parse и TryParse), но с одной стороны, это достаточно редко, с другой — совершенно не трудоемко.
«null вместо объекта — вполне себе такой стандартный флаг означающий «нет данных», но разумеется не расшифровывающий «почему их нет». если нужны подробности — всегда можно сделать проверки до или после запроса.»
На моей практике null вместо объекта — это вполне себе стандартный говнокод.
Почему такая оценка?
1. Сигнатура функции нагло врет, что возвращает объект.
2. Все места использования результата (с точностью минимум до подпрограммы) надо окружать уродливыми проверками на null, причем, так как по сигнатуре не догадаешься, это придется делать и для нормальных функций.
3. Отсутствие такой проверки прекрасно работает до момента X, после чего выдает access violation совсем не там, где вернулся null.
4. Проверки до запроса — это вообще нонсенс. Я запрашиваю что-либо не для того, чтобы проверять возможность возврата самому заранее: это будет грубым нарушением инкапсуляции.
Пример с «readXmlFile.lastError» — тоже стандартный говнокод — на пустом месте введено дополнительное состояние, да еще и невалидное, да еще и в виде глобальной переменной. Большой привет от отладки, очень большой от многопоточности.
Пример с изготовлением null из всех исключений — тоже стандартный говнокод, кстати, рассматриваемый в любом руководстве как пример неправильного использования исключений: глушится все, включая заведомо фатальные сбои.
Кстати, как в вашем бархатном пути отработает следующий сценарий:
1. Копируем дефолтный конфиг на 2000 строк в рабочий.
2. Вносим небольшое, но важное для критических ситуаций под нагрузкой изменение.
3. Допускаем ошибку, порождая невалидный конфиг.
4. Ваше приложение в результате молча грузит дефолтный конфиг, никак не сообщая о косяке и прекрасно работает.
5. Происходит критическая ситуация — приложение падает из-за той самой настройки.
6. Никаких следов того, что использовался дефолтный конфиг вместо рабочего, нигде нет.
7. Счастливой отладки!
Это, кстати, простейший пример на коленке, на практике встречаются случаи гораздо хуже и запутаннее.
Например, плавающий memory corruption из-за кода в сторонней библиотеке, отловленный и исправленный только по исключениям в логе. А с кодами ошибок было бы неясно даже где искать.
эти два примера кода показывают как преобразуется один стиль в другой. преобразовать ошибку в исключение легко. а вот наоборот — сложно. поэтому исключения годятся, когда в большинстве случаев нужно падать. а вот если в большинстве случаев нужно постараться выжить, то код на исключениях превращается в какашку. в приложениях, работающих с пользователем в большинстве случаев при получения данных, если произошёл сбой, нужно сгенерировать дефолтные данные, а не падать. а для отладки существуют логи, кода пишутся все возникшие в процессе работы ошибки. пр желании можно во время отладки включить падение при любых ошибках, но незачем тащить эту отладочную панику на продакшен.

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

2. не все места, а где разница между положительным и дефотным важна. в твоём же случае придётся откружать уродливыми try-catch все места, где она не важна.

3. я не вижу проблемы. смотрим по логам, почему не создался такой-то объект и добавляем проверку в результате которой возвращаем дефолт.

4. не вижу принципиальной разницы между «проверить и прочитать» и «прочитать, а если не удалось — проверить». кроме той, что в первом случае проверка опциональна, а во втором перехват обязателен иначе у дома рухнет пол стены.

5. а кто сказал, что readXmlFile — это глобальная функция? это вполне себе привязанный к потоку инстанс.

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

касательно битого критического конфига — да, приложение грузит дефолтный конфиг и _продолжает работать_ попутно записывая в лог ошибку парсинга, по которой можно спокойно и без паники диагностировать проблему, не вставляя экстренный try-catch, чтобы это хоть как-то заработало сейчас, пока идёт исправление бага. кстати, это беда многих разработчиков на исключениях и тебя видимо в том числе. вы когда глотаете исключения (чтобы приложение не падало, продолжало работать), то забываете, что проглоченные ошибки (и не важно все ли исключения перехватывались или только конкретное подмножество) _надо логировать_, иначе это действительно будет пипец при отладке.
1. Очень смешно, но проект, который делает моя команда, выполнен в виде трехзвенки, причем клиент и сервер приложений имеют плагинную архитектуру собственной разработки. Падение любого из звеньев, представьте себе, не приводит к потере данных.
2. Логирование всех выбрасываемых исключений сделано автоматическим — никакого специального кода для логирования во всех местах, где могут быть ошибки, писать не надо. Если выброшено исключение — оно уже в логе со стеком.
3. Вы скатились на банальное хамство — полагаю дальнейшую дискуссию излишней.

1. падение приложения и потеря данных — это перпендикулярные понятия.

2. прям совсем всех? ну круто, чо. только зачем после записи в лог ещё и пытаться убить всё приложение исключением? чтобы сообщить пользователю, что разработчик-криворучка допустил ошибку? пользователю на это положить. ему надо чтобы хоть что-то работало. вот у меня например на virtual pc появляются графические артефакты, но это гораздо лучше, чем бсод вызываемый virual box.

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

Вдумайтесь в терминологию: throw и catch, бросить и поймать — очень ёмкие понятия. Исключения нужны для того что бы их ПРОБРАСЫВАТЬ через каскад функций, которые вызывают друг друга. Написал бы статейку, но кармы не хватает.
Спасибо всем, кто поверил в меня :) К сожалению, кармы не хватило, что бы опубликовать в разделе «разработка», по этому опубликовал в «php»:
habrahabr.ru/blogs/php/130597/
В плюсах эсцепшны — единственный способ грохнуть конструктор, если объект невозможно сконструировать. Да, можно делать флаг типа isValid(), но семантически оно ничуть не лучше try..catch — его же надо проверять и соовтетственно обрабатывать невалидные объекты, что ведет к куче защитного кода.
Ну и плюс, выбирая между
int returnSomething(Something *out)
и
Something &returnSomething()
я выбираю второе как более интуитивное. А эксцепшны… ну что эксцепшны.
Конечно, во втором случае можно возвращать указатель и при ошибке NULL. Зависит от ситуации, что лучше, а это — просто пример.

Однако на каждый чих кидать эксцепшн тоже не стоит. Правило левой руки для меня такое: использовать исключения только если без них заметно сложнее (как с конструкторами) или для действительно исключительных режимов работы. Часто это пересекающиеся области.
И да, как пишет resurection выше, исключения незаменимы для выхода из каскада функций. Как дешевую замену return их конечно использовать нельзя, но это гораздо лучше, чем в каждой функции писать защитный код.
Использование исключений в нештатных ситуациях (таких как обрыв сетевого соединения) это хорошо,
а вот для ситуаций, когда функция может иметь или не иметь результата (как в случае в getPoint) есть интересный тип данных Maybe (haskell) или Option (scala):

def point: Option[Point]
val withDefault = point.getOrElse(new Point(NaN,NaN))
val throwIfNone = point.get
val orNull = point.getOrNull
val complexResult = for {
    p1 <- point
    p2 <- point
} 
    yield distance(p1,p2) // Some(distance) only if p1 and p2 are Some


В той же java будут проблемы с накладными расходами на создание лишних объектов, а так же сказывается отсутствие такого инструмента как scala:for, но подход интересный — null это все-таки не объект, а Option вполне полноценный результат, обработку которого решает уже прикладной программист, а не разработчик библиотеки.
> тип данных Maybe

И еще интереснее — Either. Кстати, я скоро переведу 5-ю часть YAMT именно про Either.
Это немного оффтоп )
Это другой взгляд на обработку ошибок и исключений. Я думаю, для общего развития нужно видеть больше одной-двух точек зрения. )
Такой функциональный вариант очень хорош, но увы, прежде всего в функциональных языках, позволяя возложить контроль наличия обработки особых случаев на компилятор. Без алгебраических типов результат получается громоздкий и ненадежный.
С таким контролем справится любой компилятор, проверяющий типы. Конечно использование этого типа не получится таким же красивым (в первую очередь из-за отсутствия монад и сопоставления с образцом), но вообще подход достоин внимания как иная точка зрения.
Вообще использование функциональных подходов в нефункциональных языках часто дает неплохие результаты (например избегание изменяемых данных).
Не справится — а хаскель даст ошибку компиляции, если при разборе Maybe пропустить ветку с Null.
Громоздкий же некрасивый код будет априори хуже читаться.
Хотя сама техника очень симпатичная и изящная, не вопрос.
С неизменяемыми данными (и объектами) — двумя руками за, экономит просто дикое количество времени как на проектирование, так и на отладку, плюс еще и памяти частенько меньше жрет.
Sign up to leave a comment.

Articles