web-dev-qa-db-ja.com

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方法は何ですか?

28
Paul

Boost Serialization のようなものを使用することは、決して標準ではありませんが、(ほとんどの場合)非常によく書かれたライブラリであり、うまい仕事をします。

最後に、明確な継承ツリーを使用して事前定義されたレコード構造を手動で解析する必要があったときに、登録可能なクラスで factory pattern を使用することになりました(つまり、(テンプレート)作成者関数へのキーのマップを使用する)多くのスイッチ機能よりも)あなたが持っていた問題を試して回避するために。

[〜#〜]編集[〜#〜]
上記の段落で述べたオブジェクトファクトリの基本的な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)();
    }
};
27
Yacoby

シリアライゼーションはC++の扱いにくいトピックです...

簡単な質問:

  • シリアル化:存続期間の短い構造、1つのエンコーダー/デコーダー
  • メッセージング:長寿命、複数言語のエンコーダー/デコーダー

2つは便利で、用途があります。

Boost.Serialization は通常、シリアライゼーションに最も推奨されるライブラリですが、const-nessに応じてシリアライズまたはデシリアライズする_operator&_の奇妙な選択は、実際にはオペレーターのオーバーロードの悪用です。

メッセージングについては、むしろ Google Protocol Buffer をお勧めします。それらはメッセージを記述するための明確な構文を提供し、多種多様な言語のエンコーダーとデコーダーを生成します。パフォーマンスが重要な場合には、もう1つの利点があります。これにより、設計により遅延直列化解除(つまり、一度にblobの一部のみ)が可能になります。

次に進む

さて、実装の詳細については、それは本当にあなたが望むものに依存します。

  • versioningが必要です。通常のシリアル化であっても、とにかく以前のバージョンとの下位互換性が必要になるでしょう。
  • tag + factoryのシステムが必要な場合と必要でない場合があります。ポリモーフィッククラスにのみ必要です。そして、継承ツリーごとに1つのfactoryが必要になります(kind)...コードはもちろんテンプレート化できます!
  • ポインタ/参照はお尻を噛みます...逆シリアル化後に変更されるメモリ内の位置を参照します。私は通常、接線アプローチを選択します。各kindの各オブジェクトにはidが与えられ、そのkindに対して一意であるため、ではなくidをシリアル化しますポインタ。一部のフレームワークは、循環依存関係がなく、最初に参照/参照されるオブジェクトをシリアル化しない限り、それを処理します。

個人的には、シリアライゼーション/デシリアライゼーションのコードを、クラスを実行する実際のコードからできる限り分離するように努めました。特に、コードのこの部分を変更してもバイナリ互換性が失われないように、ソースファイルで分離するようにしています。

バージョニングについて

私は通常、1つのバージョンのシリアライゼーションとデシリアライゼーションを近づけるようにしています。それらが本当に対称的であることを確認する方が簡単です。 DRYを遵守する必要があるため、私はシリアル化フレームワークで直接バージョン管理の処理を抽象化しようとします+)

エラー処理時

エラー検出を容易にするために、私は通常、一対の「マーカー」(特別なバイト)を使用して、あるオブジェクトを別のオブジェクトから分離します。ストリームの非同期化の問題を検出できるので、逆シリアル化中にすぐにスローできます(つまり、バイトが多すぎる、または十分に食べられなかった)。

許容的な逆シリアル化が必要な場合、つまり、何かが以前に失敗した場合でも残りのストリームを逆シリアル化する場合は、バイト数に移動する必要があります。各オブジェクトの前にはバイト数があり、多くのバイトしか消費できませんそれらすべてを食べるために)。このアプローチは、部分的な逆シリアル化を可能にするため、ニースです。つまり、オブジェクトに必要なストリームの一部を保存し、必要な場合にのみ逆シリアル化できます。

タグ付け(クラスID)は、ディスパッチ(だけ)ではなく、実際に正しいタイプのオブジェクトを逆シリアル化していることを確認するためだけに役立ちます。それはまたかなりのエラーメッセージを可能にします。

あなたが望むかもしれないいくつかのエラーメッセージ/例外はここにあります:

  • _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にもメッセージをネストできるため、いくつかの特典があります。

  • バイト数はもちろん、バージョン管理もサポートされています
  • 遅延デシリアライズを実行できます(つまり、メッセージを保存し、誰かが要求した場合にのみデシリアライズします)

対応するのは、メッセージの形式が固定されているため、ポリモーフィズムの処理が難しいことです。そのために注意深く設計する必要があります。

19
Matthieu M.

Yacobyの答えはさらに拡張できます。

人が実際にリフレクションシステムを実装する場合、シリアル化はマネージ言語と同様の方法で実装できると思います。

私たちは何年もの間、自動化されたアプローチを使用してきました。

私は、動作するC++ポストプロセッサーとリフレクションライブラリの実装者の1人でした: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
    }
}

リンカーオブジェクトは、Save/Loadメソッドのすべてのクラスとプロパティを解決します。

長い投稿で申し訳ありませんが、すべてのエラー処理が発生すると、実装はさらに大きくなります。

6
Viktor Latypov

残念ながら、C++でシリアライゼーションが完全に無痛になることは決してありません。少なくとも、予見可能な将来については、C++には他の言語で簡単にシリアライゼーションを可能にする重要な言語機能がないためです。reflection。つまり、クラスFooを作成する場合、C++には、実行時にクラスをプログラムで検査して、含まれるメンバー変数を判別するメカニズムがありません。

したがって、一般化されたシリアル化関数を作成する方法はありません。いずれにしても、クラスごとに特別なシリアル化関数を実装する必要があります。 Boost.Serializationも例外ではありません。これは、便利なフレームワークと、これを行うのに役立つ素晴らしいツールセットを提供するだけです。

5
Charles Salvia

たぶん私は利口ではないかもしれませんが、C++には何かを実行するためのランタイムメカニズムがないので、結局、あなたが書いたのと同じ種類のコードが書かれると思います。問題は、それが開発者によってオーダーメイドで記述されるのか、テンプレートメタプログラミング(boost.serializationがそうだと思う)によって生成されるのか、IDLコンパイラー/コードジェネレーターなどの外部ツールによって生成されるのかです。

これらの3つのメカニズムのうちのどれが(そしておそらく他の可能性もある)問題は、プロジェクトごとに評価されるべきものです。

5
Chris Cleeland

標準的な方法に最も近いものは Boost.Serialization になると思います。私は、クラスIDについてそのことをどのような状況で聞いたのか知​​りたいと思います。シリアライゼーションの場合、私は本当に他に方法を考えることはできません(もちろん、デシリアライゼーション時に期待するタイプを知っている場合を除きます)。また、 1つのサイズですべてに対応できるわけではありません

2
Björn Pollex