четверг, 24 марта 2016 г.

Сериализация в C++ 3. Шаг в сторону

Если приглядеться внимательно к тому, что я писал в предыдущих двух статьях про сериализацию (1, 2), то несложно заметить, что можно через такой же механизм привязать к методу или полю класса информацию любого типа. Например, можно привязать к метду класса максимальное допустимое время работы этого самого метода. Представьте себе threadpool у которого есть отдельная специализация метода run со следующей сигнатурой:

template<typename C, typename R, typename... A>
future<R> run(C&& c, member_ptr<C, R(A...)> method, A&&... a);
и строгой гарантией, что если к переданному методу прикреплена описанная выше метаинформация, то по истечению таймаута future будет содержать исключение какого-нибудь типа timeout_error если метод всё ещё не завершился.

Для этого заведём класс аналогичный member_info из первой статьи, но параметризованный дополнительным типом, который мы назовём MetaTag. Задача этого типа определять семантику прикреплённой метаинфорамции. Ведь просто время может значить всё что угодно, а вот тэг timeout очень чётко говорит, что это за время мы прицепили к методу. Единственное требование которое мы собираемся предъявлять тэгам это наличие в нём типа type, которое задаёт тип присоеденяемой метаинформации.

template<typename MetaTag, typename TM, Member<TM> M>
struct member_metainf {
  static const typename MetaTag::type value;
  static constexpr Member<TM> member = M;
};

Где Member<T> это эмуляция концепта посредством ранее описанного механизма, требующего в нужных нам местах указатель на поле кокого-нибудь класса или структуры. А вот назначение поля member станет ясно чуть ниже.

Теперь, наверно, стоит задуматься, как бы эту метаинформацию запрашивать. И это уже не совсем так тривиально как может сходу показаться. Ведь поле, информацию о котором мы хотим получать, почти во всех реальных сценариях использования будет браться из аргумента функции, который не может быть константным выражением и его нельзя просто запользовать в качестве шаблонного параметра. Несколько итераций с экcпериментами привели меня к реализации, которая мне самому не очень нравится, но которая максимально близка к идее static reflection. Мы заставляем компилятор генерить рутинный код за нас, не унося на этап исполнения ничего лишнего.

Итак, идея вносится в студию. То, что мы на самом деле хотим, это обойти список разных типов и для каждого из них проверить, является ли он требуемой метоинформацией для аргумента нашей функции и, если является, вернуть найденное значение. Оператор for, к несчастью, нам тут не поможет, ведь мы хотим итерироваться не то чтобы по значениям разных типов, мы хотим итерироваться просто по разным типам, чего C++ в отличии от D не умеет. Всё что у нас есть для решения данной задачи это рекурсивные шаблоны и молитвы всевышнему о том чтобы компилятор понял, что тут простой обход и заинлайнил всё как положено. Итак, смотрим на код (из которого становится ясно назначение дополнительного поля member в классе member_metainf):

namespace detail {
// recursion termination
template<
  typename MetaTag, typename M,
  typename MembersTuple, size_t Idx
>
typename std::enable_if<
  Idx >= std::tuple_size<MembersTuple>::value,
  const typename MetaTag::type*
>::type get_metainf(Member<M> m) {
  return nullptr;
}

template<
  typename MetaTag, typename M,
  typename MembersTuple, size_t Id
x>
typename std::enable_if<
  Idx < std::tuple_size<MembersTuple>::value,
  const typename MetaTag::type*
>::type get_metainf(Member<M> m) {
  if (m == std::tuple_element<Idx, MembersTuple>::type::member)
    return &std::tuple_element<Idx, MembersTuple>::type::value;
  return get_metainf<MetaTag, M, MembersTuple, Idx + 1>(m);
}
} // namespace detail

template<typename MetaTag, typename M>
const typename MetaTag::type* get_metainf(M m) {
  static_assert(
    is_member<M>::value,
    "M must be a pointer to member of some class");
  using class_type = typename member_trait<M>::class_type;
  using members_tuple = MembersMetainf<MetaTag, class_type>
  return detail::get_metainf<MetaTag, M, members_tuple, 0>(m);
}

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

Причиной, которая на самом деле побудила меня сделать этот шаг в сторону и написать код добавления произвольных метаданных к полю класса или структуры, является задача, с которой я однажды столкнулся. Мне нужно было распарсить XML выхлоп Redmine REST API и разложить информацию об отдельных задачах по полям структур. Решая её очень хотелось проаннотировать каждое поле моей структуры XPath выражением указыввающим на элемент документа откуда надо брать значение данного поля, а потом написать парсер который бы использовал их. Описанный в начале данной стати подход (сама структура member_metainf) позволяет реализовать эту идею, но для примера я взял таймауты, так как хотелось использовать метаинформацию с типом отличным от строк.

Другой важный момент, который можно найти в этой статье это код по поиску в списке полей класса, когда оный список является не списком значений, а списком типов. Данная задача возникает при написании функции десериализации с использованием информации о пропертях и полях из предыдущих двух статей. Чуть позже я хочу вернуться к этой проблеме и решить её через type-erasure, унеся принятие части решений на этап исполнения, но получив более качественный код. Если дойдут руки, то я попрофилирую оба решения и посмотрю стоит ли игра с уродливыми рекурсивными шаблонами свеч.

Продолжение следует...

понедельник, 14 марта 2016 г.

Сериализация в C++ 2: ООП головного мозга.

Продолжу начатую не так давно тему сериализации в C++ и опишу как работать с тем, что принято называть свойствами объектов (ptoperties). Ведь вам могли достаться типы из сторонней библиотеки, которая хочет сохранять стабильность своего API даже при изменении внутреннего представления данных. Но скорей всего вам достались типы от соседа у которого тяжёлая болезнь: "ООП головного мозга", и он пишет тонны "гетеров" и "сетеров" просто от того, что священная мудрость древних гласит, что так делать правильно, а вопрос почему и когда именно это правильно откидывается как ересь достойная сожжения на костре. Либо же вы сами косячите, пытаясь сохранять состояние объекта тип которого не является чистым значением. Так или иначе, периодически, работать со свойствами нам приходится и никуда от этого не деться. В конце концов, есть ситуации когда такое сокрытие прямого доступа к полю действительно оправдано и необходимо.

Фундаментально разницы по сравнению с тем, что я писал в первой статье нет. Единственное отличие в том, что информация об отдельно взятом поле принимала один указатель на член класса, а в случае свойств нам потребуется отдавать два указателя на функции члены (на getter и setter). Топорное описание шаблона аналогичного типу member_info из первой статьи натыкается на проблемы. Функции доступа могут иметь разные сигнатуры. Getter может отдавать результат

  • по значению
  • по константной ссылке
в то время как setter может принимать аргумент
  • по значению
  • по константной ссылке
  • по r-value ссылке
  • не принимать аргументов и возвращать неконстантную ссылку (далее я не буду рассматривать этот вариант, так как это требует дополнительного кода, который читатель, при необходимости, легко напишет и сам)
Это приводит нас к нелёгкому выбору: либо написать по частичной специализации шаблона property_info для каждой комбинации вариантов описанных выше, либо запользовать идею концептов, а точнее их иммитации описанной в предыдущих двух постах. Немного подумав, выкидываем вариант написания "многа повтоторяющегося кода" и идём дальше.

Начнём с getter'ов. Нам потребуется вспомогательный шаблон getter_trait, чтобы узнавать какого типа свойство и в каком оно классе.

template<T>
struct getter_trait;

template<typename C, typename T>
struct getter_trait<member_ptr<C, T() const>> {
    using class_type = C;
    using property_type = T;
};

template<typename C, typename T>
struct getter_trait<member_ptr<C, const T&() const>> {
    using class_type = C;
    using property_type = T;
};

После чего оный шаблон мы используем чтобы написать концепт Getter.

template<typename T, typename = require<>>
struct is_getter: public std::false_type {};

template<typename T>
struct is_getter<T, require<
    typename getter_trait<T>::class_type,
    typename getter_trait<T>::property_type
>>: public std::true_type {};

template<typename T>
using Getter = typename std::enable_if<is_getter<T>::value, T>::type;

Один в один такой же концепт и trait пишется для setter'а, после чего можно приступить к написанию вожделенного property_info:

template<
  typename TG, Getter<TG> Getter,
  typename TS, Setter<TS> Setter
>
struct property_info {
  static_assert(
    std::is_same<
      typename getter_trait<TG>::class_type,
      typename setter_trait<TS>::class_type
    >::value,
    "getter and setter must be members of the same class"
  );
  static_assert(
    std::is_same<
      typename getter_trait<TG>::property_type,
      typename setter_trait<TS>::property_type
    >::value,
    "getter and setter must get/set values of the same type"
  );

  static const char* const name;
  static auto get(const typename getter_trait<TG>::class_type& c)
    -> decltype((c.*Getter)()) {
    return (c.*Getter)();
  }
  template<typename U>
  static void set(typename setter_trait<TS>::class_type& c, U&& val) {
    (c.*Setter)(std::forward<U>(val));
  }
};

Тут с помощью концептов мы наложили условия на шаблонные параметры и это в значительной степени облегчает жизнь, делая код легче читаемым и ломая компиляцию именно в месте реальной ошибки. В остальном же данный тип полностью аналогичен типу member_info из первой статьи про сериализацию и может быть использован точно так же:

class Point {
public:
  Point(int x = 0, int y = 0): mX(x), mY(y) {}

  int getX() const {return mX;}
  void setX(int x) {mX = x;}
  int getY() const {return mY;}
  void setY(int y) {mY = y;}
private:
  int mX;
  int mY;
};
template<>
property_info<
  decltype(&Point::getX), &Point::getX,
  decltype(&Point::setX), &Point::setX
>::name = "x";
template<>
property_info<
  decltype(&Point::getY), &Point::getY,
  decltype(&Point::setY), &Point::setY
>::name = "y";
template<>
struct type_info<Point> {
  using members = std::tuple<
    property_info<
      decltype(&Point::getX), &Point::getX,
      decltype(&Point::setX), &Point::setX
    >,
    property_info<
      decltype(&Point::getY), &Point::getY,
      decltype(&Point::setY), &Point::setY
    >
  >;
};

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

Продолжение следует...

воскресенье, 13 марта 2016 г.

Концепты для бедных 2: require

В предыдущей статье я описал как удобным образом описывать перегрузки функций по концептам, но не рассказал как удобно описывать концепты. Сейчас я хочу заполнить сей пробел.

В Concepts Lite TS для описания требований, налагаемых концептом, зарезервированно ключевое слово requires. За неимением оного в современных компиляторах, мы попробуем добиться как можно более похожего поведения с помощью шаблонного типа который назовём require (отличаемся в одну букву, чтобы не ломать компиляцию после появления концептов в C++). А реализацию этого шаблона можно безбожно стырить, опять же, из семнадцатых плюсов, где в стандартной библиотеке появится тип void_t. Прочитав его описание несложно понять как описать концепт Wriable требующий наличия перегрузки оператора вставки в поток:

template<typename T, typename = require<>>
struct is_writable: public std::false_type {};

template<typename T>
struct is_writable<T, require<
  decltype(std::declval<std::ostream&>() << std::declval<T>())
>>: public std::true_type {};

template<typename T>
using Writable = std::enable_if<is_writable<T>::value, T>::type

Где require это:

template<typename... T>
struct make_void {typedef void type;};

template<typename... T>
using require = typename make_void<T...>::type;

среда, 9 марта 2016 г.

Концепты для бедных

Когда смотришь на то, что предполагается в Concepts Light в C++, то аппетитно облизываешься на возможность перегрузки шаблонных функций по именам концептов как по типам. Например, функция записи в данных в поток могла бы иметь следующие перегрузки:

void write(std::ostream& out, const DirectWritable& val);
void write(std::ostream& out, const Reflectable& val);

для типов которые можно записать используюя operator<< (DirectWritable) и типов которые разметили используюя технику описанную в предыдущем моём посте (Reflectable). Можно ли получить такой же функционал без компилятора который бы поддерживал Concepts Light (на данный момент оный TS поддержан только в ветке по разработке gcc 6 релиз из которой ожидается вроде этой весной)?

Ответ, да можно! Но только отчасти. Сегодня для таких перегрузок используется SFINAE и выглядит это страшно:

template<typename T>
void write(
  std::ostream& out,
  const typename std::enable_if<
    is_direct_writable<T>::value,
  T>::type& val
);

Но если приглядеться внимательно, то можно заметить, что второй параметр очень похож на то, что хочется получить. А начит можно записать его вот так:

template<typename T>
using DirectWritable = typename std::enable_if<
  is_direct_writable<T>::value,
  T
>::type;

template<typename T>
void write(std::ostream& out, const DirectWritable<T>& val);

Правда тут вылазит то самое слово "отчасти", что я употребил чуть выше. Дело в том, что такой подход убивает самую важную фичу шаблонных функций: вывод параметров шаблона из типов аргументов. И, несомненно, упущенным оказывается важный момент идеи концептов: красивые и понятные сообщения об ошибках. Чтобы устранить эти недостатки, надо выплонить "закат солнца вручную", а имнно унести такие перегрузки в условно приватное пространство имён detail (такое имя используется в реализации стандартной библиотеки поставляемой с gcc для сокрытия деталей реализации) и завести шаблонную функу, которая будет выводить параметры и генерировать красивые сообщения об ошибках:

template<typename T>
void write(std::ostream& out, const T& val) {
  static_assert(
    is_direct_writable<T>::value || is_reflectable<T>::value,
    "val argument must satisfy one of the following concepts: DirectWritable, Reflectable"
  );
  detail::write<T>(out, val);
}

На самом деле преимуществ от такого подхода к сокрытию SFINAE достаточно, чтобы не лениться и писать подобные мусорные функции-обёртки при работе с обобщённым кодом сегодя в ожидании того светлого завтра, когда у нас будут Concepts Lite.

Продолжение следует...

четверг, 3 марта 2016 г.

Удобная сериализация в C++.

Возможно ли то, что написанно в заголовке, реализовать в нашем реальном мире? Ведь все работающие с C++ знают, что программист должен страдать и вручную описывать сохранение и загрузку каждого поля структуры данных и делать эту чисто механическую работу, которую идеально мог бы сделать тупой автомат, дважды. Но нет, всё же есть способы, позволяющие сильно упростить сию рутину. И я говорю не о системах вида protobuf или apache thrift, которые, несомненно, хороши, а о чистом C++.

Давайте определимся, чего же хочется?! А хочется для каких-то типов писать код сериализации руками, учитывая нюансы хранимых в них данных. Например, сохраняя std::tm в JSON вы можете хотеть сохранить её в виде какого-то конкретного строкового представления даты, и это будет определяться конкретикой задачи, а не внутренним представлением этой самой весьма развесистой структуры. Но в большинстве случаев всё же хочется сказать компилятору: "От каждого поля структуры возьми его имя и запользуй оное в качестве JSON ключа, а значение читай и пиши в соответствии с тем, какого типа это поле".

К несчастью, SG7 весь выданный им бамбук выкурило, а ничего кроме std::experimental::source_location не придумало. Поэтому static reflection мы получим в лучшем варианте в виде TS в середине жизненного цикла 17 плюсов, если не позже. Придётся, как обычно, изворачиваться.

Для начала вспомним, что параметрами шаблона могут быть не только типы, но и значения, если последние вычислимы на этапи компиляции и различимы компилятором. И соотнесём эту информацию с тем, что указатель на поле класса являеся таким значением. Иными словами, мы можем написать такой код:

template<typename C, typename T>
using member_ptr = T C::*;

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

template<typename C>
struct type_info;
template<typename C>
using members = type_info<C>::members;

struct Point {
  int x;
  int y;
};
template<>
member_info<Point, int, &Point::x>::name = "x";
template<>
member_info<Point, int, &Point::y>::name = "y";
template<>
struct type_info<Point> {
  using members = std::tuple<
    member_info<Point, int, &Point::x>,
    member_info<Point, int, &Point::y>
  >;
};

Теперь на этапе компиляции у нас есть тип, в котором хранятся как имена полей, так и функции доступа к этим полям. Обращаю внимание: до этапа исполнения доживают только строковые константы с именами полей (которые при любых раскладах до него доживут). Информация о структуре полностью внутри компилятора в виде типа, экземпляры которого мы вряд ли когда-нибудь захотим создавать. Таким образом, мы очень сильно приблизились к идее static reflection. Можно пометить member_info::name как constexpr, но это увеличит размер описания метоинформации о типе (хоть оно и закроется макросами, ибо сейчас оно занимает больше места, чем сам описание типа) и убъёт поддержку MSVS2013, на которую я вынужден целиться, при этом реальных бонусов в задаче о сериализации это не даст.

Следующий важный момент: код выше строчки "struct Point {" самодостаточен и может быть использован в коде сериализации, не требуя forward-деклараций, правильной последовательности включения заголовочных файлов или какой бы то ни было другой информации о тех структурах, которые будут размеченны вышеуказанными средствами. Единственное, что осталось под ковром, это джентельменское соглашение при специализации шаблона type_info всегда заводить в нём тип с именем members, который при этом будет std::tuple.

Ещё один важный момент. Я сознательно не стал уносить метаинформацию в саму структуру, удлинив при этом портянку кода на 3 строчки, ибо часто хочется размечать сторонние типы, а потом читать и писать их так же, как и свои. В этом варианте отделённая от класса метаинформация - это единственны правильный путь.

Позже я напишу

Но а сейчас неплохо бы поспать перед нелёгким рабочим днём, а посему продолжение следует.