среда, 27 апреля 2016 г.

Растаманская обработка ошибок в C++

В C++ в качестве механизма обработки ошибок был выбран механизм исключений. У него есть масса своих достоинств, но он не всем нравится. И если ты работаешь в проекте, где исключений стараются избегать, то возникает вопрос: "А как же рапортовать об ошибках???".

Обычно каждый изворачивается как может, но если остановиться и немного оглядеться, то можно найти языки в которых тоже нет исключений, но есть обработка ошибок. Например в rust функция, не всегда способная справиться с поставленной перед ней задачей, возвращает tagged-union тип "результат либо ошибка". В списке рассылки по возможным кандидатам на добавление в C++ обсуждался аналогичный шаблонный класс под названием expected, но к счастью данное предложение было отклонено со словами: "механизм обработки ошибок в C++ уже есть и второй не нужен". В этом я полностью поддерживаю комитет. В C++ и без того есть слишком много способов сделать одну и ту же вещь совершенно несовместимыми друг с другом способами. Пусть хоть где-то не плодится ненужное разнообразие. Но...

Если всё же работаешь в проекте который предпочитает избегать исключений и любишь экспериментировать с шаблонами, а так же мечтаешь потрогать C++11 union в котором можно держать классы с нетривиальными кострукторами и дестркторами, то почему бы не написать свою реализацию подобного класса?!

В качестве отправной точки возмём POSIX овую функцию write и сделаем красивую обёртку с использованием этого самого expected так будто бы он у нас уже есть, а затем рассмотрим возникающие важные моменты, чтобы понять как правильно сей класс реализовывать.

expected<size_t, error_code> write(
  const file_descriptor& fd,
  const void* data,
  size_t len
) {
  const ssize_t res = ::write(fd.get(), data, len);
  if (res < 0)
    return unexpected<error_code>(errno, system_category());
  return static_cast<size_t>(res);
}

Я предполагаю что у нас есть RAII обёртка над файловым дескриптором по которой делается перегрузка и это совершенно не существенно, а существенно, что я хочу явно указывать в коде пути возврата ошибок, для этого будет заведён вспомагательный класс unexpected которй будет единственным способом создать объект ошибочного состояния. Ожидаемый результат будет возвращатся через неявное конструирование expected. Такой подход позволяет рабоать даже в таких экстремальных условиях, когд тип ошибки и тип ожидаемого результата это один и тот же тип. например сверхминималистичная обёртка над POSIX'овым open ожидаемо возвращала бы файловый дескриптор типа int, а по ошибкам возвращала бы значение errno, которое тоже типа int.

Двинемся дальше, и попробуем воспользоваться этой функцией красиво и правильно.

const string msg = "Hello world"s;
for (size_t sent = 0; sent < msg.size();) {
  const auto res = write(
    dest_fd,
    msg.data() + sent,
    msg.size() - sent
  );
  if (!res) {
    cerr << res.error().message() << endl;
    break;
  }
  sent += res;
}
Сразу же видно, что хочется уметь явно приводить результат к bool, чтобы быстро проверять наличие ошибочного состояния, хочется так же неявно уметь приводить тип expected к ожидаемому результату. Как-то неинтересно засорять код лишними подробностями в духе res.value() либо даже *res. Это создаёт проблемы в ситуации, если ожидаемое значение имеет тип bool, но на этот случай я бы предпочёл иметь явный метод is_valid, а для expected<bool,E> определить только неявное приведение к bool возвращающее ожидаемое значение, а не маркер отсутствия ошибки.

С учётом этих дополнительных требований и не только, можно рассмотреть два следующих примера:

const string msg = "Hello world"s;
const size_t res = write(fd, msg.data(), msg.size());
и
const string msg = "Hello world"s;
write(fd, msg.data(), msg.size());
В первом случае мы нагло затребовали значение не проверив на его наличие и тут в случае ошибки всё, что мы можем, это кинуть исключение. Во втором же случае мы игнорируем проверку на наличие ошибки. Моё личное мнение, что доигнорировавшись до ситуации когда эта ошибка произошла, мы должны получить этой самой ошибкой по лбу в виде исключения ибо правду не скроешь.

Но! Что если мы только что получили close-frame через открытый websocket и отсылаем в ответный close-frame, после чего собираемся сделать close который уже пойдёт посылать TCP'шный FIN? Кажется, что корректная обработка ошибки записи в этой ситуации на нашей стороне совершенно бессмысленны. Ошибка в данном сценарии совершенно не мешает нам корректно работать. Как бы всё же замолчать правду в данном случае? Ответ на мой взгляд: "Только честно сознавшись, что тут мы нагло замалчиваем правду".

const vector<char> rsp = calc_confirm(close_msg);
write(ws_fd, rsp.data(), rsp.size()).ignore_error();

Вот все важные требования и собраны и осталась только мелочь. А именно как правильно бросать исключения для разных типов ошибок. C++ позволяет кидать в качестве исключений любой тип и мягко предлагает использовать типы отнаследованные от std::exception. Я бы за выкидывание типов не отнаследованных от оного без реально вестких на то причин предложил бы подвешивать разработчиков за яйца вниз головой как минимум на день, чтобы не повадно было. Посему выброс исключений для произвольных типов я бы организовывал так:

  • Если тип это класс наследник std::exception, бросаем его как есть
  • Если тип это std::exception_ptr, бросаем его через std::rethrow_exception
  • Если тип это std::error_code бросаем std::system_erro с этим самым кодом
  • Если тип это строка, то бросаем std::runtime_error используя строку как сообщение для него
  • Во всех остальных случаях заводим свой шаблонный тип наследник std::exception внутрь которого перемещаем наше ошибочное значение и бросаем его (либо без шаблонов пользуем наследника от std::exception содержащего внутри boost::any с нашим значением)

А теперь пришло время реализации:

template<typename E>
class unexpected final {
public:
  template<typename... A>
  unexpected(A&&... a): error(forward<A>(a)...) {}

  unexpected(const unexpected&) = delete;
  const unexpected& operator= (const unexpected&) = delete;
  unexpected(unexpected&&) = default;
  unexpected& operator= (unexpected&&) = default;

  operator E&& () && {return move(error);}

private:
  E error;
};
template<typename T, typename E>
class expected final {
private: // types
  enum class State {
    expected,
    unchecked_error,
    checked_error
  };

  union Data {
    T val;
    E error;

    Data() {}
    ~Data() {}
  };
public: // public interface
  template<typename... A>
  expected(A&&... a):
    state(State::expected)
  {
    new (&data.val) T(forward<A>(a)...);
  }

  expected(unexpected<E>&& err):
    state(State::unchecked_error)
  {
    new (&data.error) E(move(err));
  }

  expected(expected&& rhs):
    state(rhs.state)
  {
    switch (rhs.state) {
    case State::expected:
      new (&data.val) T(move(rhs.data.val));
    break;

    case State::unchecked_error:
      rhs.state = State::checked_error;
      // [[fallthrough]]; TODO: Uncomment in C++17
    case State::checked_error:
      new (&data.error) E(move(rhs.data.error));
    break;
    }
  }

  const expected& operator= (expected&&) = delete;
  expected(const expected&) = delete;
  const expected& operator= (const expected&) = delete;

  ~expected() noexcept(false) {
    switch (state) {
    case State::expected: data.val.~T(); break;
    case State::checked_error: data.error.~E(); break;
    case State::unchecked_error:
      if (std::uncaught_exception())
        data.error.~E();
      else try {
        throw_value(move(data.error));
      } catch(...) {
        data.error.~E();
        throw;
      }
    }
  }

  operator const T& () const {
    if (state != State::expected) {
      state = State::checked_error;
      throw_value(move(data.error));
    }
    return data.val;
  }

  operator T& () {
    if (state != State::expected) {
      state = State::checked_error;
      throw_value(move(data.error));
    }
    return data.val;
  }

  explicit operator bool () const {
    if (state == State::expected)
      return true;
    state = State::checked_error;
    return false;
  }

  const E& error() const {
    assert(state != State::expected);
    if (state == State::expected)
      throw logic_error("Trying to get error from success value");
    state = State::checked_error;
    return data.error;
  }

  E& error() {
    assert(state != State::expected);
    if (state == State::expected)
      throw logic_error("Trying to get error from success value");
    state = State::checked_error;
    return data.error;
  }

  void ignore_error() const {
    if (state == State::unchecked_error)
      state = State::checked_error;
  }

private:
  mutable State state = State::empty;
  Data data;
};
Где throw_value реализует описанную выше схему выброса исключений.

среда, 6 апреля 2016 г.

Сериализация в C++ 4. Непереносимая магия.

Продолжаю тему сериализации в C++ и хочу вернуться, не надолго, к самым истокам, дабы посмотреть на member_info из первой статьи по новому.

template<typename C, typename T, member_ptr<C, T> M>
struct member_info {
  static const char* const name;
  static const T& get(const C& c) {return c.*M;}
  template<typename U>
  static void set(C& c, U&& val) {c.*M = std::forward<U>(val);}
};

Этот код полагается на то, что статическая переменная name будет явно специализированна для каждого поля каждого типа, который мы хотим пользовать для наших целей. Можно ли сделать так, чтобы руками этого делать не приходилось? Любопытство довело меня до экспериментов и привело к положительному, но непортируемому ответу.

Дело в том, что в природе существует __PRETTY_FUNCTION__ или его аналоги в не-gcc-подобных компиляторах. А содержимое оного, это полная сигнатура функции, включая шаблонные параметры, которыми функа, где эта гадость встретилась, параметризована. Дамаю идея уже ясна и дальнейшее повествование могло бы быть бессмысленным, а посему попробуем реализовать задумку в constexpr варианте, чтобы до момента исполнения кода вся эта вспомогательная мишура не доживала.

В C++14 в constexpr функциях можно сделать уже очень многое, а в экспериментальной части стандартной библиотеки это многое уже реализовано в виде класса string_view с одной маленькой оговоркой. Вычисление длинны строкового литерала и сравнение строк реализовано через std::char_traits, у которого соответствующие операции не constexpr. Этот нюанс легко обходится следующим кодом:

struct constexpr_char_traits: public std::char_traits<char> {
  static constexpr size_t length(const char* val) {
    size_t res = 0;
    for (; val[res] != '\0'; ++res)
      ;
    return res;
  }

  static constexpr int compare(
    const char* lhs, const char* rhs,
    std::size_t count
  ) {
    for (size_t pos = 0; pos < count; ++pos) {
      if (lhs[pos] == rhs[pos])
        continue;
      return lhs[pos] - rhs[pos];
    }
    return 0;
  }
};

using string_view = std::experimental::basic_string_view<
  char,
  constexpr_char_traits
>;

Теперь у нас есть string_view который абсолютно аналогичен std::string по публичному API, но работающий, в том числе, и на этапе компиляции, правда только в libc++. В ложке мёда затисалась бочка дёгтя в виде бага под номером 70483 в багтрекере gcc. Немного поплакав о несправедливости жизни, перейдём к написанию вожделенного функционала:

template<typename T, Member<T> SomeMemberPtr>
constexpr
string_view get_member_name() {
  string_view res = __PRETTY_FUNCTION__;
  constexpr string_view start_pattern = "SomeMemberPtr = &";
  res = res.substr(res.find(start_pattern) + start_pattern.size());
  res = res.substr(0, res.find_first_of(";]"));
  res = res.substr(res.rfind("::") + 2);
  return res;
}

static_assert(
  get_member_name<
    decltype(&string_view::data),
    &string_view::data
  >() == "data",
  "Unsupported __PRETTY_FUNCTION__ format."
);

Если вместо static_assert запользовать обычный assert, то код будет работать и в связке gcc либо clang с libstdc++ и в связке clang с libc++. А со static_assert код работает только в паре clang с libc++.

Написанный выше код совершенно непереносим, а посему дальше в вопросах сериализаци я не хочу к нему возвращаться, но пару важных моментов мне хотелось бы подчеркнуть. Во первых, при наличии возможность заточиться на конкретынй компилятор, есть возможность иметь имена полей структур без макросов используя только шаблоны и функции, да и в довольно компактном и понятном исполнении. Меня лично всегда отпугивает идея, которая для своей реализации требует препроцессора, подводных камней, с которыми придётся возиться, всегда больше чем хочется, а результат, очень часто, выглядит как некие вкрапления написанные на языке отличном от C++. Во вторых, в грядущем стандарте у нас появиться очень удобный класс для работы со строками на этапе компиляции. О некоторых вариантах его использования я очень хочу написать отдельный пост, прадварительно написав и отладив для него код.

А что касается сериализации, то продолжение следует...