Введение

Предположим, что мы, как это часто бывает, имеем вектор интерфейсов к конкретным объектам:

WidgetClassA w1;
WidgetClassB w2;

std::vector<WidgetInterface*> wifv {&w1, &w2, ...};

/*
 * Прямое манипулирование конкрециями
 */
w1.setWidgetAParam("Test");
w2.setWidgetBSize( w1.getWidgetASize() );
// ...


/*
 * Манипулирование разными виджетами через абстрактный интерфейс виджетов
 */
for (WidgetInterface* wif : wifv) {
  wif->widgetDoSomething();
  size_t i = wif->getSize();
  // ...
}

Мы хотим (де)сериализовать все объекты, на которые указывают интерфейсы (чистые виртуальные классы) в векторе wifv, т.е. сохранить/загрузить все экземпляры конретных классов (WidgetClassA и т.д.) на диск или по сети не зная их типа.

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

Сначала мы определим иерархию классов, а затем сделаем классы сериализуемыми.

Интерфейс ICreature

Итак, положим мы определили интерфейс для неких созданий:

enum class Sex {
  female,
  male
};

enum class Activity {
  eat,
  think,
  sleep,
  work
};

/*
 * Creature interface
 */
class ICreature {
public:
  virtual ~ICreature() {}

  virtual Sex getSex() const = 0;
  virtual Activity getActivity() const = 0;
  virtual void setActivity(const Activity) = 0;
  virtual void printInfo() const = 0;
};

Реализации интерфейса: конкреции Human и Cat

Далее мы написали несколько реализаций ICreature: класс людей и класс кошек:

/*
 * Humans
 */
class Human : public ICreature {
private:
  Sex sex_;
  Activity activity_;
  std::string name_;

public:
  /* ctor */
  Human(Sex s, std::string n) : sex_(s), name_(n) {
    setActivity(Activity::think);
  }

  void setName(std::string n) {
    name_ = n;
  }

  // ICreature interface
public:
  Sex getSex() const override {
    return sex_;
  }
  Activity getActivity() const override {
    return activity_;
  }
  void setActivity(const Activity a) override {
    activity_ = a;
  }
  void printInfo() const override {
    printf("%s %s %ss\n", sex_ == Sex::male ? "Mr." : "Ms.", name_.c_str(),
           activityToString(activity_).c_str());
  }
};

/*
 * Cats
 */
class Cat : public ICreature {
private:
  Sex sex_;
  Activity activity_;

public:
  Cat(Sex s) : sex_(s) {setActivity(Activity::sleep); }

  void pet() const {
    std::cout << "Prr.. prrr... Meow!" << std::endl;
  }

  // ICreature interface
public:
  Sex getSex() const override {
    return sex_;
  }
  Activity getActivity() const override {
    return activity_;
  }
  void setActivity(const Activity a) override {
    if (a == Activity::work)
      throw "Cats don't work. Mmeow!!";
    else
      activity_ = a;
  }
  void printInfo() const override {
    printf("A %s cat %ss\n", sex_ == Sex::male ? "male" : "female",
           activityToString(activity_).c_str());
  }
};

Сериализация данных с cereal

Как нам сериализовать “пользовательские” классы Human и Cat, да ещё и научиться десериализовывать их через базовый класс ICreature, т.е. правильно воссоздавать конкретные объекты? Поможет cereal!

Мы добавляем несколько методов в наши классы, а затем говорим cereal, в каких родственных отношениях они состоят.

Поддержка сериализации в конкретных классах

Чтобы просто сериализовать пользовательские классы с помощью cereal, мы добавляем в Human и Cat методы serialize() и load_and_construct():

class Human : public ICreature {
private:
  Sex sex_;
  Activity activity_;
  std::string name_;

public:

  // ...

  template<class Archive>
  void serialize( Archive & ar ) {
    ar(sex_, activity_, name_);
  }

  template <class Archive>
  static void load_and_construct( Archive & ar, cereal::construct<Human> & construct ) {
    Sex s;
    Activity a;
    std::string n;
    ar(s, a, n);
    construct(s, n);
    construct->setActivity(a);
  }


class Cat : public ICreature {
private:
  Sex sex_;
  Activity activity_;

public:

  // ...

  template<class Archive>
  void serialize( Archive & ar ) {
    ar(sex_, activity_);
  }

  template <class Archive>
  static void load_and_construct( Archive & ar, cereal::construct<Cat> & construct ) {
    Sex s;
    Activity a;
    ar(s, a);
    construct(s);
    construct->setActivity(a);
  }

Метод serialize() является общим методом и работает в обе стороны, т.е. в простых случаях его может быть достаточно для сериализации и десериализации объектов данного класса.

В нашем случае всё чуточку сложнее, поскольку через конструктор невозможно создать (параметризовать) объект в любом нужном нам состоянии: по дизайну начальная “активность” конкретного человека или кошки не может быть задана в конструкторе (для этого предусмотрен специальный метод setActivity()). Метод load_and_construct делаем именно это: мы в известном порядке читаем известные типы данных из cereal-архива функцией ar(...), конструируем экземпляр через специальный construct и настраиваем свежесозданный объект в обычном режиме перед возвращением из метода.

Регистрация сериализуемых классов и их отношений

После модификации конкретных классов мы добавляем такой код в глобальную видимость:

CEREAL_REGISTER_TYPE(Human)
CEREAL_REGISTER_TYPE(Cat)
CEREAL_REGISTER_POLYMORPHIC_RELATION(ICreature, Human)
CEREAL_REGISTER_POLYMORPHIC_RELATION(ICreature, Cat)

Два последних макроса и включают поддержку полиморфизма в cereal.

Пример сериализации вектора интерфейсов

Стоит отметить, что cereal уже из коробки поддерживает многие стандартные типы C++, а также контейнеры STL. Сырые указатели не поддерживаются, зато поддерживаются std::shared_ptr<T>, которые мы и будем использовать. Поскольку интерфейсы не могут быть не-указателями, в примере имеем std::vector<std::shared_ptr<Creature>>, в который будем класть std::shared_ptr<Human> и std::shared_ptr<Cat> (скопированных из имеющихся конкретных инстанциаций).

#include <iostream>
#include <fstream>
#include <vector>

#include <cereal/archives/xml.hpp>
#include <cereal/types/memory.hpp>
#include <cereal/types/vector.hpp>

/*
 * ...
 */


/*
 * Запись
 */
void createCreatures(std::string fn) {
  /*
   * Экземпляры классов
   */
  Human h1 {Sex::male, "Nik"}, h2 {Sex::female, "Elizabeth"};
  Cat c1 {Sex::female}, c2 {Sex::male};

  /*
   * Прямая манипуляция
   */
  h1.setActivity(Activity::work);
  h2.setActivity(Activity::think);
  h2.setName("Liza");
  c1.setActivity(Activity::eat);

  /*
   * Вектор интерфейсов (в векторе - не данные, а указатели на них!)
   */
  std::vector<std::shared_ptr<ICreature>> ifvec {
    std::make_shared<Human>(h1),
    std::make_shared<Human>(h2),
    std::make_shared<Cat>(c1),
    std::make_shared<Cat>(c2)
  };

  /*
   * Открытие файла и ассоциация с ним cereal-архива
   */
  std::ofstream ofs;
  ofs.open(fn.c_str());
  cereal::XMLOutputArchive oarc {ofs};

  /*
   * Запись в архив оператором ()
   */
  oarc(ifvec);
}


/*
 * Чтение
 */
void readCreatures(std::string fn) {
  /*
   * Вектор интерфейсов - мы не знаем размеры и типы конкреций
   */
  std::vector<std::shared_ptr<ICreature>> ifvec;

  /*
   * Файл, cereal-архив
   */
  std::ifstream ifs;
  ifs.open(fn.c_str());
  cereal::XMLInputArchive iarc(ifs);

  /*
   * Десериализация вектора интерфейсов, включая автоматическое воссоздание конкретных инстанциаций Human и Cat
   */
  iarc(ifvec);

  /*
   * Подёргаем объекты через интерфейс
   */
  for (auto sp : ifvec)
    sp->printInfo();
}


int main()
{
  createCreatures("creatures.txt");

  readCreatures("creatures.txt");

  return 0;
}

Результат:

Mr. Nik works
Ms. Liza thinks
A female cat eats
A male cat sleeps