Здравствуйте, ononim, Вы писали:
O>Активное использование исключений в целом налагает определенные требование на код
Хорошая практика программирования предполагает "активное" использование исключений только в том смысле, что они часто объявляются, но редко (в исключительных случаях) выбрасываются. Бросать исключения в ожидаемых ситуациях — плохая практика.
O>имхо делить ситуацию на "ужас-ужас-ужас" "ужас" и "норм" и использовать разные механизмы для разных уровней ужаса — это лишь повышает сложность, а профит не очень заметен. Разве что, если надо чтоб "ужас" быстро отрабатывал.
"Быстро" — это насколько? Любое исключение C++ обрабатывается через исключение ОС (по крайней мере, под виндой). Если каждая функция, которая может отработать с различными особенностями, для информирования о них будет бросать исключения — накладные расходы возрастут весьма заметно. Конечно, если таких функций немного, то на общем фоне они могут и потеряться, но сам подход к такому использованию исключений ущербен, поскольку к нему легко привыкнуть, а когда вдруг окажется, что расходы стали чрезмерными, затраты на переделку, скорее всего, окажутся неприемлемыми. И начнется традиционное "купите процессор побыстрее".
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Типичная ситуация: функция верхнего уровня вызывает функцию более низкого уровня, та — следующую и т.п., и где-то на N-м уровне очередной вызов возвращает ошибку. Если это что-то уникальное, то можно вернуть наверх (хоть через обычные return, хоть через исключения) уникальный код и/или описание. Но если ошибка общего характера (нехватка ресурсов, отказ в доступе, разрыв соединения и т.п.), то неплохо бы вернуть наверх более подробную информацию, которую можно показать пользователю, чтобы он имел более-менее адекватное представление о проблеме.
Надо понимать, что есть три класса ошибок:
1. Ошибки которые может исправить только пользователь, типа неправильны путь к файлу, неправильное значение параметра и тп
2. Ожидаемые кодом ошибки которые могут корректно обрабатываться самим кодом, полное разнообразие
3. Приложение умерло, типа нет памяти
ЕМ>Вообще, какие способы возврата уточненных результатов из многократно вложенных вызовов ныне считаются кошерными?
Думаю это вопрос из серии "священные войны"
Я пришел к некоторому объекту: CError который используется для ошибок из пунктов №1 и №2. Это класс враппер над референсным контейнером для ошибки. Если ошибки нет, то передается просто nullptr, иначе указатель на контейнер с даннмыи об ошибке. Ошибки имеют уникальный идентификатор — для обработки внутри кода, и есть опциональное текстовое поле описывающее ошибку для пользователя. Также ошибка может содержать вложенные ошибки и доп данные (пара ключ-данные).
Функция которая создает ошибку решает присвоить только номер ошибки:
err.Set( ErrorBufferNotEnough )
или сформировать читабельное сообщение об ошибке:
err.Set( FileNotFound, "File '%s' not found", file_name )
На верхнем уровне функция может просто пределать ошибку выше:
err = f();
if( err.IsError() ) {
return err;
}
или сформировать новую:
err = f();
if( err.IsError() ) {
return CError( ErrorFatal );
}
или сформировать стек ошибок:
err = f();
if( err.IsError() ) {
return CError( ErrorFatal, err );
}
или передавать ошибки со сформированнымим сообщениями, а другие хендлить другим образом:
err = f();
if( err.HasMsg() ) {
return err;
} else if( err.IsError() ) {
return CError( ErrorFatal, err );
}
Всегда можно получить ошибку err.GetMessage( opt = 0 ), по умолчанию получаешь сообщени об ошибке самого верхнего уровня, но в зависимости от опций можно получить весь стек ошибок. Если было сформировано сообщение, то получим это сообщение, если нет то стандартное сообщение ассоциированное с номером ошибки.
Так же можно аттачить доп данные (ключ-данные) к ошибке:
CError result;
if( check username field ) {
CError err( ErrorInvalidConfig, "Invalid value '%s'", val );
err[ "field" ] = "username";
result += err;
}
if( check first_name field ) {
CError err( ErrorInvalidConfig, "Invalid value '%s'", val );
err[ "field" ] = "first_name";
result += err;
}
return result;
GetMessage() вернет:
username: Invalid value 'zzzz'
first_name: Invalid value 'ddddd'
Сами ошибки можно передавать разными способами:
возвращаемое значение:
CError = f1();
как output значение:
CError err;
auto r = f1( err );
через TLS:
auto r = f1();
if( r == InvalidValue ) {
CError err = CError::GetLastError();
}
Пользователю обычно выдается сообщение только верхнего уровня, а в логи пишется полная ошибка.
Для ошибок из третьего пункта бросаю исключение. в 99% случаев это бэд аллок.
В общем как-то так.
Здравствуйте, so5team, Вы писали:
S>Шаблон tagged_scalar там сделан для того, чтобы при вызове конструктора limiter нельзя было перепутать аргументы, которые имеют один и тот же тип. Такой вот незамысловатый трюк для того, чтобы уменьшить количество ошибок в коде (ну нельзя же все на откуп assert-ам отдавать, ей Богу).
попутно замечу, что подобный tagged_scalar для типов unsigned int легче можно реализовать с помощью enum. Не надо дополнительно реализовывать операции сравнения, вычисление хэша и т. п. std::byte реализован именно так. Так что можно сказать, канонический подход.
https://godbolt.org/z/vbx1qK