Pull to refresh

Comments 14

или код снимает косвенность (dereferences) nullptr
pointer dereference = разыменование указателя. Если бы не было слова «dereference», то не понял бы о чем речь.
В оригинале: «некий код обращается по nullptr (нулевому указателю)».
Коды возврата — ужасно. Конечно в сферическом абстрактом мире они работают замечательно, но в реальной жизни рано или поздно появляются всякие out-параметры, костыли вроде GetLastError или даже такие мутанты

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());

Понятно, что можно сразу создавать переменную, передавать её и не проверять, но так, как минимум, писать больше, чем вообще игнорировать код возврата в классическом варианте.


Решение родилось, когда возникла проблема с забывчивостью на проверку кодов возврата.

Разыменовать нулевой указатель дабы получить гарантированно несуществующую ссылку, а потом взять адрес от этой ссылки и сравнить его с чем-то… UB попахивает и не удивлюсь, если в каком-то из компиляторов такое не сработает рано или поздно. Может безопаснее было сделать обертку вокруг std::error_code*?
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;
}

всё остальное остаётся так же.

На эту тему было много разговоров в Интернетах (например). Конкретно разыменование нулевого указателя для получения ссылки — это UB. Поскольку результирующая ссылка будет содержать не пойми что, то и операция взятия адреса от нее так же может давать не пойми что. Пока это все работает (т.к., по сути ссылка и указатель на низком уровне это одно и то же), но когда оптимизаторы в компиляторах станут еще умнее, то всякое может быть.

Статическая переменная внутри inline-функции в header-only библиотеках, емнип, стабильно работает пока речь идет о статической линковке всего кода в один исполняемый файл. Если же мы начинаем работать с dll/so, то в каждой из них может оказаться свой экземпляр статической переменной.

За ссылку спасибо, сходу именно по этому вопросу не смог запрос сформировать. Наверное рассмотрю вариант заменить throws() на optional_error_code или его аналог. Хабр торт ;-)

UFO just landed and posted this here

Пусть есть функция с сигнатурой


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 отдельно.

Так вот, что делает клиент dbus, если не может достучаться до сервера? Выдаёт ошибку? Нет, он пытается сервер запустить! (и разумеется, не убирает запущенный инстанс сервера после своего закрытия). А если не получается запустить сервер? Он падает, не задавая никаких больше вопросов. Нормальное поведение для сетевой программы падать в отсутствии сети? А сети, между прочим, иногда намеренно не бывает, когда я рассаживаю приложения по контейнерам и запрещаю им сетевое общение. Ну и вообще кто вам обещал, что сеть всегда доступна?

Одно питон приложение я починил оригинальным способом. Я взял и удалил из питоновской директории все библиотеки dbus. Оказалось, что без них приложение работать может! Оно ловит эксепшн, ругается, но продолжает работать. А если вернуть питон файлы — то падает наглухо. Вот так вот, от грубой ошибки «нет исполнимых файлов вообще» оказывается оправиться проще чем от специально заточенной на убивание приложения.
Эксепшены, возвращаемые значения — это всё фигня. Когда где-то в недрах сторонней библиотеки делается abort() из-за того, что ей там что-то не понравилось — вот это, я вам доложу, ад. Особенно когда срабатывает редко и только у какого-нибудь клиента, а у тебя не воспроизводится. Ни тебе дампа, ничего. Если за такое не убивать, то я вообще не знаю, за что тогда.
Sign up to leave a comment.