Подтвердить что ты не робот

Как реализовать сериализацию в С++

Всякий раз, когда мне приходится сериализовывать объекты в программе на С++, я возвращаюсь к такому виду шаблонов:

class Serializable {
  public:
    static Serializable *deserialize(istream &is) {
        int id;
        is >> id;
        switch(id) {
          case EXAMPLE_ID:
            return new ExampleClass(is);
          //...
        }
    }

    void serialize(ostream &os) {
        os << getClassID();
        serializeMe(os);
    }

  protected:
    int getClassID()=0;
    void serializeMe(ostream &os)=0;
};

Вышеприведенное работает на практике. Однако я слышал, что такой тип переключения идентификаторов классов - это зло и антипаттерн; что стандартный, OO-способ обработки сериализации в С++?

4b9b3361

Ответ 1

Используя что-то вроде Boost Serialization, хотя это не стандарт, является (по большей части) очень хорошо написанной библиотекой, которая делает хрюкать для вас.

В прошлый раз, когда мне приходилось вручную анализировать предопределенную структуру записи с явным деревом наследования, я в итоге использовал шаблон factory с (т.е. использовать карту ключа для функции создателя (шаблона), а не множество функций коммутатора), чтобы попытаться избежать проблемы, с которой вы столкнулись.

ИЗМЕНИТЬ
Основная реализация С++ объекта factory, упомянутого в предыдущем параграфе.

/**
* A class for creating objects, with the type of object created based on a key
* 
* @param K the key
* @param T the super class that all created classes derive from
*/
template<typename K, typename T>
class Factory { 
private: 
    typedef T *(*CreateObjectFunc)();

    /**
    * A map keys (K) to functions (CreateObjectFunc)
    * When creating a new type, we simply call the function with the required key
    */
    std::map<K, CreateObjectFunc> mObjectCreator;

    /**
    * Pointers to this function are inserted into the map and called when creating objects
    *
    * @param S the type of class to create
    * @return a object with the type of S
    */
    template<typename S> 
    static T* createObject(){ 
        return new S(); 
    }
public:

    /**
    * Registers a class to that it can be created via createObject()
    *
    * @param S the class to register, this must ve a subclass of T
    * @param id the id to associate with the class. This ID must be unique
    */ 
    template<typename S> 
    void registerClass(K id){ 
        if (mObjectCreator.find(id) != mObjectCreator.end()){ 
            //your error handling here
        }
        mObjectCreator.insert( std::make_pair<K,CreateObjectFunc>(id, &createObject<S> ) ); 
    }

    /**
    * Returns true if a given key exists
    *
    * @param id the id to check exists
    * @return true if the id exists
    */
    bool hasClass(K id){
        return mObjectCreator.find(id) != mObjectCreator.end();
    } 

    /**
    * Creates an object based on an id. It will return null if the key doesn't exist
    *
    * @param id the id of the object to create
    * @return the new object or null if the object id doesn't exist
    */
    T* createObject(K id){
        //Don't use hasClass here as doing so would involve two lookups
        typename std::map<K, CreateObjectFunc>::iterator iter = mObjectCreator.find(id); 
        if (iter == mObjectCreator.end()){ 
            return NULL;
        }
        //calls the required createObject() function
        return ((*iter).second)();
    }
};

Ответ 2

Сериализация - это трогательная тема в С++...

Быстрый вопрос:

  • Сериализация: короткоживущая структура, один кодер/декодер
  • Сообщения: более продолжительный срок службы, кодировщики/декодеры на нескольких языках.

2 полезны и имеют их использование.

Boost.Serialization является наиболее рекомендуемой библиотекой для сериализации, хотя странный выбор operator&, который сериализуется или десериализуется в зависимости от константы -ness действительно злоупотребляет перегрузкой оператора для меня.

Для обмена сообщениями я предпочел бы предложить Google Protocol Buffer. Они предлагают чистый синтаксис для описания сообщения и генерации кодеров и декодеров для огромного множества языков. Есть еще одно преимущество, когда имеет значение производительность: он позволяет ленивую десериализацию (т.е. Только часть блока сразу) по дизайну.

Перемещение

Теперь, что касается деталей реализации, это действительно зависит от того, что вы пожелаете.

  • Вам нужна версия, даже для обычной сериализации, вам, вероятно, потребуется обратная совместимость с предыдущей версией.
  • Вы можете или не нуждаетесь в системе tag + factory. Это необходимо только для полиморфного класса. И вам понадобится один factory для дерева наследования (kind), тогда... код может быть, темплатно, конечно!
  • Указатели/ссылки будут укусить вас в задницу... они ссылаются на позицию в памяти, которая изменяется после десериализации. Обычно я выбираю касательный подход: каждому объекту каждого kind присваивается id, уникальное для его kind, и поэтому я сериализую id, а не указатель. Некоторые структуры обрабатывают его до тех пор, пока вы не имеете циклической зависимости и сериализуете объекты, на которые указали/ссылались в первую очередь.

Лично я старался как можно больше отделить код сериализации/десериализации от фактического кода, который запускает класс. Особенно, я пытаюсь изолировать его в исходных файлах, так что изменения в этой части кода не уничтожают двоичную совместимость.

При версии

Обычно я стараюсь, чтобы сериализация и десериализация одной версии были близки друг к другу. Легче проверить, что они действительно симметричны. Я также пытаюсь абстрагировать обработку управления версиями непосредственно в моей структуре сериализации + несколько других вещей, потому что DRY следует придерживаться:)

При обработке ошибок

Чтобы облегчить обнаружение ошибок, я обычно использую пару "маркеров" (специальные байты) для разделения одного объекта на другой. Это позволяет мне сразу бросать во время десериализации, потому что я могу обнаружить проблему десинхронизации потока (т.е. Несколько съел слишком много байтов или не ел достаточно).

Если вы хотите разрешающую десериализацию, то есть десериализацию остальной части потока, даже если что-то не удалось, вам нужно будет перейти к байту-счету: каждому объекту предшествует его байт-счетчик и может есть только столько байта ( и, как ожидается, съест их всех). Этот подход хорош, поскольку он позволяет частично десериализовать: т.е. Вы можете сохранить часть потока, необходимую для объекта, и при необходимости десериализовать его только.

Тегирование (ваши идентификаторы классов) полезно здесь, а не только для диспетчеризации, а просто для проверки того, что вы фактически десериализируете правильный тип объекта. Это также позволяет получать сообщения об ошибках.

Вот некоторые сообщения об ошибках/исключения, которые вы можете пожелать:

  • No version X for object TYPE: only Y and Z
  • Stream is corrupted: here are the next few bytes BBBBBBBBBBBBBBBBBBB
  • TYPE (version X) was not completely deserialized
  • Trying to deserialize a TYPE1 in TYPE2

Обратите внимание, что насколько я помню, как Boost.Serialization и protobuf действительно помогают в обработке ошибок/версий.

protobuf имеет некоторые преимущества, из-за его возможности вложенных сообщений:

  • подсчет байтов естественно поддерживается, а также управление версиями
  • вы можете делать ленивую десериализацию (т.е. хранить сообщение и только десериализовать, если кто-то просит его).

Другим является то, что сложнее обрабатывать полиморфизм из-за фиксированного формата сообщения. Вы должны тщательно разработать их для этого.

Ответ 3

Ответ Yacoby может быть продолжен.

Я считаю, что сериализация может быть реализована способом, подобным управляемым языкам, если на самом деле реализуется система отражения.

В течение многих лет мы использовали автоматизированный подход.

Я был одним из разработчиков рабочего постпроцессора С++ и библиотеки Reflection: инструмента LSDC и ядра ядра Linderdaum (iObject + RTTI + Linker/Loader). См. Источник в http://www.linderdaum.com

Класс factory абстрагирует процесс создания экземпляра класса.

Чтобы инициализировать определенные элементы, вы можете добавить некоторые навязчивые RTTI и автогенерировать процедуры загрузки/сохранения для них.

Предположим, у вас есть класс iObject в верхней части вашей иерархии.

// Base class with intrusive RTTI
class iObject
{
public:
    iMetaClass* FMetaClass;
};

///The iMetaClass stores the list of properties and provides the Construct() method:

// List of properties
class iMetaClass: public iObject
{
public:
    virtual iObject* Construct() const = 0;
    /// List of all the properties (excluding the ones from base class)
    vector<iProperty*> FProperties;
    /// Support the hierarchy
    iMetaClass* FSuperClass;
    /// Name of the class
    string FName;
};

// The NativeMetaClass<T> template implements the Construct() method.
template <class T> class NativeMetaClass: public iMetaClass
{
public:
    virtual iObject* Construct() const
    {
        iObject* Res = new T();
        Res->FMetaClass = this;
        return Res;
    }
};

// mlNode is the representation of the markup language: xml, json or whatever else.
// The hierarchy might have come from the XML file or JSON or some custom script
class mlNode {
public:
    string FName;
    string FValue;
    vector<mlNode*> FChildren;
};

class iProperty: public iObject {
public:
    /// Load the property from internal tree representation
    virtual void Load( iObject* TheObject, mlNode* Node ) const = 0;
    /// Serialize the property to some internal representation
    virtual mlNode* Save( iObject* TheObject ) const = 0;
};

/// function to save a single field
typedef mlNode* ( *SaveFunction_t )( iObject* Obj );

/// function to load a single field from mlNode
typedef void ( *LoadFunction_t )( mlNode* Node, iObject* Obj );

// The implementation for a scalar/iObject field
// The array-based property requires somewhat different implementation
// Load/Save functions are autogenerated by some tool.
class clFieldProperty : public iProperty {
public:
    clFieldProperty() {}
    virtual ~clFieldProperty() {}

    /// Load single field of an object
    virtual void Load( iObject* TheObject, mlNode* Node ) const {
        FLoadFunction(TheObject, Node);
    }
    /// Save single field of an object
    virtual mlNode* Save( iObject* TheObject, mlNode** Result ) const {
        return FSaveFunction(TheObject);
    }
public:
    // these pointers are set in property registration code
    LoadFunction_t FLoadFunction;
    SaveFunction_t FSaveFunction;
};

// The Loader class stores the list of metaclasses
class Loader: public iObject {
public:
    void RegisterMetaclass(iMetaClass* C) { FClasses[C->FName] = C; }
    iObject* CreateByName(const string& ClassName) { return FClasses[ClassName]->Construct(); }

    /// The implementation is an almost trivial iteration of all the properties
    /// in the metaclass and calling the iProperty Load/Save methods for each field
    void LoadFromNode(mlNode* Source, iObject** Result);

    /// Create the tree-based representation of the object
    mlNode* Save(iObject* Source);

    map<string, iMetaClass*> FClasses;
};

Когда вы определяете ConcreteClass, полученный из iObject, вы используете некоторое расширение и средство генерации кода для создания списка процедур сохранения/загрузки и регистрационного кода для.

Посмотрим код для этого примера.

Где-то в рамках мы имеем пустое формальное определение

#define PROPERTY(...)

/// vec3 is a custom type with implementation omitted for brevity
/// ConcreteClass2 is also omitted
class ConcreteClass: public iObject {
public:
    ConcreteClass(): FInt(10), FString("Default") {}

    /// Inform the tool about our properties
    PROPERTY(Name=Int, Type=int,  FieldName=FInt)
    /// We can also provide get/set accessors
    PROPERTY(Name=Int, Type=vec3, Getter=GetPos, Setter=SetPos)
    /// And the other field
    PROPERTY(Name=Str, Type=string, FieldName=FString)
    /// And the embedded object
    PROPERTY(Name=Embedded, Type=ConcreteClass2, FieldName=FEmbedded)

    /// public field
    int FInt;
    /// public field
    string FString;
    /// public embedded object
    ConcreteClass2* FEmbedded;

    /// Getter
    vec3 GetPos() const { return FPos; }
    /// Setter
    void SetPos(const vec3& Pos) { FPos = Pos; }
private:
    vec3 FPos;
};

Автогенерированный регистрационный код будет:

/// Call this to add everything to the linker
void Register_ConcreteClass(Linker* L) {
    iMetaClass* C = new NativeMetaClass<ConcreteClass>();
    C->FName = "ConcreteClass";

    iProperty* P;
    P = new FieldProperty();
    P->FName = "Int";
    P->FLoadFunction = &Load_ConcreteClass_FInt_Field;
    P->FSaveFunction = &Save_ConcreteClass_FInt_Field;
    C->FProperties.push_back(P);
    ... same for FString and GetPos/SetPos

    C->FSuperClass = L->FClasses["iObject"];
    L->RegisterClass(C);
}

// The autogenerated loaders (no error checking for brevity):
void Load_ConcreteClass_FInt_Field(iObject* Dest, mlNode* Val) {
    dynamic_cast<ConcereteClass*>Object->FInt = Str2Int(Val->FValue);
}

mlNode* Save_ConcreteClass_FInt_Field(iObject* Dest, mlNode* Val) {
    mlNode* Res = new mlNode();
    Res->FValue = Int2Str( dynamic_cast<ConcereteClass*>Object->FInt );
    return Res;
}
/// similar code for FString and GetPos/SetPos pair with obvious changes

Теперь, если у вас есть JSON-подобный иерархический script

Object("ConcreteClass") {
    Int 50
    Str 10
    Pos 1.5 2.2 3.3
    Embedded("ConcreteClass2") {
        SomeProp Value
    }
}

Объект Linker будет разрешать все классы и свойства в методах сохранения/загрузки.

Извините за длинный пост, реализация становится еще больше, когда приходит вся обработка ошибок.

Ответ 4

Возможно, я не умный, но я думаю, что в конечном итоге тот же самый код, который вы написали, написан, просто потому, что С++ не имеет механизмов выполнения, чтобы делать что-то другое. Вопрос в том, будет ли он написан на заказ разработчиком, сгенерированным посредством метапрограммирования шаблонов (что я подозреваю, что boost.serialization делает), или сгенерировано с помощью какого-то внешнего инструмента, такого как генератор компилятора/кода IDL.

Вопрос о том, какой из этих трех механизмов (и, возможно, есть и другие возможности) - это то, что должно оцениваться на основе каждого проекта.

Ответ 5

Сериализация, к сожалению, никогда не будет абсолютно безболезненной в С++, по крайней мере, не в обозримом будущем, просто потому, что С++ не хватает критической языковой функции, которая упрощает сериализацию на других языках: reflection. То есть, если вы создаете класс Foo, С++ не имеет механизма для проверки класса программно во время выполнения, чтобы определить, какие переменные-члены он содержит.

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

Ответ 6

Я думаю, что самым близким к стандартному способу будет Boost.Serialization. Я хотел бы услышать, были ли и в каком контексте вы слышали эту информацию об идентификаторах классов. В случае сериализации я действительно не могу думать ни о каком другом способе (если, конечно, вы не знаете тип, который вы ожидаете при десериализации). А также Один размер не подходит для всех.