59

オブジェクトを JSON にシリアライズおよびデシリアライズする方法を、できる限り自動化する方法が必要です。

シリアル化: 私にとって理想的な方法は、インスタンス JSONSerialize() を呼び出すと、オブジェクトのすべてのパブリック プロパティを として持つ JSON オブジェクトを含む文字列を返すこと"name_of_property": "value"です。プリミティブな値の場合は簡単です。オブジェクトの場合は、各 JSONSerialize() または ToString() などで呼び出して、すべてのパブリック プロパティを再帰的にシリアル化する必要があります。コレクションの場合も正しく動作する必要があります (ベクトル/配列だけで問題ありません)。

Deserialize : 指定されたオブジェクト (犬としましょう) のインスタンスをJSONDeserialize(json_string)作成して を呼び出すだけで、すべてのパブリック プロパティが満たされ、プロパティがプリミティブでない場合に必要なオブジェクト、または必要なコレクションが作成されます。

例は次のように実行する必要があります。

Dog *d1 = new Dog();
d1->name = "myDog";

string serialized = d1->JSONSerialize();

Dog *d2 = new Dog();
d2->JSONDeserialize(serialized);
std::cout << d2->name; // This will print "myDog"

またはそのように:

Dog *d1 = new Dog();
d1->name = "myDog";

string serialized = JSONSerializer.Serialize(d1);

Dog *d2 = JSONSerializer.Deserialize(serialized, Dog);
std::cout << d2->name; // This will print "myDog"

どうすればこれを簡単にやってのけることができますか?

4

10 に答える 10

100

C++ にはリフレクションはありません。真実。ただし、コンパイラが必要なメタデータを提供できない場合は、自分で提供できます。

プロパティ構造体を作成することから始めましょう。

template<typename Class, typename T>
struct PropertyImpl {
    constexpr PropertyImpl(T Class::*aMember, const char* aName) : member{aMember}, name{aName} {}

    using Type = T;

    T Class::*member;
    const char* name;
};

template<typename Class, typename T>
constexpr auto property(T Class::*member, const char* name) {
    return PropertyImpl<Class, T>{member, name};
}

もちろん、propertyメンバーへのポインターの代わりにセッターとゲッターを受け取る を使用することもできます。シリアライズしたい計算値のプロパティのみを読み取ることもできます。C++17 を使用する場合は、さらに拡張して、ラムダで動作するプロパティを作成できます。

これで、コンパイル時のイントロスペクション システムのビルディング ブロックができました。

クラスDogに、メタデータを追加します。

struct Dog {
    std::string barkType;
    std::string color;
    int weight = 0;
    
    bool operator==(const Dog& rhs) const {
        return std::tie(barkType, color, weight) == std::tie(rhs.barkType, rhs.color, rhs.weight);
    }
    
    constexpr static auto properties = std::make_tuple(
        property(&Dog::barkType, "barkType"),
        property(&Dog::color, "color"),
        property(&Dog::weight, "weight")
    );
};

そのリストを繰り返す必要があります。タプルを反復するには、多くの方法がありますが、私の好みの方法は次のとおりです。

template <typename T, T... S, typename F>
constexpr void for_sequence(std::integer_sequence<T, S...>, F&& f) {
    using unpack_t = int[];
    (void)unpack_t{(static_cast<void>(f(std::integral_constant<T, S>{})), 0)..., 0};
}

コンパイラで C++17 の折り畳み式が使用できる場合は、次のfor_sequenceように簡略化できます。

template <typename T, T... S, typename F>
constexpr void for_sequence(std::integer_sequence<T, S...>, F&& f) {
    (static_cast<void>(f(std::integral_constant<T, S>{})), ...);
}

これにより、整数シーケンスの各定数に対して関数が呼び出されます。

この方法が機能しない場合、またはコンパイラに問題が発生する場合は、いつでも配列拡張トリックを使用できます。

目的のメタデータとツールが用意できたので、プロパティを反復処理してシリアル化を解除できます。

// unserialize function
template<typename T>
T fromJson(const Json::Value& data) {
    T object;

    // We first get the number of properties
    constexpr auto nbProperties = std::tuple_size<decltype(T::properties)>::value;
    
    // We iterate on the index sequence of size `nbProperties`
    for_sequence(std::make_index_sequence<nbProperties>{}, [&](auto i) {
        // get the property
        constexpr auto property = std::get<i>(T::properties);

        // get the type of the property
        using Type = typename decltype(property)::Type;

        // set the value to the member
        // you can also replace `asAny` by `fromJson` to recursively serialize
        object.*(property.member) = Json::asAny<Type>(data[property.name]);
    });

    return object;
}

そしてシリアライズのために:

template<typename T>
Json::Value toJson(const T& object) {
    Json::Value data;

    // We first get the number of properties
    constexpr auto nbProperties = std::tuple_size<decltype(T::properties)>::value;
    
    // We iterate on the index sequence of size `nbProperties`
    for_sequence(std::make_index_sequence<nbProperties>{}, [&](auto i) {
        // get the property
        constexpr auto property = std::get<i>(T::properties);

        // set the value to the member
        data[property.name] = object.*(property.member);
    });

    return data;
}

再帰的なシリアライゼーションとアンシリアライゼーションが必要な場合はasAnyfromJson.

これで、次のように関数を使用できます。

Dog dog;

dog.color = "green";
dog.barkType = "whaf";
dog.weight = 30;

Json::Value jsonDog = toJson(dog); // produces {"color":"green", "barkType":"whaf", "weight": 30}
auto dog2 = fromJson<Dog>(jsonDog);

std::cout << std::boolalpha << (dog == dog2) << std::endl; // pass the test, both dog are equal!

終わり!実行時のリフレクションは必要ありません。C++14 の利点だけです。

このコードは、いくつかの改善の恩恵を受ける可能性があり、もちろん、いくつかの調整により C++11 で動作する可能性があります。

関数を記述する必要があることに注意してくださいasAny。これは、 を取り、適切な関数または別のJson::Valueを呼び出す単なる関数です。as...fromJson

これは、この回答のさまざまなコードスニペットから作成された完全で実用的な例です。お気軽にご利用ください。

コメントで述べたように、このコードは msvc では機能しません。互換性のあるコードが必要な場合は、この質問を参照してください: Pointer to member: works in GCC but not in VS2015

于 2015-12-08T20:26:32.287 に答える
14

そのためには、存在しない C/C++ でのリフレクションが必要です。クラス (メンバー、継承された基本クラス) の構造を説明するメタ データが必要です。現時点では、C/C++ コンパイラは、ビルドされたバイナリでその情報を自動的に提供しません。

私は同じ考えを念頭に置いていたので、GCC XMLプロジェクトを使用してこの情報を取得しました。クラス構造を記述した XML データを出力します。私はプロジェクトを構築し、このページでいくつかの重要なポイントを説明しています:

シリアル化は簡単ですが、割り当てられたバッファーを操作する複雑なデータ構造の実装 (std::string、std::map など) を処理する必要があります。逆シリアル化はより複雑で、すべてのメンバーと vtables への参照を使用してオブジェクトを再構築する必要があります...面倒な実装です。

たとえば、次のようにシリアル化できます。

    // Random class initialization
    com::class1* aObject = new com::class1();

    for (int i=0; i<10; i++){
            aObject->setData(i,i);
    }      

    aObject->pdata = new char[7];
    for (int i=0; i<7; i++){
            aObject->pdata[i] = 7-i;
    }
    // dictionary initialization
    cjson::dictionary aDict("./data/dictionary.xml");

    // json transformation
    std::string aJson = aDict.toJson<com::class1>(aObject);

    // print encoded class
    cout << aJson << std::endl ;

データを逆シリアル化するには、次のように機能します。

    // decode the object
    com::class1* aDecodedObject = aDict.fromJson<com::class1>(aJson);

    // modify data
    aDecodedObject->setData(4,22);

    // json transformation
    aJson = aDict.toJson<com::class1>(aDecodedObject);
   
    // print encoded class
    cout << aJson << std::endl ;

出力:

>:~/cjson$ ./main
{"_index":54,"_inner":  {"_ident":"test","pi":3.141593},"_name":"first","com::class0::_type":"type","com::class0::data":[0,1,2,3,4,5,6,7,8,9],"com::classb::_ref":"ref","com::classm1::_type":"typem1","com::classm1::pdata":[7,6,5,4,3,2,1]}
{"_index":54,"_inner":{"_ident":"test","pi":3.141593},"_name":"first","com::class0::_type":"type","com::class0::data":[0,1,2,3,22,5,6,7,8,9],"com::classb::_ref":"ref","com::classm1::_type":"typem1","com::classm1::pdata":[7,6,5,4,3,2,1]}
>:~/cjson$ 

通常、これらの実装はコンパイラに依存し (ABI 仕様など)、外部記述 (GCCXML 出力) が機能する必要があるため、プロジェクトに統合するのは簡単ではありません。

于 2013-12-15T19:01:05.297 に答える
4

誰かがまだこの必要性を持っている場合に備えて (私は持っています)、この問題に対処するために自分でライブラリを作成しました。ここ を参照してください。クラス内のすべてのフィールドを記述しなければならないという点で完全に自動化されているわけではありませんが、C++ にはリフレクションがないため、取得できるものに限りなく近いものです。

于 2014-10-04T09:21:51.807 に答える
1

json_dtoを試してください。ヘッダーのみで使いやすいです。

簡単な例:

struct message_t
{
  std::string m_from;
  std::string m_text;

  // Entry point for json_dto.
  template < typename JSON_IO >
  void
  json_io( JSON_IO & io )
  {
    io
      & json_dto::mandatory( "from", m_from )
      & json_dto::mandatory( "text", m_text );
  }
};

これは JSONとの間で変換可能になります。

{ "from" : "json_dto", "text" : "Hello world!" }
于 2016-10-06T19:38:42.953 に答える
1

まだ言及されていませんが、私の検索結果では最初でした: https://github.com/nlohmann/json

リストされている特典:

  • 直感的な構文 (見栄えが良い!)
  • インクルードするヘッダー ファイルは 1 つだけで、他には何もありません
  • ばかげたテスト

また、MITライセンスの下にあります。

正直に言うと、まだ使用していませんが、いくつかの経験を通じて、本当によくできた C++ ライブラリに出くわしたときに判断するコツがあります。

于 2018-08-23T22:33:53.630 に答える