vsb>Какие есть серьёзные аргументы против исключений и за (извиняюсь за каламбур) возврат к кодам возврата?
Недавно как раз видел:
From the caller's perspective, language support for unchecked exceptions means something like:
"On any function call, instead of returning with a result with the expected type, a function may have the effect of running any destructors in scope, and then exiting the function to repeat the process, skipping all the actual instructions you wanted to execute or returning the type you were specified to return. Were you doing anything but calling functions in a linear sequence? I hope not, because the order in which they were called is going to affect your program's behavior now! Don't like it? Too bad!
So anyway, you never know whether this will happen or not. It's totally nondeterministic. You have no idea how often it happens, whether it's supposed to happen, if it can happen at all, anything. It's okay, though, because you can totally plan ahead and wrap it in a try block, and then if you have a "catch" block that accepts a type T, it might be called with data of that type, instead of returning from the function!
It might not, though. You don't really know. Make sure your code works if it does or doesn't.
But wait, if you have a finally clause after your try, that always runs, whether an exception was thrown or not! I mean, even if you return in the middle of the function, this block still runs! So that's pretty useful, no gotchas there! It's really nice, because it's the only part of this system where you can just write straightforward code!
(Just don't, like, call any functions or anything, because if you do that you might overwrite the exception type. Well, I guess you don't know if there's an exception type in scope or not. I guess just be, like, extra careful? Oh, and don't return in the finally block, that's bad news, that swallows the type too. Actually, don't do any nontrivial control flow at all. Okay, maybe it's not totally like normal code.)
Anyway, any time you call a function, make sure you keep all that in mind! And make sure that none of the above listed ways you can exit from your function can ever leave any of your state inconsistent in a way that violates the caller's expected invariants, because it would be pretty embarrassing if the caller decided to catch the exception at an inopportune moment! Oh, and make sure any destructors in scope also can't operate on their types while they're in an inconsistent state, or that could cause extreme terribleness!"
You have to think about this for every function call. And that's just the tip of the iceberg. I didn't even go into the various ways that exceptions screw you over in practice. Let's take Java, for example (which does not have destructors and does have checked exceptions, which are actually somewhat tolerable):
* Unchecked exceptions can actually happen basically anywhere. Not just inside functions. Because the Java specification raises exceptions for things like stack overflows, VM internal errors, allocation failure, etc. And Java also allocates everywhere.
* By the way, implicit conversions from unboxed to boxed primitives are function calls in this context and can thus trigger these errors.
* Thanks to the magic of inheritance, you generally can't actually enumerate all the possible error types (unless you are the that one Java developer who hates OOP and makes every class final. To that Java developer: I like you!). But that's okay, because most people just create a single error type and put 30 different exceptions under it, and just throw that from all their functions. If you're lucky.
* Also, thanks to Thread.stop, checked exceptions of any type can happen anywhere.
* Also, if you catch Throwable, you screw over anyone who was listening for a different type of exception.
* ALSO, InterruptedException has special semantics where when it's caught, it unsets the isInterrupted flag. Wisely, most people just make everything a RuntimeError or an IOException and wrap the old exception, so now you lose the flag and can't see the exception!
* Did I mention that the above is what Oracle recommends you do to use exceptions with try-with-resources?
* If you call a reflection API, you just get one exception type and get to unwrap it at runtime!
Every function call.
The "elegance" of (non-typesafe) exceptions is a myth. They're awful. You use them when you don't have an alternative.
Здравствуйте, D. Mon, Вы писали:
vsb>>Какие есть серьёзные аргументы против исключений и за (извиняюсь за каламбур) возврат к кодам возврата?
DM>Недавно как раз видел: DM>
Let's take Java, for example (which does not have destructors and does have checked exceptions, which are actually somewhat tolerable):
DM>* Unchecked exceptions can actually happen basically anywhere. Not just inside functions. Because the Java specification raises exceptions for things like stack overflows, VM internal errors, allocation failure, etc. And Java also allocates everywhere.
DM>* By the way, implicit conversions from unboxed to boxed primitives are function calls in this context and can thus trigger these errors.
Так в С++ как раз совершенно другая картина, именно из-за того, что 1) исключения не летят откуда угодно, потому что нет рантайма, который бы их бросал, и 2) из-за наличия деструкторов, в которые ты кладешь код очистки/отката в случае неудачи — именно за счет деструкторов в С++ достигается автоматическая обработка исключений.
В D тем же самым занимается scope(failure).
А в Java этого нет, да. Нам их тоже жалко
Цитата относится больше к Java, чем не к исключениям вообще.
Здравствуйте, WolfHound, Вы писали:
S>>Исключения это очень медленно и размашисто, по сравнению с if(func()!=0) err(); WH>Это не правда. Правильно реализованные исключения работают быстрее кодов возврата.
"Не всё так однозначно" (c).
DWARF-исключения заметно раздувают код для обработки ошибочных путей, а таблицы исключений заметно давят на кэш процессора. Так что использовать исключения для нормального flow-control'а — очень неэффективно.
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, smeeld, Вы писали:
S>>А тестами можете размахивать на праздных академических симпозиумах и конференциях. S>>При разработки кода для дикого и ответственного продакшена, ориентироваться надо S>>на тонкое знание реализации тех или иных используемых систем, по которому определять характер S>>использования этих систем. WH>Главное при этом не забыть, что в 99.999% случаев код возврата содержит значение "всё хорошо". WH>А значит на практике всё то чем ты тут размахиваешь просто тает на фоне постоянных if (errorcode != ok)... WH>Что бы получить реалистичный тест сделай пять функций, которые друг друга вызывают. Запрети компилятору их инлайнить. WH>Вызови их 100000 раз. А на 100001 скажи, что всё плохо. WH>И сравни время выполнения.
int func1_1(int a)
{
if (a > 1000000)
return -1;
else
return a + 1;
}
int func1_2(int a)
{
if (a > 1000001)
return -1;
else
return func1_1(a);
}
int func1_3(int a)
{
if (a > 1000002)
return -1;
else
return func1_2(a);
}
int func1_4(int a)
{
if (a > 1000003)
return -1;
else
return func1_3(a);
}
int func1_5(int a)
{
if (a > 1000004)
return -1;
else
return func1_4(a);
}
int func2_1(int a)
{
if (a > 1000000)
throw new Exception("qq");
return a + 1;
}
int func2_2(int a)
{
return func2_1(a);
}
int func2_3(int a)
{
return func2_2(a);
}
int func2_4(int a)
{
return func2_3(a);
}
int func2_5(int a)
{
return func2_4(a);
}
private string Run(string title, int max)
{
var start1 = DateTime.Now;
int res1 = 0;
for (int i = 0; i < max; i++)
{
res1 += func1_5(i);
}
var end1 = DateTime.Now;
var start2 = DateTime.Now;
int res2 = 0;
for (int i = 0; i < max; i++)
{
int val;
try
{
val = func2_5(i);
}
catch
{
val = -1;
}
res2 += val;
}
var end2 = DateTime.Now;
return String.Format("{0}: {1} ({2}), {3} ({4})", title, end1 - start1, res1, end2 - start2, res2);
}
private void button1_Click(object sender, EventArgs e)
{
Run("test run", 1000010);
var line1 = Run("Single exception", 1000002);
var line2 = Run("Two exceptions", 1000003);
System.IO.File.WriteAllLines("D:\\temp.txt", new[] { line1, line2 });
}
Компиляция — "Release".
Результат:
Single exception: 00:00:00.0311996 (1785293664), 00:00:00.0467994 (1785293664)
Two exceptions: 00:00:00.0155998 (1785293663), 00:00:00.0623992 (1785293663)
Вот это коды возврата слили так слили — аж в 6 раз быстрее в варианте с двумя исключениями.
И даже с одним исключением на миллион вызовов все равно быстрее.
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, smeeld, Вы писали:
S>>А тестами можете размахивать на праздных академических симпозиумах и конференциях. S>>При разработки кода для дикого и ответственного продакшена, ориентироваться надо S>>на тонкое знание реализации тех или иных используемых систем, по которому определять характер S>>использования этих систем. WH>Главное при этом не забыть, что в 99.999% случаев код возврата содержит значение "всё хорошо". WH>А значит на практике всё то чем ты тут размахиваешь просто тает на фоне постоянных if (errorcode != ok)... WH>Что бы получить реалистичный тест сделай пять функций, которые друг друга вызывают. Запрети компилятору их инлайнить. WH>Вызови их 100000 раз. А на 100001 скажи, что всё плохо. WH>И сравни время выполнения.
Это бред, потому что в любом случае в машинном коде ифы будут — исключения это будут, коды возврата или что угодно еще. Проверки либо есть, либо их нет (тогда просто будет UB).
Здравствуйте, jazzer, Вы писали:
J>Так в С++ как раз совершенно другая картина, именно из-за того, что 1) исключения не летят откуда угодно, потому что нет рантайма, который бы их бросал, и 2) из-за наличия деструкторов, в которые ты кладешь код очистки/отката в случае неудачи — именно за счет деструкторов в С++ достигается автоматическая обработка исключений. J>В D тем же самым занимается scope(failure). J>А в Java этого нет, да. Нам их тоже жалко J>Цитата относится больше к Java, чем не к исключениям вообще.
В Java есть try-with-resources, он ничем не хуже вышеперечисленных вариантов. Аналог scope(failure) это try-finally. Разница только в том, что в try-finally код освобождения ресурса не находится рядом с объявлением и добавляется один отступ для внутреннего кода. Это менее удобно, но принципиально ничего не меняет.
Разве что деструктор невозможно забыть вызвать, но в принципе в Java есть статические анализаторы кода, которые тебе подскажут, если ты забыл закрыть ресурс. Проблемой при хорошей организации процесса я это не считаю.
Здравствуйте, AlexRK, Вы писали:
ARK>Это бред, потому что в любом случае в машинном коде ифы будут — исключения это будут, коды возврата или что угодно еще. Проверки либо есть, либо их нет (тогда просто будет UB).
откуда ифы при работе с исключениями? Вся машинерия исключений запускается только при вызове throw.
Здравствуйте, vsb, Вы писали:
vsb>Здравствуйте, jazzer, Вы писали:
J>>Так в С++ как раз совершенно другая картина, именно из-за того, что 1) исключения не летят откуда угодно, потому что нет рантайма, который бы их бросал, и 2) из-за наличия деструкторов, в которые ты кладешь код очистки/отката в случае неудачи — именно за счет деструкторов в С++ достигается автоматическая обработка исключений. J>>В D тем же самым занимается scope(failure). J>>А в Java этого нет, да. Нам их тоже жалко J>>Цитата относится больше к Java, чем не к исключениям вообще.
vsb>В Java есть try-with-resources, он ничем не хуже вышеперечисленных вариантов.
Я, конечно, давно на это смотрел в последний раз, но, насколько я помню, try-with-resources работает только для совсем примитивных случаев.
Т.е. для одного файла он сработает, а вот для массива файлов — уже нет.
Плюс надо явно указывать все, что ты хочешь, чтоб освободилось в случае исключения, никакой автоматики, как в С++, нет.
Плюс там могут быть только декларации, если хочешь в промежутке между декларациями позвать еще какой-то код — придется писать еще один уровень try/catch.
Поправь, если не так.
vsb>Аналог scope(failure) это try-finally. Разница только в том, что в try-finally код освобождения ресурса не находится рядом с объявлением и добавляется один отступ для внутреннего кода. Это менее удобно, но принципиально ничего не меняет.
В Java можно сделать автоматическое Memento (в смысле, чтоб в случае исключения указанная переменная возвращалась к своему предыдущему значению автоматически)?
Ну и в С++ вообще try/catch очень редко появляется в коде.
Именно за счет автоматики деструкторов.
Совершенно нет никакой необходимости использовать try/catch, чтобы написать exception-safe код.
Большинство такого кода вообще try/catch не содержит, а пишется в стиле
Memento m1(a);
// что-то делаем (исключения OK)
File f;
// что-то делаем (исключения OK)
Memento m2(b);
// что-то делаем (исключения OK)
DBConnection db;
// что-то делаем (исключения OK)
// закончили делать
// раз мы пришли сюда, значит, никаих исключений не случилось, коммитим изменения
m1.commit();
m2.commit();
всё. Этот код exception-safe. Где бы ни появилось исключение — a и b корректно откатятся к старым значениям, файлы/сессии закроются, всё автоматически. И никаких try/catch, простой линейный код.
vsb>Разве что деструктор невозможно забыть вызвать, но в принципе в Java есть статические анализаторы кода, которые тебе подскажут, если ты забыл закрыть ресурс. Проблемой при хорошей организации процесса я это не считаю.
Хм. Ну это и про goto можно сказать — что недостаток языка компенсируется правильной организацией процесса и статическими анализаторами кода.
Здравствуйте, jazzer, Вы писали:
J>Здравствуйте, AlexRK, Вы писали:
ARK>>Это бред, потому что в любом случае в машинном коде ифы будут — исключения это будут, коды возврата или что угодно еще. Проверки либо есть, либо их нет (тогда просто будет UB).
J>откуда ифы при работе с исключениями? Вся машинерия исключений запускается только при вызове throw.
Да, наверно тут я не прав. Я имел в виду изначальные проверки в том месте, где может быть ошибка — от них никуда не деться. Переполнение там, деление на ноль или еще чего-нибудь. Выше по иерархии вызовов проверок уже не будет (впрочем, смотря как реализовать).
Здравствуйте, jazzer, Вы писали:
J>>>Так в С++ как раз совершенно другая картина, именно из-за того, что 1) исключения не летят откуда угодно, потому что нет рантайма, который бы их бросал, и 2) из-за наличия деструкторов, в которые ты кладешь код очистки/отката в случае неудачи — именно за счет деструкторов в С++ достигается автоматическая обработка исключений. J>>>В D тем же самым занимается scope(failure). J>>>А в Java этого нет, да. Нам их тоже жалко J>>>Цитата относится больше к Java, чем не к исключениям вообще.
vsb>>В Java есть try-with-resources, он ничем не хуже вышеперечисленных вариантов.
J>Я, конечно, давно на это смотрел в последний раз, но, насколько я помню, try-with-resources работает только для совсем примитивных случаев. J>Т.е. для одного файла он сработает, а вот для массива файлов — уже нет.
try-with-resources вызывает метод close(). Можно имплементировать интерфейс Closeable и делать там что угодно. Можно создать класс CloseableFiles, оборачивающий массив ресурсов и всё будет работать.
J>Плюс надо явно указывать все, что ты хочешь, чтоб освободилось в случае исключения, никакой автоматики, как в С++, нет. J>Плюс там могут быть только декларации, если хочешь в промежутке между декларациями позвать еще какой-то код — придется писать еще один уровень try/catch. J>Поправь, если не так.
Всё так, но это вопрос удобства. О забытом закрыть Closeable предупредит статический анализатор. Лишний уровень вложенности — заводим новую функцию. Деструкторы удобней, но существенной разницы нет.
vsb>>Аналог scope(failure) это try-finally. Разница только в том, что в try-finally код освобождения ресурса не находится рядом с объявлением и добавляется один отступ для внутреннего кода. Это менее удобно, но принципиально ничего не меняет.
J>В Java можно сделать автоматическое Memento (в смысле, чтоб в случае исключения указанная переменная возвращалась к своему предыдущему значению автоматически)?
Можно чего-нибудь придумать, но выглядеть будет не очень, главным образом потому, что нет возможности передать переменную по ссылке, чтобы иметь возможность изменять её значение. Если бы была такая возможность, можно было бы сделать так:
memento(a, {
...
});
void memento(T& var, Function body) {
T valueBefore = var;
try {
body();
} catch (Exception e) {
var = valueBefore;
}
});
к сожалению +1 уровень вложенности аналогично try-with-resources, ну и такое не работает, потому что нет передачи по ссылке/указателю, только передача по значению. В общем то можно сделать класс-указатель, class Ref<T> { T value; }, но это уже совсем некрасиво будет.
vsb>>Разве что деструктор невозможно забыть вызвать, но в принципе в Java есть статические анализаторы кода, которые тебе подскажут, если ты забыл закрыть ресурс. Проблемой при хорошей организации процесса я это не считаю.
J>Хм. Ну это и про goto можно сказать — что недостаток языка компенсируется правильной организацией процесса и статическими анализаторами кода.
Ну в goto в ограниченном количестве (break/continue/return) ничего плохого нет. А с безудержной фантазией можно что-угодно сделать нечитаемым, имхо. Хотя это вопрос на отдельный холивар.
Здравствуйте, Cyberax, Вы писали:
C>DWARF-исключения заметно раздувают код для обработки ошибочных путей, а таблицы исключений заметно давят на кэш процессора. Так что использовать исключения для нормального flow-control'а — очень неэффективно.
А для нормального их никто и не использует.
Они нужны для того чтобы обработать ситуацию когда что-то пошло не так.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, AlexRK, Вы писали:
ARK>Вот это коды возврата слили так слили — аж в 6 раз быстрее в варианте с двумя исключениями. ARK>И даже с одним исключением на миллион вызовов все равно быстрее.
1)А где у тебя тут коды возврата?
Не вижу. То как ты реализовал функции func1_* к кодам возврата отношения не имеет.
try/catch в реальном приложении стоит снаружи цикла.
2)Я что-то с ходу не нашел как исключения в .NET реализованы.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, WolfHound, Вы писали:
WH>1)А где у тебя тут коды возврата? WH>Не вижу. То как ты реализовал функции func1_* к кодам возврата отношения не имеет.
Да? То есть проверка результата malloc — это не код возврата?
WH>try/catch в реальном приложении стоит снаружи цикла.
Когда снаружи — для одного исключения ничего не меняется, я проверял.
WH>2)Я что-то с ходу не нашел как исключения в .NET реализованы.
Так сойдет?
int func1_1(int a, out int result)
{
result = a + 1;
if (a > 1000000)
return -1;
else
return 0;
}
int func1_2(int a, out int result)
{
if (func1_1(a, out result) == 0)
return 0;
else
return -1;
}
int func1_3(int a, out int result)
{
if (func1_2(a, out result) == 0)
return 0;
else
return -1;
}
int func1_4(int a, out int result)
{
if (func1_3(a, out result) == 0)
return 0;
else
return -1;
}
int func1_5(int a, out int result)
{
if (func1_4(a, out result) == 0)
return 0;
else
return -1;
}
int func2_1(int a)
{
if (a > 1000000)
throw new Exception("qq");
return a + 1;
}
int func2_2(int a)
{
return func2_1(a);
}
int func2_3(int a)
{
return func2_2(a);
}
int func2_4(int a)
{
return func2_3(a);
}
int func2_5(int a)
{
return func2_4(a);
}
private string Run(string title, int max)
{
var start1 = DateTime.Now;
int res1 = 0;
for (int i = 0; i < max; i++)
{
int val;
func1_5(i, out val);
res1 += val;
}
var end1 = DateTime.Now;
var start2 = DateTime.Now;
int res2 = 0;
try
{
for (int i = 0; i < max; i++)
{
res2 += func2_5(i);
}
}
catch
{
// do nothing
}
var end2 = DateTime.Now;
return String.Format("{0}: {1} ({2}), {3} ({4})", title, end1 - start1, res1, end2 - start2, res2);
}
private void button1_Click(object sender, EventArgs e)
{
Run("test run", 1000010);
var line1 = Run("Single exception", 1000002);
var line2 = Run("Two exceptions", 1000003);
System.IO.File.WriteAllLines("D:\\temp.txt", new[] { line1, line2 });
}
Результат (Debug):
Single exception: 00:00:00.0890089 (1786293667), 00:00:00.1050105 (1785293665)
Two exceptions: 00:00:00.0950095 (1787293670), 00:00:00.1080108 (1785293665)
Результат (Release):
Single exception: 00:00:00.0430043 (1786293667), 00:00:00.0680068 (1785293665)
Two exceptions: 00:00:00.0320032 (1787293670), 00:00:00.0420042 (1785293665)
Здравствуйте, AlexRK, Вы писали:
ARK>Да? То есть проверка результата malloc — это не код возврата?
Случаев, когда можно совместить результат и код возврата, ничтожно мало.
ARK>Когда снаружи — для одного исключения ничего не меняется, я проверял.
Ты сначала тест исправь.
А потом прогони два варианта.
С ошибкой и без.
Вариант без ошибки будет в подавляющем большинстве случаев.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, WolfHound, Вы писали:
WH>Ты сначала тест исправь. WH>А потом прогони два варианта.
Исправил, прогнал — см. мой исправленный пост.
Ничего не изменилось, исключения медленнее.
WH>С ошибкой и без. WH>Вариант без ошибки будет в подавляющем большинстве случаев.
Совсем без ошибки — да, медленнее. Но в реальном приложении на это рассчитывать не стоит, ИМХО.
Здравствуйте, AlexRK, Вы писали:
ARK>Исправил, прогнал — см. мой исправленный пост. ARK>Ничего не изменилось, исключения медленнее.
Что смотреть?
ARK>Совсем без ошибки — да, медленнее. Но в реальном приложении на это рассчитывать не стоит, ИМХО.
Это 99.999% случаев.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
ARK>>Совсем без ошибки — да, медленнее. Но в реальном приложении на это рассчитывать не стоит, ИМХО. WH>Это 99.999% случаев.
Во-первых, реальное приложение выполняет реальную работу, и проверки на ее фоне будут в микроскоп не видны.
Во-вторых, какой тезис вы хотите доказать? Что exception-based code быстрее, чем retval-based code? Это надо обосновать, тестами, примерами или еще чем-то, а не просто голословными заявлениями про 146%.
Мой тест показывает, что одно исключение уже медленнее, чем миллион проверок.
Там как не было кодов возврата, так и нет.
ARK>Мой тест показывает, что одно исключение уже медленнее, чем миллион проверок.
Так ты даже корректный тест написать не можешь...
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Внесу свою лепту
Процессор прогнозирует ветвления-переходы, поэтому лишняя ветка на производительность практически не сказывается. Кроме того, существует такая процессорная реализация, как параллельное исполнение кода по двум веткам сразу. В случае исключения это невозможно в принципе.