Comments 14
или код снимает косвенность (dereferences) nullptrpointer dereference = разыменование указателя. Если бы не было слова «dereference», то не понял бы о чем речь.
В оригинале: «некий код обращается по nullptr (нулевому указателю)».
HRESULT ADsGetLastError(
_Out_ LPDWORD lpError,
_Out_ LPWSTR lpErrorBuf,
_In_ DWORD dwErrorBufLen,
_Out_ LPWSTR lpNameBuf,
_In_ DWORD dwNameBufLen
);
Всё, что вводит пользователь, должно проверяться соответствующим образом.
Такой способ мне очень по нраву.
В реальной жизни приличные компании ( гугль и везде где я работал) исключений не бросают (если это не драйвера конечно).
иначе если на серверном приложении кто-то бросит исключение, то могут не поймать и не увидеть.
Давать ссылку на питон, где обсуждается С++ по меньшей мере странно.
Увы, в С++ обработка ошибок через исключения меньше всего превращает код функции в бесконечные проверки, за которыми не видно логики. Коды возврата и коды состояния никуда не годятся, т.к. их очень легко проигнорировать. Either-style возвращаемые значения тоже не айс т.к. их во-первых нет в стандартной библиотеке, а во-вторых "раздувают" early return код — нет возможности сделать return из выражения.
Я бы предпочёл std::expected и немножко сахарку, чтобы это красиво чейнилось.
Я примерно так поступил: или передаёшь, куда сохранить код ошибки или имеешь исключение:
Немного инфраструктуры, подсмотренной, в том числе, в boost:
namespace detail { inline std::error_code * throws() { return 0; } }
...
std::error_code& throws()
{
return *detail::throws();
}
...
template<typename Category, typename Exception = av::Exception>
void throws_if(std::error_code &ec, int errcode, const Category &cat)
{
if (&ec == &throws())
throw Exception(std::error_code(errcode, cat));
else
ec = std::error_code(errcode, cat);
}
А вот так может выглядеть API:
void some_your_api_proc(..., std::error_code &ec = throws())
{
auto sts = some_ext_api();
if (sts < 0) {
throws_if(ec, sts, some_ext_api_category());
return;
}
}
Такой вызов позволит проверить код ошибки:
std::error_code ec;
some_your_api_proc(..., ec);
if (ec) {
// что-то делаем
}
А если лень, и что-то случится, то бросится исключение:
some_your_api_proc(...);
// или равнозначно
some_your_api_proc(..., throws());
Понятно, что можно сразу создавать переменную, передавать её и не проверять, но так, как минимум, писать больше, чем вообще игнорировать код возврата в классическом варианте.
Решение родилось, когда возникла проблема с забывчивостью на проверку кодов возврата.
class optional_error_code {
std::error_code * v_;
optional_error_code() : v_{nullptr} {}
public:
optional_error_code(std::error_code & ec) : v_{&ec} {}
static optional_error_code make_null() { return optional_error_code{}; }
operator bool() const { return v_ != nullptr; }
std::error_code & operator*() { return *v_; }
}
template<typename Category, typename Exception = av::Exception>
void throws_if(optional_error_code opt_ec, int errcode, const Category &cat)
{
if (opt_ec)
*opt_ec = std::error_code(errcode, cat);
else
throw Exception(std::error_code(errcode, cat));
}
void some_your_api_proc(..., optional_error_code opt_ec = optional_error_code::make_null())
{
auto sts = some_ext_api();
if (sts < 0) {
throws_if(opt_ec, sts, some_ext_api_category());
return;
}
}
UB попахивает
конкретно это подсмотрено в недрах boost, причём без всяких обёрток под различные компиляторы. Но не суть. Как мне кажется, пока я не пытаюсь работать с таким объектом, кроме как взять адрес, то ничего страшного произойти не может. Пусть знатоки меня поправят.
Но ваш вариант тоже неплох. А в моём варианте можно вместо nullptr вполне использовать реальный объект, за которым вполне будет закреплено звание "специального". С ним никто всё равно работать не будет, нужен только его адрес:
std::error_code& throws()
{
static std::error_code dummy_code;
return dummy_code;
}
всё остальное остаётся так же.
Статическая переменная внутри inline-функции в header-only библиотеках, емнип, стабильно работает пока речь идет о статической линковке всего кода в один исполняемый файл. Если же мы начинаем работать с dll/so, то в каждой из них может оказаться свой экземпляр статической переменной.
Пусть есть функция с сигнатурой
std::expected<int, SomeError> doStuff();
В С++ распаковку придётся писать так (псевдокод):
std::expected<unit, SomeError> doComplexStuff()
{
auto result = doStuff();
if(result.isError())
return result.getError();
auto intResult = result.getResult();
... // use intResult
}
но не получится написать проверку на ошибку, распаковку результата и возврат значения-ошибки в одном выражении:
std::expected<unit, SomeError> doComplexStuff()
{
auto result = (auto result = doStuff()) ? result.getResult() : return result.getError();
... // result is int here, do smth with it
}
Это Swift и Rust умеют красоту вида
let result = doFirst()?.doSecond()?.doThird()?;
Так вот, что делает клиент dbus, если не может достучаться до сервера? Выдаёт ошибку? Нет, он пытается сервер запустить! (и разумеется, не убирает запущенный инстанс сервера после своего закрытия). А если не получается запустить сервер? Он падает, не задавая никаких больше вопросов. Нормальное поведение для сетевой программы падать в отсутствии сети? А сети, между прочим, иногда намеренно не бывает, когда я рассаживаю приложения по контейнерам и запрещаю им сетевое общение. Ну и вообще кто вам обещал, что сеть всегда доступна?
Одно питон приложение я починил оригинальным способом. Я взял и удалил из питоновской директории все библиотеки dbus. Оказалось, что без них приложение работать может! Оно ловит эксепшн, ругается, но продолжает работать. А если вернуть питон файлы — то падает наглухо. Вот так вот, от грубой ошибки «нет исполнимых файлов вообще» оказывается оправиться проще чем от специально заточенной на убивание приложения.
Выбор правильной стратегии обработки ошибок (части 1 и 2)