Как реализовать сериализацию в C ++ - PullRequest
28 голосов
/ 27 ноября 2009

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

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-способ обработки сериализации в C ++?

Ответы [ 6 ]

26 голосов
/ 27 ноября 2009

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

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

EDIT
Базовая реализация C ++ фабрики объектов, упомянутой в предыдущем абзаце.

/**
* 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)();
    }
};
19 голосов
/ 27 ноября 2009

Сериализация - тема для C ++ ...

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

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

2 полезны и имеют свое применение.

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

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

Переезд

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

  • Вам потребуется версия , даже для регулярной сериализации вам, вероятно, все равно потребуется обратная совместимость с предыдущей версией.
  • Вам может понадобиться, а может и не понадобиться система 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 также имеет некоторые привилегии из-за своей способности вложения сообщений:

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

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

6 голосов
/ 26 апреля 2012

Ответ Якоби может быть расширен в дальнейшем.

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

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

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

Фабрика классов абстрагирует процесс создания класса.

Чтобы инициализировать определенные элементы, вы можете добавить некоторые навязчивые 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's 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-подобный иерархический скрипт

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

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

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

5 голосов
/ 27 ноября 2009

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

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

5 голосов
/ 27 ноября 2009

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

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

2 голосов
/ 27 ноября 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...