Я программировал на C и C++ долгое время и до сих пор никогда не использовал исключения и try / catch. Каковы преимущества использования этого вместо того, чтобы функции возвращали коды ошибок?





Обработка исключений полезна, поскольку позволяет легко отделить код обработки ошибок от кода, написанного для обработки функции программы. Это упрощает чтение и написание кода.
в первом случае вызывающая функция должна проверить код ошибки на предмет ожидаемого сбоя; в последнем случае исключение может быть обработано любым вызывающим абонентом в стеке (или обработчиком по умолчанию) в зависимости от ситуации.
Преимущество состоит в том, что вам не нужно проверять код ошибки после каждого потенциально неудачного вызова. Однако для того, чтобы это работало, вам необходимо объединить его с классами RAII, чтобы все автоматически очищалось по мере раскручивания стека.
С сообщениями об ошибках:
int DoSomeThings()
{
int error = 0;
HandleA hA;
error = CreateAObject(&ha);
if (error)
goto cleanUpFailedA;
HandleB hB;
error = CreateBObjectWithA(hA, &hB);
if (error)
goto cleanUpFailedB;
HandleC hC;
error = CreateCObjectWithA(hB, &hC);
if (error)
goto cleanUpFailedC;
...
cleanUpFailedC:
DeleteCObject(hC);
cleanUpFailedB:
DeleteBObject(hB);
cleanUpFailedA:
DeleteAObject(hA);
return error;
}
С исключениями и RAII
void DoSomeThings()
{
RAIIHandleA hA = CreateAObject();
RAIIHandleB hB = CreateBObjectWithA(hA);
RAIIHandleC hC = CreateCObjectWithB(hB);
...
}
struct RAIIHandleA
{
HandleA Handle;
RAIIHandleA(HandleA handle) : Handle(handle) {}
~RAIIHandleA() { DeleteAObject(Handle); }
}
...
На первый взгляд версия RAII / Exceptions кажется длиннее, пока вы не поймете, что код очистки нужно писать только один раз (и есть способы упростить это). Но вторая версия DoSomeThings намного понятнее и удобнее.
НЕ пытайтесь использовать исключения в C++ без идиомы RAII, поскольку это приведет к утечке ресурсов и памяти. Вся ваша очистка должна выполняться в деструкторах объектов, размещенных в стеке.
Я понимаю, что есть и другие способы обработки кода ошибки, но все они выглядят примерно одинаково. Если вы отбрасываете gotos, вы в конечном итоге повторяете код очистки.
Один момент для кодов ошибок заключается в том, что они показывают, где что-то может выйти из строя и как оно может выйти из строя. В приведенном выше коде вы пишете его с предположением, что что-то не произойдет (но если это произойдет, вы будете защищены оболочками RAII). Но в конечном итоге вы меньше обращаете внимание на то, где что-то может пойти не так.
Вы также можете комбинировать RAII и обычные коды ошибок. Оператор return в некоторой степени эквивалентен throw в том смысле, что оба будут вызывать dtors объектов стека, которые находятся в области видимости. Так что никаких goto не требуется.
Хорошая точка зрения. В моей голове коды ошибок всегда для C, поэтому мой мозг не совершил скачок.
Вот - хорошее объяснение EAFP («Проще просить прощения, чем разрешения»), которое, я думаю, применимо здесь, даже если это страница Python в Википедии. Использование исключений приводит к более естественному стилю кодирования, IMO - и, по мнению многих других, тоже.
Возможно, это очевидный момент - разработчик может игнорировать (или не знать) ваш статус возврата и продолжать блаженно не осознавать, что что-то не удалось.
Исключение необходимо каким-то образом признать - его нельзя игнорировать, не приняв для этого никаких мер.
У вас есть один из двух пунктов: вы можете легко переместить код для обработки ошибок в нелокальную точку (выше изменения вызова), которая знает, как справиться с проблемой в более обширном контексте.
Помимо прочего, вы не можете вернуть код ошибки из конструктора. Деструкторов тоже, но вам также следует избегать создания исключения из деструктора.
Как отметил @Martin, выброс исключений заставляет программиста обрабатывать ошибку. Например, непроверка кодов возврата - один из самых больших источников дыр в безопасности в программах на языке C. Исключения гарантируют, что вы обработаете ошибку (надеюсь) и предоставите какой-то путь восстановления для вашей программы. И если вы решите игнорировать исключение, а не создавать брешь в безопасности, ваша программа выйдет из строя.
Преимущество исключений двоякое:
Их нельзя игнорировать. Вы должны разобраться с ними на каком-то уровне, иначе они завершат вашу программу. С кодом ошибки вы должны явно проверить их, иначе они потеряны.
Их можно игнорировать. Если ошибка не может быть устранена на одном уровне, она автоматически переходит на следующий уровень, где может быть. Коды ошибок должны передаваться явным образом до тех пор, пока они не достигнут уровня, на котором с этим можно будет иметь дело.
это должен быть лучший ответ ИМО!
не могли бы вы уточнить второй пункт (возможно, с образцом кода), я не очень хорошо его понимаю - Спасибо.
@Xsmael Предположим, что FuncA () вызывает FuncB (), которая вызывает FuncC (), которая вызывает FuncD (). FuncD () может генерировать исключение, и если в FuncB или FuncC нет перехватов, его можно перехватить с помощью FuncA (). Используя код ошибки, FuncC должен принять код от FuncD и, в частности, вернуть его в FuncB.
о, теперь посмотрим, поэтому из FuncA () я могу поймать любое исключение, которое было выброшено в любой из вложенных функций B, C, D .... независимо от того, насколько глубоко оно заходит?
Иногда вам действительно нужно использовать исключение, чтобы отметить исключительный случай. Например, если что-то пойдет не так в конструкторе, и вы обнаружите, что имеет смысл уведомить об этом вызывающего абонента, тогда у вас нет другого выбора, кроме как выбросить исключение.
Другой пример: иногда нет значения, которое ваша функция может вернуть для обозначения ошибки; любое значение, которое может вернуть функция, означает успех.
int divide(int a, int b)
{
if ( b == 0 )
// then what? no integer can be used for an error flag!
else
return a / b;
}
Хороший пример. Другой - это функции обратного вызова, которые вы передаете сторонним библиотекам, над которыми у вас нет контроля. Тогда обработка исключений также может быть единственным способом избавиться от ошибки.
Когда я преподавал C++, наше стандартное объяснение заключалось в том, что они позволяют избежать путаницы в сценариях солнечного и дождливого дня. Другими словами, вы можете написать функцию, как если бы все работало нормально, и в конце перехватить исключение.
Без исключений вам нужно будет получать возвращаемое значение от каждого вызова и гарантировать, что оно по-прежнему является законным.
Связанное с этим преимущество, конечно же, заключается в том, что вы не «тратите» свое возвращаемое значение на исключения (и, таким образом, позволяете методам, которые должны быть недействительными, быть недействительными), а также можете возвращать ошибки из конструкторов и деструкторов.
Выбрасывание исключений из деструкторов считается плохой идеей.
Я написал об этом запись в блоге (Исключения делают Elegant Code), которая впоследствии была опубликована в Перегрузка. На самом деле я написал это в ответ на то, что Джоэл сказал в подкасте StackOverflow!
В любом случае, я твердо верю, что исключения предпочтительнее кодов ошибок в большинстве случаев. Мне очень больно использовать функции, возвращающие коды ошибок: вы должны проверять код ошибки после каждого вызова, что может нарушить поток вызывающего кода. Это также означает, что вы не можете использовать перегруженные операторы, поскольку нет способа сообщить об ошибке.
Боль от проверки кодов ошибок означает, что люди часто пренебрегают этим, что делает их совершенно бессмысленными: по крайней мере, вы должны явно игнорировать исключения с помощью оператора catch.
Использование деструкторов в C++ и средств удаления в .NET для обеспечения правильного освобождения ресурсов при наличии исключений также может значительно упростить код. Чтобы получить такой же уровень защиты с кодами ошибок, вам потребуется либо множество операторов if, либо много дублированного кода очистки, либо вызовы goto к общему блоку очистки в конце функции. Ни один из этих вариантов не нравится.
Тот факт, что вы должны подтверждать исключения, является правильным, но это также можно реализовать с помощью структур ошибок. Вы можете создать базовый класс ошибок, который проверяет в своем dtor, был ли вызван определенный метод (например, IsOk). Если нет, вы можете зарегистрировать что-то, а затем выйти, или выбросить исключение, или поднять утверждение и т. д.
Тогда простой вызов IsOk для объекта ошибки без реакции на него будет эквивалентен записи catch (...) {} Оба утверждения демонстрируют одно и то же отсутствие у программиста доброй воли.
Перенос кода ошибки до правильного уровня вызывает большую озабоченность. По сути, вам придется заставить почти все методы возвращать код ошибки с единственной целью распространения. Но опять же, функция или метод всегда должны быть аннотированы исключениями, которые они могут генерировать. Таким образом, в основном у вас есть такая же проблема, без интерфейса для ее поддержки.
Руководство по стилю C++ от Google содержит подробный анализ плюсов и минусов использования исключений в коде C++. Он также указывает на некоторые из более важных вопросов, которые вам следует задать; т.е. собираюсь ли я распространять свой код среди других (у кого могут возникнуть трудности с интеграцией с базой кода с включенными исключениями)?
Это отличная ссылка. Интересно отметить, что Google не использует исключения (из-за существующего кода), но что «все могло бы быть иначе, если бы нам пришлось делать это снова и снова с нуля».
См. Ответ Джеймса Каррана по основным причинам > 2 <.