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

Уже достаточно давно заинтересовался темой сериализации, а если конкретно, то сериализацией объектов, хранящихся по указателю на базовый класс. Например, если мы хотим загружать интерфейс приложения из файла, то скорее всего нам придется заполнять полиморфными объектами контейнер по типу “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;
}


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

Если возникло желание посмотреть на это чудо в действии, то можете скачать исходники.

Средняя зарплата в IT

113 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 10 037 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 17

    +1
    Использование wide-литералов и std::wstring — так себе идея.
    Взамен специализации шаблона функции в данном случае лучше прибегнуть к перегрузке и не передавать builtin типы по ссылке.
    Явный вызов new/delete рядом с лямбда-выражениями и прочими C++11 фишками выглядит дико :-)
      0
      Простите, не там ответил на комментарий — ответ
      0
      Использование wide-литералов и std::wstring — так себе идея.

      не передавать builtin типы по ссылке

      Хотелось бы узнать поподробнее, чем плохи wide-литералы, и как Вы бы предпочли сериализовать/десериализовать встроенные типы?

      Явный вызов new/delete рядом с лямбда-выражениями и прочими C++11 фишками выглядит дико :-)

      А как Вам вызов malloc в PrototypeFactory?
        +1
        Про wchar_t есть неплохой ответ по ссылке stackoverflow.com/questions/11107608/whats-wrong-with-c-wchar-t-and-wstrings-what-are-some-alternatives-to-wide
        В данном случае они как минимум являются преждевременной пессимизацией, так как зачастую sizeof(wchar_t) > sizeof(char), а все используемые в идентификаторах C++ символы помещаются в обычный char.
        Про встроенные типы, я предлагаю заменить первое объявление на второе:
        template <> void serialize (int& value, const wstring& name)
        void serialize(const int value, const wstring& name)

        Применение malloc/realloc оправдано, там где это нужно (свои аллокаторы, еще какие-то нужды). В данном коде необходимость malloc'a мне не ясна. Более того, судя по всему вы выделяете память под произвольный тип T и «инициализируете» его неким прототипом с помощью memcpy, а что если тип T не является trivially-copyable (например он строка, или любой другой тип, владеющий какими-то динамическими ресурсами)? Или десериализация нетривиальных типов не предполагалась?

        Кстати, вот тут exception-unsafety имеется:
        std::vector<BaseClass*> vec_ptr = { new BaseClass, new BaseClass, new MyConcreteClass, new SomeonesConcreteClass };
        Любое исключение приведет к утечке памяти.
          0
          Применение malloc/realloc оправдано, там где это нужно (свои аллокаторы, еще какие-то нужды). В данном коде необходимость malloc'a мне не ясна. Более того, судя по всему вы выделяете память под произвольный тип T и «инициализируете» его неким прототипом с помощью memcpy, а что если тип T не является trivially-copyable (например он строка, или любой другой тип, владеющий какими-то динамическими ресурсами)? Или десериализация нетривиальных типов не предполагалась?

          При десериализации объекта мы, зная имя класса этого объекта, должны будем получить участок памяти равный размеру объекта этого класса. В PrototypeFactory мы его и получим. Естественно это будет точная копия объекта, созданного конструктором по умолчанию. Остальную работу по наполнению его содержимым должен будет проделать объект, получивший этот участок памяти.
            0
            Где этот код? Что будет с ним, если memcpy скопирует объект, владеющий какими-либо ресурсами?
              0
              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));
              }
              — собственно пример использования из статьи.

              Что касается ресурсов, то если конструктор класса по умолчанию создает объекты в динамической памяти, то это поле должно быть перезадано в полученном таким образом объекте.
              Предполагается, что если мы используем для создания объектов класса PrototypeFactory, то мы должны обеспечить ему простой конструктор, не создающий ресурсов, и все данные будем вводить в объект после получения его из PrototypeFactory. Естественно в этом случае выделять память в конструкторе по умолчанию бессмысленно, так как она будет лежать в прототипе безо всякой пользы.
            0
            Про встроенные типы, я предлагаю заменить первое объявление на второе:
            template <> void serialize (int& value, const wstring& name)
            void serialize(const int value, const wstring& name)

            Но в таком случае мы не сможем изменить значение данных класса при десериализации.
              0
              Ок, не заметил, что ссылка не на константу. А почему сериализация модифицирует объект?
                0
                Потому что в общем случае мы не можем знать что будет происходить — сериализация или десериализация.
          +2
          Почему бы просто не взять буст?

          #include <boost/archive/text_oarchive.hpp> 
          #include <boost/archive/text_iarchive.hpp> 
          #include <iostream> 
          #include <sstream>  
          
          class MyConcreteClass 
          { 
          public: 
            MyConcreteClass() 
            { 
            } 
          
            MyConcreteClass (float a, double b) 
              : mFloat(a) 
              , mDouble(b)
            { 
            } 
          
            float getFloat() const
            {
              return mFloat;
            }
            
            double getDouble() const
            {
              return mDouble;
            }
          
          private: 
            friend class boost::serialization::access; 
          
            template <typename Archive> 
            void serialize(Archive &ar, const unsigned int version) 
            { 
              ar & mFloat; 
              ar & mDouble;
            } 
          
            float mFloat;
            double mDouble;
          }; 
          
          template<typename T>
          std::string serialize(T* object)
          {
            std::stringstream sss; 
            boost::archive::text_oarchive oa(sss); 
            oa << object; 
            return sss.str();
          }
          
          template<typename T>
          T* deserialize(const std::string& object) 
          { 
            std::stringstream sss(object);
            boost::archive::text_iarchive ia(sss); 
            T* retval; 
            ia >> retval; 
            return retval;
          } 
          
          int main() 
          { 
            MyConcreteClass* mcc = new MyConcreteClass( 55.4f, 23.9 );
            auto str = serialize(mcc);
            std::cout << str << std::endl;
            
            MyConcreteClass* dmcc = deserialize<MyConcreteClass>(str);
            std::cout << "float: " << dmcc->getFloat() << "; double: " << dmcc->getDouble() << std::endl; 
          } 
          


          (понятно, что оформлено все на скорую руку, но я примера ради)
            0
            Написал этот код я просто из спортивного интереса, а статью решил написать после прочтения этой ветки обсуждения статьи, также посвященной сериализации, решив, что кому-нибудь это покажется любопытным.
            +1
            Самое интересное, зачем здесь полиморфизм? Зачем сувать vtable туда, где этого не требуется? Всё же это C++, а не Java или C#, принципы разработки отличаются. Да и уж хотя бы использовали бы CRTP, тогда код будет более эффективным.
              0
              Уже достаточно давно заинтересовался темой сериализации, а если конкретно, то сериализацией объектов, хранящихся по указателю на базовый класс.
              Тут объекты полиморфны сами по себе по условию задачи.
                0
                Всё же это C++, а не Java или C#


                Если Вы помните, C++ предоставляет множество парадигм и абсолютно не ограничивает их использование и даже смешивание. Конечно есть некий оверхед в использовании динамического полиморфизма. Но чем конкретно Вам не нравится подобный подход?
                0
                Cereal не смотрели?
                  0
                  Только недавно узнал об этой библиотеке, и ещё в ней не разобрался.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое