2009-11-27 19 views
26

Cada vez que me encuentro que necesitan para serializar objetos en un programa en C++, que caen de nuevo a este tipo de patrón:Cómo implementar la serialización en 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; 
}; 

lo anterior funciona bastante bien en la práctica. Sin embargo, he escuchado que este tipo de cambio de ID de clase es malo y antipatrón; ¿Cuál es el estándar, OO-way de manejar la serialización en C++?

+0

@SergeyK:. Que los cambios recientes? Ciertamente no he oído hablar de ninguno. –

+0

Me refiero a esta respuesta: http://stackoverflow.com/a/10332336/1065190 –

+0

@SergeyK .: ¡Ah! Creo que la sección de comentarios sobre la respuesta en sí es probablemente el mejor lugar para discutirlo. De hecho, ya he comenzado. Me parece bastante esotérico, especialmente la idea de fusionar la serialización con getters y setters automáticos (generalmente es malo mezclar diferentes conceptos). También me recuerda a cierto proyecto de QT ... al final, tienes cuasi-C++ y pierdes la portabilidad porque dependes de la disponibilidad de la herramienta que se supone debe transformarla en C++ apropiado y compilable. No estoy conteniendo la respiración. –

Respuesta

26

El uso de algo como Boost Serialization, aunque de ninguna manera es un estándar, es una biblioteca (en su mayor parte) muy bien escrita que hace el trabajo duro para usted.

La última vez que tuve que analizar manualmente una estructura de registro predefinida con un árbol de herencia claro, terminé usando el factory pattern con clases registrables (es decir, utilizando un mapa de clave para una función de creador de plantilla en lugar de una gran cantidad de cambiar funciones) para tratar de evitar el problema que estaba teniendo.

EDITAR
Una implementación en C++ básico de una factoría de objetos mencionados en el párrafo anterior.

/** 
* 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

supongo que lo más parecido a una forma estándar sería Boost.Serialization. Me gustaría saber qué fue y en qué contexto oíste sobre las identificaciones de clase. En el caso de la serialización, realmente no puedo pensar de otra manera (a menos, por supuesto, que conozcas el tipo que esperas al deserializar). Y también, One size does not fit all.

5

Quizás no soy inteligente, pero creo que al final se escribe el mismo tipo de código que usted ha escrito, simplemente porque C++ no tiene los mecanismos de tiempo de ejecución para hacer algo diferente. La pregunta es si será escrito a medida por un desarrollador, generado a través de metaprogramación de plantillas (que es lo que sospecho que realza boost.serialization), o generado a través de alguna herramienta externa como un compilador/generador de códigos IDL.

La pregunta de cuál de estos tres mecanismos (y tal vez hay otras posibilidades, también) es algo que debe evaluarse por proyecto.

+0

¡Exactamente lo que quería decir! – kizzx2

5

serialización es, por desgracia nunca va a ser totalmente indoloro en C++, al menos no en el futuro inmediato, simplemente porque C++ carece de la característica del lenguaje crítico que hace posible la fácil serialización en otros idiomas: reflection. Es decir, si crea una clase Foo, C++ no tiene ningún mecanismo para inspeccionar la clase mediante programación en tiempo de ejecución para determinar qué variables de miembro contiene.

Por lo tanto, no hay forma de crear funciones de serialización generalizadas. De una forma u otra, debe implementar una función de serialización especial para cada clase. Boost.Serialization no es diferente, simplemente le proporciona un marco conveniente y un buen conjunto de herramientas que lo ayudan a hacer esto.

+0

El [C++ Middleware Writer] (http://webebenezer.net) automatiza la escritura de las funciones de serialización. –

+0

En realidad, C++ tiene (alguna) reflexión en tiempo de compilación disponible a través de las bibliotecas metatemplate. Se puede aprovechar con un abuso de la directiva de preprocesador y Boost.Fusion. Preferiría no seguir ese camino: x –

18

La serialización es un tema delicado en C++ ...

pregunta rápida:

  • serialización: Estructura de corta duración, un codificador/decodificador
  • Mensajería: vida más larga, codificadores/decodificadores en varios idiomas

El 2 son útiles y tienen su uso.

Boost.Serialization es la biblioteca más recomendado para la serialización por lo general, aunque la elección extraña de operator& la que serializa o Deserializa dependiendo de la const-dad es realmente un abuso de la sobrecarga de operadores para mí.

Para mensajes, yo sugeriría Google Protocol Buffer. Ofrecen una sintaxis limpia para describir el mensaje y generar codificadores y decodificadores para una gran variedad de idiomas. También hay otra ventaja cuando el rendimiento importa: permite la deserialización lenta (es decir, solo una parte del blob a la vez) por diseño.

Pasando

Ahora, en cuanto a los detalles de la aplicación, lo que realmente depende de lo que desea.

  • Usted necesita versiones, incluso para la serialización regular, es probable que tengas la compatibilidad con la versión anterior de todos modos.
  • Puede o no necesitar un sistema de tag + factory. Solo es necesario para la clase polimórfica. Y necesitará un factory por árbol de herencia (kind), luego ... ¡el código se puede templatar, por supuesto!
  • Los punteros/referencias te van a morder el trasero ... hacen referencia a una posición en la memoria que cambia después de la deserialización. Normalmente elijo un enfoque tangente: a cada objeto de cada kind se le asigna un id, único para su kind, así que serializo el id en lugar de un puntero. Algún marco lo maneja siempre que no tenga dependencia circular y serialice los objetos apuntados/referenciados primero.

Personalmente, traté tanto como pude para separar el código de serialización/deserialización del código real que ejecuta la clase. Especialmente, trato de aislarlo en los archivos fuente para que los cambios en esta parte del código no anulen la compatibilidad binaria.

En versiones

lo general tratan de mantener la serialización y deserialización de una versión muy juntos. Es más fácil comprobar que son verdaderamente simétricos. También trato de abstraer el manejo de versiones directamente en mi marco de serialización + algunas otras cosas, porque seco debe ser adherido a :)

El control de errores

Para facilitar la detección de errores, que suelen utilizar un par de 'marcadores' (bytes especiales) para separar un objeto de otro.Me permite lanzar inmediatamente durante la deserialización porque puedo detectar un problema de desincronización de la secuencia (es decir, algo comí demasiados bytes o no comí lo suficiente).

Si desea una deserialización permisiva, es decir, deserializar el resto de la transmisión, incluso si algo falló anteriormente, tendrá que moverse hacia el recuento de bytes: cada objeto va precedido de su número de bytes y solo puede comer tanto byte (y se espera que se los coma a todos). Este enfoque es bueno porque permite la deserialización parcial: es decir, puede guardar la parte de la secuencia requerida para un objeto y solo deserializarla si es necesario.

El etiquetado (sus ID de clase) es útil aquí, no (solo) para despachar, sino simplemente para comprobar que está deserializando realmente el tipo correcto de objeto. También permite bonitos mensajes de error.

Éstos son algunos mensajes de error/excepciones que pueden desear:

  • 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

Tenga en cuenta que por lo que yo recuerdo tanto Boost.Serialization y protobuf realmente ayuda para el manejo del error/versión.

protobuf tiene algunas ventajas también, debido a su capacidad de mensajes de anidación:

  • se apoya de forma natural el byte de recuento, así como el control de versiones
  • que puede hacer deserialización vago (es decir, almacenar el mensaje y solo deserializar si alguien lo solicita)

La contraparte es que es más difícil manejar el polimorfismo debido al formato fijo del mensaje. Tienes que diseñarlos cuidadosamente para eso.

6

La respuesta de Yacoby se puede ampliar aún más.

Creo que la serialización se puede implementar de forma similar a los lenguajes administrados si uno realmente implementa un sistema de reflexión.

Durante años hemos estado utilizando el enfoque automatizado.

Fui uno de los implementadores del postprocesador de C++ en funcionamiento y de la biblioteca Reflection: herramienta LSDC y Linderdaum Engine Core (iObject + RTTI + Linker/Loader). Consulte la fuente en http://www.linderdaum.com

La fábrica de clases abstrae el proceso de creación de instancias de clases.

Para inicializar miembros específicos, puede agregar algunos RTTI intrusivos y generar automáticamente los procedimientos de carga/guardado para ellos.

Supongamos que tiene la clase iObject en la parte superior de su jerarquía.

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

Al definir la ConcreteClass derivado de IObject, se utiliza alguna extensión y la herramienta de generación de código para producir la lista de guardar/procedimientos de carga y el código de registro para.

Déjanos ver el código para esta muestra.

En algún lugar en el marco tenemos un vacío formal de definir

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

El código de registro autogenerado sería:

/// 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 

Ahora, si usted tiene la JSON-como la escritura jerárquica

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

el enlazador resolvería todas las clases y propiedades en Guardar/Cargar métodos.

Lo siento por el largo post, la aplicación crece aún más grande cuando todo el manejo de errores viene en

+5

Lo he visto más feo ... pero no a menudo. Realmente no me gusta tener un pase extra en el proceso de compilación modificando * mi * código. No me importa tener agregado el código * extra (como los archivos 'protobuf'), pero cuando el pase extra arruina el archivo y termina con una compilación incorrecta, rastrear el error es una pesadilla. –

+2

Bueno, estamos trabajando en torno a la falta de herramientas en el idioma en sí, esto no se puede hacer sin problemas. No hay destrucción de código fuente: la metainformación generada también se agrega a los nuevos archivos fuente. Si no los incluye en su proyecto, no hay basura (y tampoco fábricas/serialización). Los errores de compilación parecen complicados (una vez que omite algo en la declaración PROPERTY), pero uno puede acostumbrarse a ellos, como todos hacemos con los errores de la clase de plantilla. –

+2

No promociono esto como LA solución. Su velocidad no se puede comparar con la serialización binaria, por lo que solo es adecuada para configuraciones pequeñas. –

Cuestiones relacionadas