如何在C ++中实现序列化

每当我发现自己需要在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; }; 

上述工作在实践中相当好。 不过,我听说这种类ID的切换是邪恶的,也是反模式的。 在C ++中处理序列化的OO方式是什么标准?

使用诸如Boost Serialization之类的东西,绝不是一个标准,它是一个(大部分)编写得非常好的库,它为你做了很多努力工作。

最后一次我不得不用一个清晰​​的inheritance树来手动parsing一个预定义的logging结构时,我最终使用了可注册类的工厂模式 (即使用(模板)创build者函数的键映射而不是很多开关函数)试图避免你遇到的问题。

编辑
上面提到的对象工厂的一个基本的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)(); } }; 

序列化是C ++中的一个敏感话题

快速提问:

  • 序列化:短暂的结构,一个编码器/解码器
  • 信息:更长的使用寿命,多种语言的编码器/解码器

2是有用的,并有其用途。

Boost.Serialization通常是序列化最值得推荐的库,尽pipeoperator&的奇怪selectoperator&序列化或反序列化取决于常量,实际上是对操作符重载的滥用。

对于消息传递,我宁愿build议Google协议缓冲区 。 它们为描述消息提供了一个干净的语法,并为各种各样的语言生成编码器和解码器。 当性能很重要时,还有另外一个优势:它允许通过devise懒惰的反序列化(即,只有一部分是一次性的)。

继续

现在,关于实施的细节,真的取决于你的意愿。

  • 你需要版本控制 ,即使对于常规的序列化,你也可能需要向前兼容以前的版本。
  • 你可以,也可以不需要tag + factory系统。 这只是多态类的必要。 而且你将需要每个inheritance树( kind )一个factory ,然后…代码当然可以模板化!
  • 指针/引用将咬你屁股…他们引用一个内存中的位置,反序列化后发生了变化。 我通常select一种相切的方法:每种kind每个对象都有一个唯一的id ,所以我序列化id而不是指针。 只要没有循环依赖关系,并且首先序列化指向/引用的对象,某个框架就可以处理它。

就我个人而言,我尽可能地将运行该类的实际代码与序列化/反序列化的代码分开。 特别是,我尝试在源文件中将其隔离,以便在这部分代码上的更改不会消除二进制兼容性。

在版本上

我通常会尽量保持一个版本的序列化和反序列化。 检查它们是否真正对称更容易。 我也尝试直接在我的序列化框架+一些其他的东西,抽象的版本控制处理,因为DRY应该坚持:)

在error handling

为了简化错误检测,我通常使用一对“标记”(特殊字节)将一个对象与另一个对象分开。 它允许我在反序列化过程中立即抛出,因为我可以检测到stream的失步问题(即,有点太多的字节或没有足够的吃了)。

如果你想要宽松的反序列化,也就是说,即使事情失败了,也要反序列化其余的stream,你必须转向字节计数:每个对象前面都有字节数,只能吃这么多字节全部吃掉)。 这种方法很好,因为它允许部分反序列化:也就是说,您可以保存对象所需的stream的部分,只有在需要时才反序列化它。

标记(你的类ID)在这里很有用,不是(仅)用于调度,而仅仅是检查你实际上正在反序列化正确types的对象。 它也允许漂亮的错误信息。

以下是您可能希望的一些错误消息/例外:

  • 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.Serializationprotobuf真的帮助错误/版本处理。

protobuf也有一些优点,因为它的嵌套消息的能力:

  • 字节数自然是支持的,以及版本
  • 你可以做懒惰的反序列化(即,存储消息,只有反序列化,如果有人要求)

对应的是由于消息的固定格式,处理多态性更困难。 你必须仔细devise它们。

Yacoby的答案可以进一步延伸。

我相信如果实际上实现了一个reflection系统,序列化可以用类似于托pipe语言的方式来实现。

多年来,我们一直在使用自动化方法。

我是工作的C ++后处理器和reflection库的实现者之一:LSDC工具和Linderdaum引擎核心(iObject + RTTI +链接器/加载器)。 在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; }; 

在定义从iObject派生的ConcreteClass时,使用一些扩展和代码生成器工具来生成保存/加载过程的列表以及注册码。

让我们看看这个例子的代码。

在框架的某个地方,我们有一个空的正式定义

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

链接器对象将parsing保存/加载方法中的所有类和属性。

很抱歉,很长的文章,当所有的error handling进来的时候,实现会变得更大。

也许我不聪明,但是我认为最终会写出与你编写的相同types的代码,因为C ++没有运行时机制来做任何不同的事情。 问题是,它是否会被开发人员定制,通过模板元编程生成(这是我猜测boost.serialization所做的),或者通过一些外部工具(如IDL编译器/代码生成器)生成的。

这三个机制中的哪一个(也许还有其他的可能性)是应该在每个项目基础上进行评估的问题。

不幸的是,在C ++中,序列化不会是完全无痛的,至less在可预见的将来是如此,因为C ++缺乏使得在其他语言中简单序列化的关键语言特性: reflection 。 也就是说,如果你创build一个Foo类,C ++没有机制在运行时以编程方式检查类来确定它包含的成员variables。

因此,没有办法创build广义的序列化函数。 无论如何,你必须为每个类实现一个特殊的序列化函数。 Boost.Serialization没有什么不同,它只是为您提供一个方便的框架和一套很好的工具来帮助您做到这一点。

我猜最接近标准的方法是Boost.Serialization 。 我希望听到您在什么情况下听到了有关class级ID的事情。 在序列化的情况下,我可以真的没有别的办法(除非你知道反序列化时期望的types)。 而且, 一个尺寸不适合所有 。