web-dev-qa-db-ja.com

シリアル化に現在必要な定型文を減らす方法

私たちのソフトウェアはハードウェアを抽象化しており、このハードウェアの状態を表すクラスがあり、その外部ハードウェアのすべてのプロパティのデータメンバーがたくさんあります。その状態に関する他のコンポーネントを定期的に更新する必要があります。そのために、MQTTおよび他のメッセージングプロトコルを介してprotobufでエンコードされたメッセージを送信します。ハードウェアのさまざまな側面を説明するさまざまなメッセージがあるため、それらのクラスのデータのさまざまなビューを送信する必要があります。これがスケッチです:

struct some_data {
  Foo foo;
  Bar bar;
  Baz baz;
  Fbr fbr;
  // ...
};

foobarを含む1つのメッセージと、barbazを含むメッセージを送信する必要があると仮定します。これを行う現在の方法は、多くの定型文です。

struct foobar {
  Foo foo;
  Bar bar;
  foobar(const Foo& foo, const Bar& bar) : foo(foo), bar(bar) {}
  bool operator==(const foobar& rhs) const {return foo == rhs.foo && bar == rhs.bar;}
  bool operator!=(const foobar& rhs) const {return !operator==(*this,rhs);}
};

struct barbaz {
  Bar bar;
  Baz baz;
  foobar(const Bar& bar, const Baz& baz) : bar(bar), baz(baz) {}
  bool operator==(const barbaz& rhs) const {return bar == rhs.bar && baz == rhs.baz;}
  bool operator!=(const barbaz& rhs) const {return !operator==(*this,rhs);}
};

template<> struct serialization_traits<foobar> {
  static SerializedFooBar encode(const foobar& fb) {
    SerializedFooBar sfb;
    sfb.set_foo(fb.foo);
    sfb.set_bar(fb.bar);
    return sfb;
  }
};

template<> struct serialization_traits<barbaz> {
  static SerializedBarBaz encode(const barbaz& bb) {
    SerializedBarBaz sbb;
    sfb.set_bar(bb.bar);
    sfb.set_baz(bb.baz);
    return sbb;
  }
};

次に、これを送信できます。

void send(const some_data& data) {
  send_msg( serialization_traits<foobar>::encode(foobar(data.foo, data.bar)) );
  send_msg( serialization_traits<barbaz>::encode(barbaz(data.foo, data.bar)) );
}

送信されるデータセットは2つのアイテムよりもはるかに大きいことが多く、そのデータもデコードする必要があり、これらのメッセージが大量にあることを考えると、このスケッチにあるものよりもはるかに多くの定型文が関係しています。だから私はこれを減らす方法を探していました。これが最初のアイデアです:

typedef std::Tuple< Foo /* 0 foo */
                  , Bar /* 1 bar */
                  > foobar;
typedef std::Tuple< Bar /* 0 bar */
                  , Baz /* 1 baz */
                  > barbaz;
// yay, we get comparison for free!

template<>
struct serialization_traits<foobar> {
  static SerializedFooBar encode(const foobar& fb) {
    SerializedFooBar sfb;
    sfb.set_foo(std::get<0>(fb));
    sfb.set_bar(std::get<1>(fb));
    return sfb;
  }
};

template<>
struct serialization_traits<barbaz> {
  static SerializedBarBaz encode(const barbaz& bb) {
    SerializedBarBaz sbb;
    sfb.set_bar(std::get<0>(bb));
    sfb.set_baz(std::get<1>(bb));
    return sbb;
  }
};

void send(const some_data& data) {
  send_msg( serialization_traits<foobar>::encode(std::tie(data.foo, data.bar)) );
  send_msg( serialization_traits<barbaz>::encode(std::tie(data.bar, data.baz)) );
}

私はこれを機能させました、そしてそれはボイラープレートをかなりカットします。 (この小さな例ではありませんが、12個のデータポイントがエンコードおよびデコードされていると想像すると、データメンバーの繰り返しリストが消えることで、大きな違いが生じます)。ただし、これには2つの欠点があります。

  1. これは、FooBar、およびBazが別個のタイプであることに依存しています。それらがすべてintの場合、ダミーのタグタイプをタプルに追加する必要があります。

    これは可能ですが、このアイデア全体の魅力が大幅に低下します。

  2. 古いコードの変数名は、新しいコードではコメントと数字になります。これはかなり悪いことであり、2つのメンバーを混乱させるバグがエンコードとデコードに存在する可能性が高いことを考えると、単純な単体テストでは検出できませんが、他のテクノロジで作成されたテストコンポーネントが必要です(したがって統合テスト)そのようなバグをキャッチするため。

    これを修正する方法がわかりません。

誰かが私たちのために定型文を減らす方法についてより良いアイデアを持っていますか?

注:

  • 当分の間、C++ 03で立ち往生しています。はい、あなたはその権利を読んでいます。私たちにとって、それはstd::tr1::Tuple。ラムダはありません。そして、autoもありません。
  • これらのシリアル化特性を採用したコードはたくさんあります。スキーム全体を捨てて、まったく違うことをすることはできません。既存のフレームワークに適合する将来のコードを簡素化するソリューションを探しています。全体を書き直す必要があるアイデアは、却下される可能性が非常に高くなります。
28
sbi

私はあなたの提案したソリューションに基づいて構築しますが、代わりにboost :: fusion :: tuplesを使用します(それが許可されていると仮定します)。データ型が次のようになっていると仮定しましょう

struct Foo{};
struct Bar{};
struct Baz{};
struct Fbr{};

そしてあなたのデータは

struct some_data {
    Foo foo;
    Bar bar;
    Baz baz;
    Fbr fbr;
};

コメントから、SerialisedXYZクラスを制御できないことは理解していますが、特定のインターフェイスがあります。私はこのようなものが十分に近いと仮定します(?):

struct SerializedFooBar {

    void set_foo(const Foo&){
        std::cout << "set_foo in SerializedFooBar" << std::endl;
    }

    void set_bar(const Bar&){
        std::cout << "set_bar in SerializedFooBar" << std::endl;
    }
};

// another protobuf-generated class
struct SerializedBarBaz {

    void set_bar(const Bar&){
        std::cout << "set_bar in SerializedBarBaz" << std::endl;
    }

    void set_baz(const Baz&){
        std::cout << "set_baz in SerializedBarBaz" << std::endl;
    }
};

次のように、ボイラープレートを減らして、データ型順列ごとに1つのtypedefに制限し、SerializedXYZクラスの各set_XXXメンバーに対して1つの単純なオーバーロードに制限することができます。

typedef boost::fusion::Tuple<Foo, Bar> foobar;
typedef boost::fusion::Tuple<Bar, Baz> barbaz;
//...

template <class S>
void serialized_set(S& s, const Foo& v) {
    s.set_foo(v);
}

template <class S>
void serialized_set(S& s, const Bar& v) {
    s.set_bar(v);
}

template <class S>
void serialized_set(S& s, const Baz& v) {
    s.set_baz(v);
}

template <class S, class V>
void serialized_set(S& s, const Fbr& v) {
    s.set_fbr(v);
}
//...

今の良い点は、serialization_traitsを特殊化する必要がなくなったことです。以下では、boost :: fusion :: fold関数を使用しています。これはプロジェクトで使用しても問題ないと思います。

template <class SerializedX>
class serialization_traits {

    struct set_functor {

        template <class V>
        SerializedX& operator()(SerializedX& s, const V& v) const {
            serialized_set(s, v);
            return s;
        }
    };

public:

    template <class Tuple>
    static SerializedX encode(const Tuple& t) {
        SerializedX s;
        boost::fusion::fold(t, s, set_functor());
        return s;
    }
};

そして、これがどのように機能するかの例です。 SerializedXYZインターフェイスに準拠していないsome_dataのデータメンバーを誰かが結び付けようとすると、コンパイラがそのことを通知することに注意してください。

void send_msg(const SerializedFooBar&){
    std::cout << "Sent SerializedFooBar" << std::endl;
}

void send_msg(const SerializedBarBaz&){
    std::cout << "Sent SerializedBarBaz" << std::endl;
}

void send(const some_data& data) {
  send_msg( serialization_traits<SerializedFooBar>::encode(boost::fusion::tie(data.foo, data.bar)) );
  send_msg( serialization_traits<SerializedBarBaz>::encode(boost::fusion::tie(data.bar, data.baz)) );
//  send_msg( serialization_traits<SerializedFooBar>::encode(boost::fusion::tie(data.foo, data.baz)) ); // compiler error; SerializedFooBar has no set_baz member
}

int main() {

    some_data my_data;
    send(my_data);
}

コード ここ

編集:

残念ながら、このソリューションはOPの問題#1に対処していません。これを改善するために、データメンバーごとに1つずつ、一連​​のタグを定義し、同様のアプローチに従うことができます。タグと、変更されたserialized_set関数は次のとおりです。

struct foo_tag{};
struct bar1_tag{};
struct bar2_tag{};
struct baz_tag{};
struct fbr_tag{};

template <class S>
void serialized_set(S& s, const some_data& data, foo_tag) {
    s.set_foo(data.foo);
}

template <class S>
void serialized_set(S& s, const some_data& data, bar1_tag) {
    s.set_bar1(data.bar1);
}

template <class S>
void serialized_set(S& s, const some_data& data, bar2_tag) {
    s.set_bar2(data.bar2);
}

template <class S>
void serialized_set(S& s, const some_data& data, baz_tag) {
    s.set_baz(data.baz);
}

template <class S>
void serialized_set(S& s, const some_data& data, fbr_tag) {
    s.set_fbr(data.fbr);
}

ボイラープレートも、データメンバーごとに1つのserialized_setに制限されており、以前の回答と同様に線形にスケーリングされます。変更されたserialization_traitsは次のとおりです。

// the serialization_traits doesn't need specialization anymore :)
template <class SerializedX>
class serialization_traits {

    class set_functor {

        const some_data& m_data;

    public:

        typedef SerializedX& result_type;

        set_functor(const some_data& data)
        : m_data(data){}

        template <class Tag>
        SerializedX& operator()(SerializedX& s, Tag tag) const {
            serialized_set(s, m_data, tag);
            return s;
        }
    };

public:

    template <class Tuple>
    static SerializedX encode(const some_data& data, const Tuple& t) {
        SerializedX s;
        boost::fusion::fold(t, s, set_functor(data));
        return s;
    }
};

そして、これがどのように機能するかです:

void send(const some_data& data) {

    send_msg( serialization_traits<SerializedFooBar>::encode(data,
    boost::fusion::make_Tuple(foo_tag(), bar1_tag())));

    send_msg( serialization_traits<SerializedBarBaz>::encode(data,
    boost::fusion::make_Tuple(baz_tag(), bar1_tag(), bar2_tag())));
}

更新されたコード ここ

7
linuxfever

私の意見では、最善の万能ソリューションは、スクリプト言語の外部C++コードジェネレーターです。次の利点があります。

  • 柔軟性:生成されたコードをいつでも変更できます。これは、いくつかのサブ理由に非常に適しています。

    • サポートされているすべての古いリリースのバグをすぐに修正します。
    • 将来C++ 11以降に移行する場合は、新しいC++機能を使用してください。
    • 別の言語のコードを生成します。これは非常に便利です(特に、組織が大きい場合やユーザーが多い場合)。たとえば、ハードウェアとインターフェイスするためのCLIツールとして使用できる小さなスクリプトライブラリ(例:Pythonモジュール))を出力できます。私の経験では、これはハードウェアエンジニアに非常に好まれていました。
    • GUIコード(または、XML/JSONなどのGUI記述、またはWebインターフェイス)を生成します。これは、最終的なハードウェアとテスターを使用するユーザーに役立ちます。
    • 他の種類のデータの生成。たとえば、図、統計など。あるいは、protobufの説明自体です。
  • Maintenance:C++よりも保守が簡単になります。別の言語で書かれている場合でも、通常、新しいC++開発者がC++テンプレートメタプログラミング(特にC++ 03)に飛び込むよりも、その言語を学ぶ方が簡単です。

  • パフォーマンス:C++側のコンパイル時間を簡単に短縮できます(非常に単純なC++を出力できるため-プレーンCでも)。もちろん、ジェネレータはこの利点を相殺する可能性があります。あなたの場合、クライアントコードを変更できないように見えるため、これは当てはまらない可能性があります。

私はいくつかのプロジェクト/システムでそのアプローチを使用しましたが、それは非常にうまくいきました。特に、ハードウェア(C++ lib、Python lib、CLI、GUI ...)を使用するためのさまざまな代替手段は、very ありがとうございます。


補足:生成の一部で既存のC++コードの解析が必要な場合(例:SerializedタイプのOPの場合のように、シリアル化するデータタイプのヘッダー) ;次に、非常に優れたソリューションは、 LLVM/clangのツール を使用してこれを行うことです。

私が取り組んだ特定のプロジェクトでは、数十のC++タイプを自動的にシリアル化する必要がありました(ユーザーによっていつでも変更される可能性があります)。 clang Pythonバインディングを使用して、ビルドプロセスに統合するだけで、コードを自動的に生成できました。Pythonバインディングでは、すべてが公開されていませんでした。 AST詳細(少なくとも当時)では、すべてのタイプ(テンプレート化されたクラス、コンテナーなどを含む)に必要なシリアル化コードを生成するのに十分でした。

12
Acorn

ボイラープレートが実際には、簡単な比較演算子を使用した単純な古いデータ構造体の集まりである場合は、いくつかのマクロでうまくいく可能性があります。

#define POD2(NAME, T0, N0, T1, N1) \
struct NAME { \
    T0 N0; \
    T1 N1; \
    NAME(const T0& N0, const T1& N1) \
        : N0(N0), N1(N1) {} \
    bool operator==(const NAME& rhs) const { return N0 == rhs.N0 && N1 == rhs.N1; } 
\
    bool operator!=(const NAME& rhs) const { return !operator==(rhs); } \
};

使用法は次のようになります。

POD2(BarBaz, Bar, bar, Baz, baz)

template <>
struct serialization_traits<BarBaz> {
    static SerializedBarBaz encode(const BarBaz& bb) {
        SerializedBarBaz sbb;
        sbb.set_bar(bb.bar);
        sbb.set_baz(bb.baz);
        return sbb;
    }
};

N個のマクロが必要になります。ここでNは、引数カウントの順列の数ですが、これは1回限りの初期費用になります。

あるいは、タプルを活用して、提案したように多くの手間のかかる作業を行うこともできます。ここでは、タプルのゲッターに名前を付けるための「NamedTuple」テンプレートを作成しました。

#define NAMED_Tuple2_T(N0, N1) NamedTuple##N0##N1

#define NAMED_Tuple2(N0, N1) \
template <typename T0, typename T1> \
struct NAMED_Tuple2_T(N0, N1) { \
    typedef std::Tuple<T0, T1> TupleType; \
    const typename std::Tuple_element<0, TupleType>::type& N0() const { return std::get<0>(Tuple_); } \
    const typename std::Tuple_element<1, TupleType>::type& N1() const { return std::get<1>(Tuple_); } \
    NAMED_Tuple2_T(N0, N1)(const std::Tuple<T0, T1>& Tuple) : Tuple_(Tuple) {} \
    bool operator==(const NAMED_Tuple2_T(N0, N1)& rhs) const { return Tuple_ == rhs.Tuple_; } \
    bool operator!=(const NAMED_Tuple2_T(N0, N1)& rhs) const { return !operator==(rhs); } \
    private: \
        TupleType Tuple_; \
}; \
typedef NAMED_Tuple2_T(N0, N1)

使用法:

NAMED_Tuple2(foo, bar)<int, int> FooBar;

template <>
struct serialization_traits<FooBar> {
    static SerializedFooBar encode(const FooBar& fb) {
        SerializedFooBar sfb;
        sfb.set_foo(fb.foo());
        sfb.set_bar(fb.bar());
        return sfb;
    }
};
3
Cusiman7

必要なのはタプルのようなですが、実際のタプルではありません。すべての_Tuple_like_クラスがtie()を実装していると仮定すると、これは基本的にメンバーを結び付けるだけです。これが私の架空のコードです。

_template<typename T> struct Tuple_like {
    bool operator==(const T& rhs) const {
        return this->tie() == rhs.tie();
    }
    bool operator!=(const T& rhs) const {
        return !operator==(*this,rhs);
    }        
};
template<typename T, typename Serialised> struct serialised_Tuple_like : Tuple_like<T> {
};
template<typename T, typename Serialised>
struct serialization_traits<serialised_Tuple_like<T, Serialised>> {
    static Serialised encode(const T& bb) {
        Serialised s;
        s.tie() = bb.tie();
        return s;
    }
};
_

両側が適切なtie()を実装している限り、これは問題ありません。ソースクラスまたは宛先クラスが直接コントロールにない場合は、tie()を実装する継承クラスを定義して使用することをお勧めします。複数のクラスをマージするには、メンバーの観点からtie()を実装するヘルパークラスを定義します。

3
Puppy

少し違うアプローチを考えましたか?個別のFooBarとBarBazの表現を持つのではなく、次のようなFooBarBazを検討してください。

message FooBarBaz {
  optional Foo foo = 1;
  optional Bar bar = 2;
  optional Baz baz = 3;
}

そして、アプリケーションコードで、次のように利用できます。

FooBarBaz foo;
foo.set_foo(...);
FooBarBaz bar;
bar.set_bar(...);
FooBarBaz baz;
baz.set_baz(...);
FooBarBaz foobar = foo;
foobar.MergeFrom(bar);
FooBarBaz barbaz = bar;
barbaz.MergeFrom(baz);

または、protobufエンコーディングを利用して、メッセージをシリアル化することもできます。 (protobuf自体は実際にはシリアル化されていません。これは、ProtobufのToStringメソッドの1つを呼び出すことで取得できます)。

// assume string_foo is the actual serialized foo from above, likewise string_bar
string serialized_foobar = string_foo + string_bar;
string serialized_barbaz = string_bar + string_baz;

FooBarBaz barbaz;
barbaz.ParseFromString(serialized_barbaz);

これは、ほとんどのAPIを明示的なフィールドのセットから離れて、オプションのフィールドを持つ一般的なメッセージに移動して、必要なものだけを送信できることを前提としています。システムのエッジをまとめて、特定のプロセスに必要なフィールドが設定されていることを表明してから使用することをお勧めしますが、他の場所では定型文が少なくなる可能性があります。文字列連結トリックは、実際に何が含まれているかを気にしないシステムを通過する場合にも便利です。

2
Charlie