четверг, 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 строчки, ибо часто хочется размечать сторонние типы, а потом читать и писать их так же, как и свои. В этом варианте отделённая от класса метаинформация - это единственны правильный путь.

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

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

Комментариев нет: