понедельник, 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
    >
  >;
};

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

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

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