Проблема:
В течение долгого времени я беспокоился о exceptions
механизме, потому что я чувствую, что он действительно не решает, что он должен.
ПРЕТЕНЗИЯ: Есть долгие дебаты по этой теме, и большинство из них пытаются сравнить с exceptions
возвратом кода ошибки. Это определенно не тема здесь.
Пытаясь определить ошибку, я бы согласился с CppCoreGuidelines, от Bjarne Stroustrup & Herb Sutter
Ошибка означает, что функция не может достичь своей объявленной цели
ПРЕТЕНЗИЯ: exception
Механизм является языковой семантикой для обработки ошибок.
ПРЕТЕНЗИЯ: Для меня "нет оправдания" функции для невыполнения задачи: либо мы неправильно определили предварительные / последующие условия, чтобы функция не могла обеспечить результаты, либо какой-то конкретный исключительный случай не считается достаточно важным для того, чтобы тратить время на разработку решение. Учитывая, что, IMO, разница между обычным кодом и обработкой кода ошибки (до реализации) очень субъективная линия.
ПРЕТЕНЗИЯ: Использование исключений для указания того, что условие pre или post не выполняется, является еще одной целью exception
механизма, главным образом для целей отладки. Я не нацелена на это использование exceptions
здесь.
Во многих книгах, учебных пособиях и других источниках они, как правило, показывают, что обработка ошибок является довольно объективной наукой, с которой решаются проблемы, exceptions
и вам просто необходимо catch
иметь надежное программное обеспечение, способное восстанавливаться в любой ситуации. Но мои несколько лет как разработчика заставляют меня видеть проблему с другого подхода:
- Программисты стремятся упростить свою задачу, создавая исключения, когда конкретный случай кажется слишком редким, чтобы его можно было тщательно реализовать. Типичные случаи этого: проблемы с нехваткой памяти, проблемы с заполнением диска, проблемы с поврежденными файлами и т. Д. Этого может быть достаточно, но это не всегда решается на архитектурном уровне.
- Программисты, как правило, не читают внимательно документацию об исключениях в библиотеках и обычно не знают, что и когда выдает функция. Кроме того, даже когда они знают, они на самом деле не управляют ими.
- Программисты имеют тенденцию не ловить исключения достаточно рано, и когда они это делают, это в основном, чтобы войти и бросить дальше. (обратитесь к первому пункту).
Это имеет два последствия:
- Часто возникающие ошибки выявляются на ранних этапах разработки и отлаживаются (что хорошо).
- Редкие исключения не управляются и приводят к сбою системы (с хорошим сообщением журнала) в доме пользователя. Иногда сообщается об ошибке, или даже нет.
Учитывая это, IMO основной целью механизма ошибок должно быть:
- Сделать видимым в коде, где какой-то конкретный случай не управляется.
- Сообщите время выполнения проблемы связанному коду (по крайней мере, вызывающему), когда такая ситуация произойдет.
- Обеспечивает механизмы восстановления
Основным недостатком exception
семантики как механизма обработки ошибок является IMO: легко увидеть, где throw
находится a в исходном коде, но совершенно не очевидно, может ли конкретная функция выдать, посмотрев объявление. Это приносит все проблемы, которые я представил выше.
Язык не применяет и проверяет код ошибки так строго, как это делается для других аспектов языка (например, сильные типы переменных)
Попытка решения
Чтобы улучшить это, я разработал очень простую систему обработки ошибок, которая пытается придать обработке ошибок тот же уровень важности, что и обычному коду.
Идея заключается в следующем:
- Каждая (соответствующая) функция получает ссылку на
success
очень легкий объект и может установить для нее статус ошибки в случае. Объект очень легкий, пока ошибка с текстом не будет сохранена. - Функция рекомендуется пропустить свою задачу, если предоставленный объект уже содержит ошибку.
- Ошибка никогда не должна быть отменена.
Полный дизайн, очевидно, тщательно рассматривает каждый аспект (около 10 страниц), а также то, как применить его к ООП.
Пример Success
класса:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Использование:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Я использовал это во многих моих (собственных) кодах, и это заставляет программиста (меня) думать дальше о возможных исключительных случаях и о том, как их решать (хорошо). Тем не менее, он имеет кривую обучения и плохо интегрируется с кодом, который сейчас его использует.
Вопрос
Я хотел бы лучше понять последствия использования такой парадигмы в проекте:
- Является ли предпосылка к проблеме правильной? или я пропустил что-то актуальное?
- Является ли решение хорошей архитектурной идеей? или цена слишком высока?
РЕДАКТИРОВАТЬ:
Сравнение между методами:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.
источник
Ответы:
Обработка ошибок , пожалуй, самая сложная часть программы.
В общем, понять, что есть условие ошибки, легко; однако сигнализировать об этом таким способом, который невозможно обойти, и обращаться с ним надлежащим образом (см. Уровни исключительной безопасности Абрахамса ) действительно сложно.
В C сигнализация ошибок осуществляется кодом возврата, который изоморфен вашему решению.
C ++ ввел исключения из-за недостатка такого подхода; а именно, это работает, только если вызывающие абоненты помнят, чтобы проверить, произошла ошибка или нет, и ужасно терпит неудачу в противном случае. Всякий раз, когда вы обнаруживаете, что говорите: «Все в порядке, пока каждый раз ...», у вас возникает проблема; люди не такие дотошные, даже когда им небезразлично.
Проблема, однако, в том, что исключения имеют свои проблемы. А именно невидимый / скрытый поток управления. Это было задумано: скрыть регистр ошибок, чтобы логика кода не была запутана шаблоном обработки ошибок. Это делает «счастливый путь» намного более понятным (и быстрым!) За счет того, что пути ошибок становятся почти непостижимыми.
Мне интересно посмотреть, как другие языки подходят к этой проблеме:
В C ++ раньше была какая-то форма проверяемых исключений, вы, возможно, заметили, что она устарела и
noexcept(<bool>)
вместо этого упростилась до базовой : либо объявлена функция, которая может выдавать, либо она никогда не объявляется. Проверенные исключения несколько проблематичны тем, что им не хватает расширяемости, что может привести к неуклюжему отображению / вложению. И извилистые иерархии исключений (один из основных случаев виртуального наследования - исключения ...).Go и Rust, напротив, используют подход, который:
Последнее довольно очевидно в том, что (1) они называют свои исключения паникой и (2) здесь нет иерархии типов / сложного предложения. Язык не предлагает средств для проверки содержимого «паники»: нет иерархии типов, нет пользовательского содержимого, просто «упс, все пошло не так, восстановление невозможно».
Это эффективно побуждает пользователей использовать правильную обработку ошибок, в то же время оставляя простой способ помочь в исключительных ситуациях (таких как: «подождите, я еще не реализовал это!»).
Конечно, подход Go, к сожалению, очень похож на ваш в том, что вы можете легко забыть проверить ошибку ...
... однако подход Rust в основном сосредоточен вокруг двух типов:
Option
, который похож наstd::optional
,Result
, который является вариантом двух вариантов: Ok и Err.это намного лучше, потому что нет возможности случайно использовать результат, не проверив его на успех: если вы это сделаете, программа паникует.
Языки FP формируют свою обработку ошибок в конструкциях, которые можно разбить на три уровня: - Функтор - Применимый / Альтернативный - Монады / Альтернативный
Давайте посмотрим на
Functor
класс типов Haskell :Прежде всего, классы типов несколько похожи, но не равны интерфейсам. Сигнатуры функций Haskell на первый взгляд выглядят немного страшно. Но давайте расшифруем их. Функция
fmap
принимает функцию в качестве первого параметра, который чем-то похож наstd::function<a,b>
. Следующая вещь - этоm a
. Вы можете представитьm
как нечто подобноеstd::vector
иm a
как нечто подобноеstd::vector<a>
. Но разница в том, чтоm a
это не значит, что это должно быть явноstd:vector
. Так что этоstd::option
тоже может быть . Сказав языку, что у нас есть экземпляр класса типовFunctor
для определенного типа, такого какstd::vector
илиstd::option
, мы можем использовать функциюfmap
для этого типа. То же самое должно быть сделано для классов типовApplicative
,Alternative
иMonad
который позволяет вам выполнять вычисления с возможным состоянием, возможные ошибки. КлассAlternative
типов реализует абстракции восстановления после ошибок. Таким образом, вы можете сказать что-то вродеa <|> b
значенияa
или терминb
. Если ни одно из обоих вычислений не выполнено, это все равно ошибка.Давайте посмотрим на
Maybe
тип Хаскелла .Это означает, что там , где вы ожидаете
Maybe a
, вы получаете либоNothing
илиJust a
. Если смотретьfmap
сверху, реализация может выглядеть так:case ... of
Выражение называется сопоставлением с образцом и напоминает то , что известно в мире как ООПvisitor pattern
. Представьте, что строкаcase m of
as,m.apply(...)
а точки - это экземпляр класса, реализующего функции диспетчеризации. Строки подcase ... of
выражением - это соответствующие диспетчерские функции, доставляющие поля класса непосредственно в области видимости по имени. ВNothing
ветви, которую мы создаем,Nothing
и вJust a
ветви мы называем наше единственное значениеa
и создаем другоеJust ...
сf
примененной функцией преобразованияa
. Читайте , как:new Just(f(a))
.Теперь он может обрабатывать ошибочные вычисления, абстрагируя от фактических ошибок. Существуют реализации для других интерфейсов, что делает этот вид вычислений очень мощным. На самом деле,
Maybe
это вдохновение дляOption
Rust's-Type.Я бы посоветовал вам переделать свой
Success
классResult
вместо этого. Александреску фактически предложил нечто очень близкое, названноеexpected<T>
, для чего были сделаны стандартные предложения .Я буду придерживаться именования и API Rust просто потому, что ... это задокументировано и работает. Конечно, у Rust есть отличный
?
суффиксный оператор, который сделает код намного слаще; в C ++ мы будем использоватьTRY
макрос и выражение операторов GCC для его эмуляции.Примечание: это
Result
заполнитель. Правильная реализация будет использовать инкапсуляцию иunion
. Однако этого достаточно, чтобы понять суть.Что позволяет мне написать ( увидеть это в действии ):
который я считаю действительно опрятным:
Success
класса), забыв проверить ошибки, вы получите ошибку времени выполнения 1, а не случайное поведение,concepts
в стандарте. Это сделало бы такой вид программирования гораздо более приятным, поскольку мы могли бы оставить выбор над типом ошибки. Например, с реализациейstd::vector
как результат, мы могли бы вычислить все возможные решения одновременно. Или мы могли бы улучшить обработку ошибок, как вы предложили.1 С правильно инкапсулированной
Result
реализацией;)Примечание: в отличие от исключения, этот легкий
Result
не имеет следов, что делает регистрацию менее эффективной; может оказаться полезным, по крайней мере, записать номер файла / строки, в котором генерируется сообщение об ошибке, и вообще написать расширенное сообщение об ошибке. Это может быть составлено путем захвата файла / строки каждый раз, когда используетсяTRY
макрос, по сути, создания обратной трассировки вручную или использования кода и библиотек, специфичных для платформы, таких какlibbacktrace
список символов в стеке вызовов.Однако есть одна большая оговорка: существующие библиотеки C ++ и даже
std
основаны на исключениях. Использовать этот стиль будет непросто, поскольку API любой сторонней библиотеки должен быть заключен в адаптер ...источник
({...})
это какое-то расширение gcc, но не так лиif (!result.ok) return result;
? Ваше состояние появляется в обратном направлении, и вы делаете ненужную копию ошибки.({...})
это выражение выражения gcc .std::variant
для реализации,Result
если вы используете C ++ 17. Кроме того, чтобы получить предупреждение, если вы игнорируете ошибку, используйте[[nodiscard]]
std::variant
или нет - дело вкуса, учитывая компромиссы в обработке исключений.[[nodiscard]]
это действительно чистая победа.Исключением является механизм управления потоком. Мотивация для этого механизма потока управления заключалась в том, чтобы отделить обработку ошибок от кода, не относящегося к обработке ошибок, в общем случае, когда обработка ошибок является очень повторяющейся и имеет мало отношения к основной части логики.
Подумайте: я пытаюсь создать файл. Запоминающее устройство заполнено.
Теперь это не является ошибкой в определении моих предварительных условий: вы не можете использовать «должно быть достаточно хранилища» в качестве предварительного условия в целом, потому что общее хранилище подчиняется условиям гонки, которые делают это невозможным для удовлетворения.
Итак, должна ли моя программа каким-то образом освободить место и затем продолжить успешно, иначе мне просто лень «разрабатывать решение»? Это кажется откровенно бессмысленным. «Решение» по управлению общим хранилищем выходит за рамки моей программы , и позволить моей программе изящно завершиться с ошибкой и перезапуститься, как только пользователь освободит некоторое пространство или добавит дополнительное хранилище, это нормально .
Что делает ваш класс успеха, так это чётко чередует обработку ошибок с логикой вашей программы. Перед запуском каждой функции необходимо проверить, не возникла ли какая-либо ошибка, что означает, что она не должна ничего делать. Каждая библиотечная функция должна быть обернута в другую функцию с еще одним аргументом (и, мы надеемся, совершенной пересылкой), который делает то же самое.
Также обратите внимание, что ваша
mySqrt
функция должна возвращать значение, даже если оно не удалось (или предыдущая функция потерпела неудачу). Таким образом, вы либо возвращаете магическое значение (напримерNaN
), либо внедряете неопределенное значение в вашу программу и надеетесь, что ничто не использует это без проверки состояния успеха, которое вы проделали в ходе выполнения.Для корректности - и производительности - гораздо лучше вывести контроль из области видимости, если вы не можете добиться прогресса. Исключения и явная проверка ошибок в стиле C с ранним возвратом позволяют это сделать.
Для сравнения, пример вашей идеи, которая действительно работает, - это монада Error в Haskell. Преимущество перед вашей системой заключается в том, что вы обычно пишете основную часть своей логики, а затем оборачиваете ее в монаду, которая заботится о том, чтобы остановить оценку при сбое одного шага. Таким образом, единственный код, непосредственно касающийся системы обработки ошибок, - это код, который может дать сбой (выдает ошибку), и код, который должен справиться с ошибкой (перехватить исключение).
Я не уверен, что стиль монады и ленивая оценка хорошо переводят на C ++.
источник
and allowing my program to fail gracefully, and be re-run
когда он только что потерял 2-std::exception
логические операции на более высоком уровне, говорите пользователю «X не удалось из-за ex.what ()» и предлагаете повторить всю операцию, когда и когда они будут готовы.showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try
. Это изящная обработка проблемы, которая обычно не может быть выполнена из кода, который обнаруживает, что первое хранилище заполнено.Ваш подход приносит некоторые большие проблемы в ваш исходный код:
он опирается на клиентский код, всегда помнящий проверить значение
s
. Это часто встречается при использовании кодов возврата для подхода к обработке ошибок , и одна из причин, по которой исключения были введены в язык: с исключениями, если вы терпите неудачу, вы не отказываете молча.чем больше кода вы пишете с этим подходом, тем больше шаблонного кода ошибок вам придется добавить для обработки ошибок (ваш код больше не является минималистичным), и ваши усилия по обслуживанию возрастают.
Решения этих проблем должны быть найдены на уровне технического лидера или команды:
Если вы обнаружите, что постоянно обрабатываете все типы исключений, которые могут быть выброшены, тогда дизайн не очень хорош; Какие ошибки обрабатываются, должно решаться в соответствии со спецификациями для проекта, а не в соответствии с тем, что разработчики чувствуют, как реализовать.
Решите проблему, установив автоматическое тестирование, разделив спецификацию модульных тестов и реализации (пусть это делают два разных человека).
Вы не будете решать это, написав больше кода. Я думаю, что ваша лучшая ставка - тщательно проверенные обзоры кода.
Правильная обработка ошибок трудна, но менее утомительна с исключениями, чем с возвращаемыми значениями (независимо от того, возвращаются ли они на самом деле или передаются как аргументы ввода / вывода).
Самая сложная часть обработки ошибок не в том, как вы получаете ошибку, а в том, как убедиться, что ваше приложение поддерживает согласованное состояние при наличии ошибок.
Чтобы решить эту проблему, необходимо уделять больше внимания идентификации и работе в условиях ошибки (больше тестирования, больше юнит-тестов / интеграционных тестов и т. Д.).
источник