Уже достаточно давно заинтересовался темой сериализации, а если конкретно, то сериализацией объектов, хранящихся по указателю на базовый класс. Например, если мы хотим загружать интерфейс приложения из файла, то скорее всего нам придется заполнять полиморфными объектами контейнер по типу “std::vector<iWidget*>”. Возникает вопрос, как подобное реализовать. Этим я недавно решил заняться и вот что получилось.
Для начала я предположил, что нам все-таки придется унаследовать в базовом классе интерфейс iSerializable, такого вида:
class iSerializable
{
public:
virtual void serialize (Node node) = 0;
};
И конечный класс должен выглядеть примерно так:
class ConcreteClass : public iSerializable
{
public:
virtual void serialize (Node node) override
{
node.set_name (L"BaseClass");
node.serialize (m_int, L"int");
node.serialize (m_uint, L"uint");
}
private:
int m_int = 10;
unsigned int m_uint = 200;
};
Класс Node должен, в таком случае реализовывать обработку объекта, с помощью XML-парсера. Для парсинга я взял pugixml. Node содержит поле:
xml_node m_node;
Шаблон функции принимающей объект и его имя:
template <typename T> void serialize (T& value, const wstring& name)
{
value.serialize (get_node (name));
}
(где функция get_node ищет элемент xml-файла с нужным именем, или создает его сама).
И уточняет шаблон функции serialize для базовых типов примерно таким образом:
template <> void serialize (int& value, const wstring& name)
{
xml_attribute it = m_node.attribute (name.c_str ());
if (it) value = it.as_int ();
else m_node.append_attribute (name.c_str ()).set_value (value);
}
А также указателей на объекты, являющихся наследниками интерфейса iSerializable:
template <> void serialize (iSerializable*& object, const wstring& name)
Здесь начинается самое интересное. По указателю может быть любой объект из иерархии классов, соответственно требуется однозначно определить класс объекта и создать именно его.
{
if (!object) m_factory->get_object (object, m_node.find_child_by_attribute (L"name", name.c_str ()).name ());
object->serialize (get_node_by_attr (name));
}
Стоит обратить внимание, что здесь мы используем для получения нового объекта Node функцию get_node_by_attr, которая действует также как функция get_node, с той разницей, что эта функция ищет элемент не по имени, а по значению атрибута «name», так как именем элемента здесь будет класс требуемого объекта.
Здесь же в игру вступает объект m_factory класса под названием PrototypeFactory, которым пользуется класс Node. Если посмотреть определение класса, то там будет определена структура Object:
struct Object
{
public:
wstring name;
void* object;
size_t size;
template <typename T> Object (const wstring& _name, T* obj) :
name (_name), object (obj), size (sizeof (T)) {}
};
объекты которой хранятся в векторе. Содержимое этого вектора контролируется двумя функциями:
template <typename T> void set_object (wstring name)
{
auto it = std::find_if (
m_prototypes.begin (),
m_prototypes.end (),
[name] (Object obj) { return obj.name == name; }
);
if (m_creating)
{
if (it == m_prototypes.end ())
m_prototypes.push_back (Object (name, new T ()));
}
else
if (it != m_prototypes.end ()) delete ((T*) obj.object);
}
template <typename T> void get_object (T*& object, const wstring& name) const
{
auto iter = find_if (m_prototypes.begin (), m_prototypes.end (), [name] (Object obj) { return obj.name == name; });
if (iter != m_prototypes.end ()) memcpy_s (object = (T*) malloc (iter->size), iter->size, iter->object, iter->size);
else throw std::exception ("Prototype wasn't found!");
}
Таким образом, мы можем помещать в PrototypeFactory объекты любого типа и получать их, указав, под каким именем они хранятся. Для того чтобы, проконтролировать внесение объектов в начале работы фабрики и корректного их удаления в деструкторе, пришлось ввести глобальную функцию:
void init_prototypes (Prototypes::PrototypeFactory*);
Определение функции будет необходимо сделать после определения всех классов. Она должна содержать ввод всех необходимых для работы класса PrototypeFactory объектов:
void init_prototypes (Prototypes::PrototypeFactory* factory)
{
factory->set_object< ConcreteClass > (L" ConcreteClass ");
}
Эта функция будет вызываться дважды: в конструкторе и в деструкторе.
PrototypeFactory () : m_creating (true) { init_prototypes (this); m_creating = false; }
~PrototypeFactory () { init_prototypes (this); }
В зависимости от состояния переменной m_creating она будет сначала вводить, а потом удалять объекты из фабрики.
Таким образом, мы выполняем сериализацию объекта по его указателю.
Так же в классе Node есть функции позволяющие сериализовать контейнеры элементов:
template <template <typename T, class alloc> class Container, typename Data, typename alloc> void serialize (
Container<Data*, alloc>& container,
const wstring& name,
const wstring& subname
)
{
Node node = get_node (name);
size_t size (container.size ());
node.serialize (size, L"size");
if (container.empty ()) container.assign (size, nullptr);
size_t count (0);
for (auto i = container.begin (); i < container.end (); ++i)
(*i)->serialize (node.get_node_by_attr (subname + std::to_wstring (count++)));
}
template <template <typename T, class alloc> class Container, typename Data, typename alloc> void serialize (
Container<Data, alloc>& container,
const wstring& name,
const wstring& subname
)
{
Node node = get_node (name);
size_t size (container.size ());
node.serialize (size, L"size");
if (container.empty ()) container.assign (size, Data ());
size_t count (0);
for (auto i = container.begin (); i < container.end (); ++i)
i->serialize (node.get_node (subname + std::to_wstring (count++)));
}
Заправляет всей этой конструкцией класс Serializer:
class Serializer
{
public:
Serializer() {}
template <class Serializable> void serialize (Serializable* object, const wstring& filename)
{
m_document.reset ();
object->serialize (m_document.append_child (L""));
m_document.save_file ((filename+L".xml").c_str ());
}
template <class Serializable> void deserialize (Serializable* object, const wstring& filename)
{
m_document.reset ();
if (m_document.load_file ((filename + L".xml").c_str ())) object->serialize (m_document.first_child ());
}
private:
xml_document m_document;
PrototypeFactory m_factory;
};
Теперь настало время посмотреть, как это будет выглядеть в использовании:
class BaseClass : public iSerializable
{
public:
virtual ~BaseClass () {}
virtual void serialize (Node node) override
{
node.set_name (L"BaseClass");
node.serialize (m_int, L"int");
node.serialize (m_uint, L"uint");
}
private:
int m_int = 10;
unsigned int m_uint = 200;
};
class MyConcreteClass : public BaseClass
{
public:
virtual void serialize (Node node) override
{
BaseClass::serialize (node);
node.set_name (L"MyConcreteClass");
node.serialize (m_float, L"float");
node.serialize (m_double, L"double");
}
private:
float m_float = 1.0f;
double m_double = 2.0;
};
class SomeonesConcreteClass : public BaseClass
{
public:
virtual void serialize (Node node) override
{
BaseClass::serialize (node);
node.set_name (L"SomeonesConcreteClass");
node.serialize (m_str, L"string");
node.serialize (m_bool, L"boolean");
}
private:
wstring m_str = L"ololo";
bool m_bool = true;
};
class Container
{
public:
~Container ()
{
for (BaseClass* ptr : vec_ptr) delete ptr;
vec_ptr.clear ();
vec_arg.clear ();
}
void serialize (Node node)
{
node.set_name (L"SomeContainers");
node.serialize (vec_ptr, L"containerPtr", L"myclass_");
node.serialize (vec_arg, L"containerArg", L"myclass_");
}
private:
std::vector<BaseClass*> vec_ptr = { new BaseClass, new BaseClass, new MyConcreteClass, new SomeonesConcreteClass };
std::vector<BaseClass> vec_arg = { BaseClass (), BaseClass (), BaseClass (), BaseClass () };
};
void init_prototypes (Prototypes::PrototypeFactory* factory)
{
factory->set_object<BaseClass> (L"MyClass");
factory->set_object<MyConcreteClass> (L"MyConcreteClass");
factory->set_object<SomeonesConcreteClass> (L"SomeonesConcreteClass");
factory->set_object<Container> (L"SomeContainers");
}
int main (int argc, char* argv[])
{
Serializer* document = new Serializer ();
Container* container = new Container ();
document->deserialize (container, L"document");
document->serialize (container, L"doc");
delete container;
delete document;
return 0;
}
Отсюда видно, что интерфейс использования получился довольно громоздким, но это является платой (на мой взгляд, неизбежной) за возможность сериализовать полиморфные объекты.
Если возникло желание посмотреть на это чудо в действии, то можете скачать исходники.
Автор: garik710