Простая сериализация/десериализация полиморфических данных в C++
Введение⌗
Предположим, что мы, как это часто бывает, имеем вектор интерфейсов к конкретным объектам:
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