serialize serializar serialización serializacion que informatica electronico ejemplo deserialize deserializar deserializacion and c++ design-patterns serialization

c++ - serializar - serialización en netbeans



Cómo implementar la serialización en C++ (6)

Cada vez que encuentro que necesito serializar objetos en un programa C ++, recurro a este tipo de patrón:

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 ++?


La respuesta de Yacoby puede ampliarse más.

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

Durante años hemos estado usando 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). Ver la fuente en http://www.linderdaum.com

La fábrica de clases abstrae el proceso de instanciación 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; };

Cuando define el ConcreteClass derivado de iObject, utiliza alguna extensión y la herramienta del generador de código para producir la lista de procedimientos de guardar / cargar y el código de registro para.

Veamos el código para esta muestra.

En algún lugar del marco tenemos una definición formal vacía

#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 generado automáticamente 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 tiene la secuencia de comandos jerárquica similar a JSON

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

El objeto Linker resolvería todas las clases y propiedades en los métodos Guardar / Cargar.

Lo siento por la publicación larga, la implementación crece aún más cuando llega todo el manejo de errores.


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 múltiples idiomas

Los 2 son útiles y tienen su uso.

Boost.Serialization suele ser la biblioteca más recomendada para la serialización, aunque la extraña elección del operator& que se serializa o deserializa dependiendo de la constidad es realmente un abuso de la sobrecarga del operador para mí.

Para los mensajes, prefiero sugerir el Buffer de Protocolo de Google . 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.

Avanzando

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

  • Necesita control de versiones , incluso para la serialización regular, de todos modos, probablemente necesite compatibilidad con versiones anteriores.
  • Puede o no necesitar un sistema de tag + factory . Solo es necesario para la clase polimórfica. Y necesitarás una factory por árbol de herencia ( kind ), entonces ... ¡el código puede templarse por supuesto!
  • Los punteros / referencias van a morderte el culo ... hacen referencia a una posición en la memoria que cambia después de la deserialización. Por lo general, elijo un enfoque tangente: a cada objeto de cada kind se le asigna una id , única para su kind , así que serializo la id lugar de un puntero. Algún marco lo maneja siempre que no tenga dependencia circular y serialice los objetos apuntados / referenciados primero.

Personalmente, intenté todo lo que 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 el control de versiones

Por lo general, intento mantener la serialización y la deserialización de una versión muy juntas. 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 DRY debe ser adherido :)

En el manejo de errores

Para facilitar la detección de errores, suelo usar 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 quieres una deserialización permisiva, es decir, deserializar el resto de la transmisión incluso si algo falla antes, tendrás que moverte hacia la cuenta de bytes: cada objeto está precedido por su conteo de bytes y solo puede comer tanto byte (y se espera) para comerlos 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 verificar que esté deserializando realmente el tipo correcto de objeto. También permite bonitos mensajes de error.

Aquí hay algunos mensajes de error / excepciones que puede 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 recuerdo, tanto Boost.Serialization como protobuf realmente ayudan en el manejo de errores / versiones.

protobuf tiene algunos beneficios, debido a su capacidad de anidar mensajes:

  • el conteo de bytes es naturalmente compatible, así como el control de versiones
  • puedes hacer una deserialización perezosa (es decir, almacenar el mensaje y deserializar solo 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.


La serialización, desafortunadamente, nunca va a ser completamente indolora en C ++, al menos no en el futuro previsible, simplemente porque C ++ carece de la función de lenguaje crítico que hace posible la fácil serialización en otros idiomas: la 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.


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, Un tamaño no sirve para todos .


Tal vez no soy inteligente, pero creo que finalmente 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 esos tres mecanismos (y tal vez también hay otras posibilidades) es algo que debería evaluarse por proyecto.


Usar algo como Boost Serialization , aunque de ningún modo es un estándar, es (en su mayor parte) una biblioteca muy bien escrita que funciona a su medida.

La última vez que tuve que analizar manualmente una estructura de registro predefinida con un árbol de herencia claro, terminé usando el patrón de fábrica con clases registrables (es decir, utilizando un mapa de clave para una función de creador (plantilla) en lugar de muchas funciones de conmutación) para tratar de evitar el problema que estabas teniendo.

EDITAR
Una implementación básica de C ++ de una fábrica de objetos mencionada 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)(); } };