Pull to refresh

Сериализация C++ с полиморфизмом и прототипами

Reading time6 min
Views20K
Уже достаточно давно заинтересовался темой сериализации, а если конкретно, то сериализацией объектов, хранящихся по указателю на базовый класс. Например, если мы хотим загружать интерфейс приложения из файла, то скорее всего нам придется заполнять полиморфными объектами контейнер по типу “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 ("BaseClass");
        node.serialize (m_int, "int");
        node.serialize (m_uint, "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 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);
} 

Эта функция производит сериализацию/десериализацию в зависимости от наличия атрибута в xml-файле.
Также определена специализация шаблона для указателей на объекты, являющихся наследниками интерфейса iSerializable:

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


Здесь начинается самое интересное. По указателю может потребоваться любой объект из иерархии классов, соответственно требуется однозначно определить класс объекта по имени и создать объект именно этого класса.

{
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));
}

Стоит обратить внимание, что здесь мы используем для получения нового объекта Node функцию get_node_by_attr, которая действует также как функция get_node, с той разницей, что эта функция ищет элемент не по имени, а по значению атрибута «name», так как именем элемента здесь будет класс требуемого объекта.

Здесь же в игру вступает объект m_factory класса PrototypeFactory, которым пользуется класс Node. Он передает указатель на новый объект, созданный по прототипу, хранящемуся в нем. Если посмотреть определение класса, то там будет определена структура Object:

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 ();
    }
};

объекты которой хранятся в векторе. Содержимое этого вектора контролируется двумя функциями:

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!");
}

Таким образом, мы можем помещать в PrototypeFactory объекты любого типа и получать их, указав, под каким именем они хранятся. Для того чтобы, проконтролировать внесение объектов в начале работы фабрики и корректного их удаления в деструкторе, пришлось ввести глобальную функцию:

void init_prototypes (Prototypes::PrototypeFactory*);

Определение функции будет необходимо сделать после определения всех классов. Она должна содержать ввод всех необходимых для работы класса PrototypeFactory объектов:

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

Эта функция будет вызываться в конструкторе и будет вводить объекты в фабрику.

PrototypeFactory () { init_prototypes (this); }

Таким образом, мы выполняем сериализацию объекта по его указателю.

Так же в классе Node есть функции позволяющие сериализовать/десериализовать контейнеры элементов:
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++)));
}


Заправляет всей этой конструкцией класс Serializer:

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;
};


Теперь настало время посмотреть, как это будет выглядеть в использовании:

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;
}


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

Если возникло желание посмотреть на это чудо в действии, то можете скачать исходники.
Tags:
Hubs:
Total votes 11: ↑9 and ↓2+7
Comments18

Articles