Проблема:
В течение долгого времени я беспокоился о 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.