2009-11-27 37 views
26

Bất cứ khi nào tôi thấy mình cần phải serialize các đối tượng trong một chương trình C++, tôi rơi trở lại để loại mẫu:Làm thế nào để thực hiện tuần tự trong 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; 
}; 

Các công trình trên khá tốt trong thực tế. Tuy nhiên, tôi đã nghe nói rằng loại chuyển đổi này trên các ID lớp là ác và một mẫu giả; tiêu chuẩn, OO-cách xử lý serialization trong C + + là gì?

+0

@SergeyK: những thay đổi gần đây? Tôi chắc chắn không nghe nói về bất kỳ. –

+0

Ý tôi là câu trả lời này: http://stackoverflow.com/a/10332336/1065190 –

+0

@SergeyK .: Ah! Tôi nghĩ rằng phần bình luận trên chính câu trả lời có lẽ là nơi tốt nhất để thảo luận về nó. Trong thực tế, tôi đã bắt đầu. Nó có vẻ khá bí truyền đối với tôi, đặc biệt là ý tưởng sáp nhập serialization với getters tự động và setters (nó thường là xấu để kết hợp các khái niệm khác nhau). Cũng nhắc tôi về một dự án QT nào đó ... cuối cùng, bạn có bán C++ và bạn mất tính di động vì bạn phụ thuộc vào khả năng sẵn có của công cụ được cho là biến nó thành đúng C++. Tôi không giữ được hơi thở. –

Trả lời

26

Sử dụng một cái gì đó như Boost Serialization, trong khi không có nghĩa là một tiêu chuẩn, là thư viện được viết rất tốt (đối với hầu hết các phần) mà grunt làm việc cho bạn. Lần cuối cùng tôi phải phân tích cú pháp cấu trúc bản ghi được xác định trước bằng cây kế thừa rõ ràng, tôi đã sử dụng factory pattern với các lớp có thể đăng ký (ví dụ: Sử dụng bản đồ khóa cho hàm tạo mẫu) thay vì nhiều chức năng chuyển đổi) để thử và tránh sự cố bạn gặp phải.

EDIT
Thực hiện C++ cơ bản của nhà máy đối tượng được đề cập trong đoạn trên.

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

Tôi đoán điều gần nhất theo cách tiêu chuẩn sẽ là Boost.Serialization. Tôi muốn nghe và trong bối cảnh bạn nghe điều đó về ID lớp học. Trong trường hợp serialization tôi thực sự có thể nghĩ không có cách nào khác (trừ khi tất nhiên, bạn biết loại bạn mong đợi khi deserializing). Và cũng có thể, One size does not fit all.

5

Có lẽ tôi không thông minh, nhưng tôi nghĩ rằng cuối cùng cùng một loại mã mà bạn đã viết được viết, đơn giản vì C++ không có cơ chế thời gian chạy để làm bất cứ điều gì khác. Câu hỏi đặt ra là liệu nó sẽ được viết bởi một nhà phát triển, được tạo ra thông qua lập trình meta mẫu (đó là điều tôi nghi ngờ boost.serialization), hoặc được tạo ra thông qua một số công cụ bên ngoài như trình biên dịch IDL/trình tạo mã.

Câu hỏi về ba cơ chế đó (và có thể có những khả năng khác) là điều cần được đánh giá trên cơ sở từng dự án.

+0

Chính xác những gì tôi muốn nói! – kizzx2

5

serialization, rất tiếc, chưa bao giờ sẽ hoàn toàn không đau trong C++, ít nhất là không trong tương lai gần, đơn giản chỉ vì C++ thiếu tính năng ngôn ngữ quan trọng mà làm cho dễ dàng serialization thể bằng các ngôn ngữ khác: reflection. Tức là, nếu bạn tạo một lớp Foo, C++ không có cơ chế để kiểm tra lớp theo chương trình tại thời gian chạy để xác định các biến thành viên nào chứa nó.

Vì vậy, không có cách nào để tạo các hàm tuần tự hóa tổng quát. Bằng cách này hay cách khác, bạn phải thực hiện một chức năng tuần tự đặc biệt cho mỗi lớp. Boost.Serialization không khác gì, nó đơn giản cung cấp cho bạn một khung công tác thuận tiện và một bộ công cụ tuyệt vời giúp bạn thực hiện điều này.

+0

[C++ Middleware Writer] (http://webebenezer.net) tự động viết các hàm tuần tự hóa. –

+0

Trên thực tế, C++ có (một số) phản ánh thời gian biên dịch có sẵn thông qua các thư viện metatemplate. Nó có thể được tận dụng với một sự lạm dụng của chỉ thị tiền xử lý và Boost.Fusion. Tôi không muốn đi xuống con đường đó mặc dù: x –

18

Tuần tự hóa là một chủ đề nhạy cảm trong C++ ...

câu hỏi nhanh:

  • serialization: ngắn ngủi cấu trúc, một bộ mã hóa/giải mã
  • Nhắn tin: còn sống, các bộ mã hóa/giải mã bằng nhiều ngôn ngữ

2 có ích và sử dụng chúng.

Boost.Serialization là thư viện được đề xuất thường xuyên nhất để đăng ký tuần tự, mặc dù lựa chọn lẻ operator&, tuần tự hóa hoặc deserializes tùy thuộc vào const-ness thực sự là một lạm dụng của nhà điều hành quá tải cho tôi.

Để nhắn tin, tôi muốn đề xuất Google Protocol Buffer. Chúng cung cấp cú pháp rõ ràng để mô tả thông điệp và tạo bộ giải mã và bộ giải mã cho nhiều ngôn ngữ khác nhau. Ngoài ra còn có một lợi thế khác khi các vấn đề hiệu suất: nó cho phép deserialization lười biếng (tức là chỉ là một phần của blob cùng một lúc) bởi thiết kế.

Chuyển

Bây giờ, đối với các chi tiết thực hiện, nó thực sự phụ thuộc vào những gì bạn muốn.

  • Bạn cần versioning, ngay cả đối với serialization thông thường, có thể bạn sẽ cần tương thích ngược với phiên bản trước đó.
  • Bạn có thể hoặc không cần hệ thống tag + factory. Nó chỉ cần thiết cho lớp đa hình. Và bạn sẽ cần một factory cho mỗi cây thừa kế (kind) sau đó ... mã có thể được templatized tất nhiên!
  • Con trỏ/Tham chiếu sẽ cắn bạn trong ass ... chúng tham chiếu đến vị trí trong bộ nhớ thay đổi sau khi deserialization. Tôi thường chọn cách tiếp cận tiếp tuyến: mỗi đối tượng của mỗi kind được cấp một id, duy nhất cho số kind và vì vậy, tôi tuần tự hóa số id thay vì một con trỏ. Một số khung công tác xử lý nó miễn là bạn không có phụ thuộc vòng tròn và tuần tự hóa các đối tượng được trỏ đến/tham chiếu đầu tiên.

Cá nhân, tôi đã cố gắng hết sức để tách mã tuần tự hóa/deserialization khỏi mã thực tế chạy lớp. Đặc biệt, tôi cố gắng cách ly nó trong các tệp nguồn để các thay đổi trên phần mã này không hủy bỏ khả năng tương thích nhị phân.

On versioning

Tôi thường cố gắng giữ serialization và deserialization của một phiên bản gần nhau. Nó dễ dàng hơn để kiểm tra xem chúng có thực sự đối xứng hay không. Tôi cũng cố gắng để tóm tắt các phiên bản xử lý trực tiếp trong khuôn khổ tuần tự của tôi + một vài điều khác, bởi vì khô nên được tôn trọng :)

On lỗi xử lý

Để giảm bớt lỗi phát hiện, tôi thường sử dụng một cặp 'dấu' (các byte đặc biệt) để tách một đối tượng khỏi một đối tượng khác.Nó cho phép tôi ngay lập tức ném trong quá trình deserialization vì tôi có thể phát hiện một vấn đề desynchronization của dòng (tức là, hơi ăn quá nhiều byte hoặc không ăn đủ).

Nếu bạn muốn deserialization, tức là deserializing phần còn lại của dòng ngay cả khi một cái gì đó không thành công trước đó, bạn sẽ phải di chuyển về phía byte-count: mỗi đối tượng được trước bởi số byte của nó và chỉ có thể ăn quá nhiều byte (và dự kiến ​​sẽ ăn tất cả). Cách tiếp cận này là tốt đẹp bởi vì nó cho phép một phần deserialization: tức là bạn có thể lưu một phần của dòng cần thiết cho một đối tượng và chỉ deserialize nó nếu cần thiết.

Gắn thẻ (ID lớp học của bạn) hữu ích ở đây, không phải (chỉ) để gửi đi, mà chỉ cần kiểm tra xem bạn có thực sự deserializing đúng loại đối tượng không. Nó cũng cho phép thông báo lỗi khá.

Dưới đây là một số thông báo lỗi/trường hợp ngoại lệ bạn có thể muốn:

  • 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

Lưu ý rằng như xa như tôi nhớ cả Boost.Serializationprotobuf thực sự giúp đỡ để xử lý lỗi/phiên bản.

protobuf có một số đặc quyền quá, bởi vì khả năng của mình thông điệp làm tổ:

  • byte-count được hỗ trợ một cách tự nhiên, cũng như các phiên bản
  • bạn có thể làm deserialization lười biếng (ví dụ, lưu trữ tin nhắn và chỉ deserialize nếu ai đó yêu cầu nó)

Đối tác là khó xử lý đa hình do định dạng cố định của tin nhắn. Bạn phải cẩn thận thiết kế chúng cho điều đó.

6

Câu trả lời của Yacoby có thể được mở rộng thêm.

Tôi tin rằng việc tuần tự hóa có thể được triển khai theo cách tương tự như ngôn ngữ được quản lý nếu thực sự thực hiện một hệ thống phản chiếu.

Trong nhiều năm, chúng tôi đã sử dụng phương pháp tự động.

Tôi là một trong những người triển khai bộ xử lý C++ hoạt động và thư viện Reflection: công cụ LSDC và Linderdaum Engine Core (iObject + RTTI + Linker/Loader). Xem nguồn tại số http://www.linderdaum.com

Nhà máy lớp tóm tắt quá trình khởi tạo lớp.

Để khởi tạo các thành viên cụ thể, bạn có thể thêm một số RTTI xâm nhập và tự động tạo các quy trình tải/lưu cho chúng.

Giả sử bạn có lớp iObject ở đầu hệ thống phân cấp của mình.

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

Khi bạn xác định ConcreteClass bắt nguồn từ iObject, bạn sử dụng một số tiện ích mở rộng và công cụ tạo mã để tạo danh sách quy trình lưu/tải và mã đăng ký.

Hãy để chúng tôi xem mã cho mẫu này.

Một nơi nào đó trong khuôn khổ chúng ta có một sản phẩm nào chính thức xác định

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

Mã đăng ký autogenerated sẽ là:

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

Bây giờ, nếu bạn có kịch bản thứ bậc JSON như

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

Đối tượng Trình liên kết sẽ giải quyết tất cả các lớp và thuộc tính trong Lưu/Tải phương pháp.

Xin lỗi vì bài đăng dài, triển khai sẽ phát triển lớn hơn nữa khi tất cả xử lý lỗi đến.

+5

Tôi đã nhìn thấy uglier ... nhưng không thường xuyên. Tôi thực sự không thích có một extra-pass trong quá trình biên dịch sửa đổi mã * my *. Tôi không nhớ có * thêm * mã thêm (như 'protobuf' tập tin), nhưng khi vượt qua thêm trashes các tập tin và bạn kết thúc với một biên dịch sai, theo dõi các lỗi là một cơn ác mộng. –

+2

Vâng, chúng tôi đang làm việc xung quanh việc thiếu các công cụ trong ngôn ngữ chính nó - điều này không thể được thực hiện liền mạch. Không có mã nguồn trashing - metainformation được tạo ra cũng được thêm vào các tệp nguồn mới. Nếu bạn không bao gồm chúng trong dự án của bạn, không có trashing (và không có nhà máy/serialization mặc dù). Các lỗi biên dịch có vẻ phức tạp (một khi bạn bỏ sót điều gì đó trong khai báo THUỘC TÍNH), nhưng có thể làm quen với chúng như tất cả chúng ta làm với các lỗi của lớp mẫu. –

+2

Tôi không quảng cáo điều này như là giải pháp. Tốc độ của nó không thể so sánh với serialization nhị phân, do đó, nó chỉ thích hợp cho các cấu hình nhỏ. –

Các vấn đề liên quan