📜 ⬆️ ⬇️

C ++ serialization with polymorphism and prototypes

For a long time already, I became interested in the topic of serialization, and if specifically, in the serialization of objects stored by pointer to the base class. For example, if we want to load the application interface from a file, then most likely we will have to fill the container with the type “std :: vector <iWidget *>” with polymorphic objects. The question arises how to implement this. This I recently decided to do and that's what happened.

To begin with, I assumed that we would still have to inherit the iSerializable interface in the base class, of the following form:

class iSerializable { public: virtual void serialize (Node node) = 0; }; 

And the final class should look something like this:
')
 class ConcreteClass : public iSerializable { public: virtual void serialize (Node node) override { node.set_name ("BaseClass"); node.serialize (m_int, "int"); node.serialize (m_uint, "uint"); } private: int m_int = 10; unsigned int m_uint = 200; }; 

The Node class must, in this case, implement object processing using an XML parser. For parsing, I took pugixml. Node contains a field:
 xml_node m_node; 

the function template of the host object and its name:
 template <typename T> void serialize (T& value, const wstring& name) { value.serialize (get_node (name)); } 

(where the get_node function looks for the xml-file element with the necessary name, or creates it itself).

For built-in types, the serialize function template is specified as follows:

 template <> void serialize (int& value, const string& 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); } 

This function performs serialization / deserialization depending on the presence of an attribute in the xml file.
The template specialization for pointers to objects that are inherited from the iSerializable interface is also defined:

 template <> void serialize (iSerializable*& object, const string& name) 


This is where the fun begins. According to the pointer, any object from the class hierarchy may be required, respectively, it is required to unambiguously determine the class of the object by name and create an object of this particular class.

 { if (!object) m_factory->get_object (object, m_node.find_child_by_attribute ("name", name.c_str ()).name ()); object->serialize (get_node_by_attr (name)); } 

It is worth noting that here we use the get_node_by_attr function to get the Node object, which also acts as the get_node function, with the difference that this function searches for an element not by name but by the value of the “name” attribute, since class of the required object.

This is also where the m_factory object of the PrototypeFactory class, which is used by the Node class, comes into play. It passes a pointer to a new object created by the prototype stored in it. If you look at the class definition, then the Object structure will be defined there:

 struct ObjectBase { wstring name; ObjectBase (const string& _name) : name (_name) {} virtual void* get_copy () = 0; }; template <typename Data> struct Object : public ObjectBase { Object (const string& _name) : ObjectBase (_name) {} virtual void* get_copy () override { return (void*) new Data (); } }; 

objects which are stored in the vector. The content of this vector is controlled by two functions:

 template <typename T> void set_object (const string& name) { if (std::find_if ( m_prototypes.begin (), m_prototypes.end (), [name] (ObjectBase* obj) { return obj->name == name; } ) == m_prototypes.end () ) m_prototypes.push_back (new Object<T> (name)); } template <typename T> void get_object (T*& object, const string& name) const { auto it = find_if (m_prototypes.begin (), m_prototypes.end (), [name] (ObjectBase* obj) { return obj->name == name; }); if (it != m_prototypes.end ()) object = (T*) (*it)->get_copy (); else throw std::exception ("Prototype wasn't found!"); } 

Thus, we can place objects of any type into PrototypeFactory and receive them by specifying the name under which they are stored. In order to control the introduction of objects at the beginning of the factory and correct their removal in the destructor, it was necessary to introduce a global function:

 void init_prototypes (Prototypes::PrototypeFactory*); 

The function definition will need to be made after the definition of all classes. It should contain input of all objects necessary for the work of the PrototypeFactory class:

 void init_prototypes (Prototypes::PrototypeFactory* factory) { factory->set_object< ConcreteClass > (" ConcreteClass "); } 

This function will be called in the constructor and will enter objects into the factory.

 PrototypeFactory () { init_prototypes (this); } 

Thus, we perform the serialization of an object according to its pointer.

Also in the Node class there are functions that allow serializing / deserializing element containers:
 template <template <typename T, class alloc> class Container, typename Data, typename alloc> void serialize ( Container<Data*, alloc>& container, const string& name, const string& subname ) { Node node = get_node (name); size_t size (container.size ()); node.serialize (size, "size"); if (container.empty ()) container.assign (size, nullptr); size_t count (0); for (auto i = container.begin (); i < container.end (); ++i) node.serialize (*i, subname + std::to_string (count++)); } template <template <typename T, class alloc> class Container, typename Data, typename alloc> void serialize ( Container<Data, alloc>& container, const string& name, const string& subname ) { Node node = get_node (name); size_t size (container.size ()); node.serialize (size, "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_by_attr (subname + std::to_string (count++))); } 


The Serializer class runs all this construction:

 class Serializer { public: Serializer() {} template <class Serializable> void serialize (Serializable* object, const string& filename) { object->serialize (m_document.append_child (L"")); m_document.save_file ((filename+".xml").c_str ()); m_document.reset (); } template <class Serializable> void deserialize (Serializable* object, const string& filename) { if (m_document.load_file ((filename + ".xml").c_str ())) object->serialize (m_document.first_child ()); m_document.reset (); } private: xml_document m_document; PrototypeFactory m_factory; }; 


Now it's time to see how it will look to use:

 class BaseClass : public iSerializable { public: virtual ~BaseClass () {} virtual void serialize (Node node) override { node.set_name ("BaseClass"); node.serialize (m_int, "int"); node.serialize (m_uint, "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 ("MyConcreteClass"); node.serialize (m_float, "float"); node.serialize (m_double, "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 ("SomeonesConcreteClass"); node.serialize (m_str, "string"); node.serialize (m_bool, "boolean"); } private: wstring m_str = "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 ("SomeContainers"); node.serialize (vec_ptr, "containerPtr", "myclass_"); node.serialize (vec_arg, "containerArg", "myclass_"); } private: std::vector<BaseClass*> vec_ptr; std::vector<BaseClass> vec_arg; }; void init_prototypes (Prototypes::PrototypeFactory* factory) { factory->set_object<BaseClass> ("BaseClass"); factory->set_object<MyConcreteClass> ("MyConcreteClass"); factory->set_object<SomeonesConcreteClass> ("SomeonesConcreteClass"); } int main (int argc, char* argv[]) { Serializer* document = new Serializer (); Container* container = new Container (); document->deserialize (container, "document"); document->serialize (container, "doc"); delete container; delete document; return 0; } 


This shows that the use interface turned out to be rather cumbersome, but this is a fee (in my opinion, inevitable) for the ability to serialize polymorphic objects.

If you want to look at this miracle in action, you can download the source .

Source: https://habr.com/ru/post/245305/


All Articles