問題のステートメントから始めましょう。 ネットワークゲームを作成しています(もちろん、すぐにMMORPGも!)。また、アーキテクチャに関係なく、ネットワークを介してデータを常に送受信する必要があります。 ほとんどの場合、いくつかの異なる種類のパッケージ(プレーヤーのアクション、ゲームワールドの更新、単純に認証など)を送信する必要があります。そして、それぞれに読み取り関数と書き込み関数が必要です。 落ち着いてこれらの2つの関数を落ち着いて記述し、緊張しないことは問題ではないように思えますが、すぐに多くの問題が発生します。
- フォーマットの選択。 JavaScriptで簡単なゲームを作成する場合、JSONまたはその説明的な親withに満足します。 しかし、私たちはトラフィックを要求する深刻なマルチプレイヤーゲームを書いています。 4バイトではなく〜16バイトをフロートに送信する余裕はありません。 したがって、生のバイナリ形式が必要です。 ただし、バイナリデータはデバッグを複雑にします。 すべての読み取り/書き込み機能を完全に書き換えることなく、いつでもフォーマットを変更できると便利です。
- セキュリティの懸念。 ネットワークゲームの最初のルール: クライアントから送信されたデータを信頼しないでください ! 読み取り機能はいつでも中断でき、何か問題が発生した場合は
false
を返すことができます。 ただし、例外を使用するのは遅すぎるため、重要ではないと考えられます。 Momkinのハッカーは、たとえあなたのサーバーを破壊しなくても、継続的に実行することでサーバーの速度を低下させることができます。 しかし、ifとreturnで構成されるコードを手動で記述することは、不快で見た目が悪いです。 - コードが重複しています。 読み取り機能と書き込み機能は似ていますが、完全ではありません。 パッケージの構造を変更する必要があると、 2つの機能を変更する必要が生じ、遅かれ早かれ、いずれか1つを変更するのを忘れたり、異なる方法で変更したりすることになり、キャッチしにくいバグにつながります。 Gaffer on Gamesが正しく観察しているように、 読み取り機能と書き込み機能を別々に維持するのは本当に面倒です。
ベンダーがどのように約束を果たし、同時に特定された問題を解決したかに興味がある人は、猫の下でお願いします。
ストリームの読み取りと書き込み
最初の仮定から始めましょう。 テキストとバイナリ形式を読み書きできるようにしたい。 テキスト形式を標準のSTLストリームから読み取り/
std::basic_istream
ます(それぞれ
std::basic_istream
および
std::basic_ostream
)。 バイナリ形式の場合、ストリームと同様のSTLインターフェイスをサポートする独自の
BitStream
クラスがあります(少なくとも
<<
および
>>
演算子、読み取り/書き込みエラーがない場合は0を返し、その他の場合は0を返さない
rdstate()
メソッド、およびマニピュレーターを食べる機能) ; また、8ビットの倍数ではない長さのデータを読み書きできるとすばらしいでしょう。
可能なBitStreamクラスインターフェイス
using byte = uint8_t; class BitStream { byte* bdata; uint64_t position; uint64_t length, allocated; int mode; // 0 = read, other = write int state; // 0 = OK void reallocate(size_t); public: static const int MODE_READ = 0; // , , static const int MODE_WRITE = 1; // enum class, inline int get_mode(void) const noexcept { return mode; } BitStream(void); // BitStream(void*, uint64_t); // ~BitStream(void); int rdstate(void) const; // how_much : void write_bits(char how_much, uint64_t bits); // how_much : uint64_t read_bits(char how_much); void* data(void); BitStream& operator<<(BitStream&(*func)(BitStream&)); // BitStream& operator>>(BitStream&(*func)(BitStream&)); // }; template<typename Int> typename std::enable_if<std::is_integral<Int>::value, BitStream&>::type operator<<(BitStream& out, const Int& arg); // 8*sizeof(Int) template<typename Int> typename std::enable_if<std::is_integral<Int>::value, BitStream&>::type operator>>(BitStream& in, Int& arg); // 8*sizeof(Int)
enable_ifがここにあるのはなぜですか?
は、条件
をチェックし、条件が満たされた場合(つまり、ゼロに等しくない場合)、タイプ
決定し、ユーザーが指定したタイプ
またはデフォルト)
条件が満たされない場合、
への呼び出しは未定義を返します。 このようなエラーはテンプレートのコンパイルを妨げますが、 置換の失敗はエラーではないため(SFINAE) 、テンプレートに引数を代入するときのエラーはコンパイルエラーではないため、プログラムのコンパイルは妨げられません。
別の実装が適切なシグネチャでどこかで定義されている場合、または呼び出す適切な関数が単にないという場合、プログラムは正常にコンパイルされます(スマートコンパイラは、試行したことをSFINAEで指定できます)。
std::enable_if<condition, T>
は、条件
condition
をチェックし、条件が満たされた場合(つまり、ゼロに等しくない場合)、タイプ
std::enable_if<...>::type
決定し、ユーザーが指定したタイプ
T
またはデフォルト)
void
条件が満たされない場合、
std::enable_if<...>::type
への呼び出しは未定義を返します。 このようなエラーはテンプレートのコンパイルを妨げますが、 置換の失敗はエラーではないため(SFINAE) 、テンプレートに引数を代入するときのエラーはコンパイルエラーではないため、プログラムのコンパイルは妨げられません。
operator<<
別の実装が適切なシグネチャでどこかで定義されている場合、または呼び出す適切な関数が単にないという場合、プログラムは正常にコンパイルされます(スマートコンパイラは、試行したことをSFINAEで指定できます)。
シリアライザーインターフェイス
シリアライザーの基本的な「ブリック」が必要であることは明らかです。整数または浮動小数点数をシリアル化および解析できる関数またはオブジェクトです。 ただし、我々は(もちろん!)拡張性、つまり プログラマーが自分のデータ型をシリアル化するための「ブリック」を作成し、それをシリアライザーで使用できるようにします。 そのようなレンガはどのように見えるべきですか? 最も簡単な形式をお勧めします。
struct IntegerField { template<class OutputStream> static void serialize(OutputStream& out, int t) { out << t; // ! } // bool, template<class InputStream> static bool deserialize(InputStream& in, int& t) { in >> t; // ! return !in.rdstate(); // true, } };
2つの静的メソッドとおそらく無制限のオーバーロードを持つクラス。 (したがって、1つのテンプレートメソッドの代わりに、
std::basic_ostream
に1つ、
BitStream
に1つ、プログラマーの好みに合わせて他のストリームに無制限の数を書き込むことができます。)
たとえば、要素の動的配列をシリアル化および解析する場合、インターフェイスは次のようになります。
template<typename T> struct ArrayField { template<class OutputStream> static void serialize(OutputStream& out, size_t n, const T* data); template<class OutputStream> static void serialize(OutputStream& out, const std::vector<T>& data); template<class InputStream> static bool deserialize(InputStream& in, size_t& n, T*& data); template<class InputStream> static bool deserialize(InputStream& in, std::vector<T>& data); };
ヘルパーパターンcan_serialize
およびcan_deserialize
次に、そのようなフィールドがそのような引数およびそのような引数を使用してシリアル化/解析をトリガーできるかどうかを確認する機能が必要です。 ここで、可変長テンプレートとSFINAEのより詳細な説明に進みます。
コードから始めましょう:
template<typename... Types> struct TypeList { // , « » static const size_t length = sizeof...(Types); }; template<typename F, typename L> class can_serialize; template<typename F, typename... Ts> class can_serialize<F, TypeList<Ts...>> { template <typename U> static char func(decltype(U::serialize(std::declval<Ts>()...))*); template <typename U> static long func(...); public: static const bool value = ( sizeof(func<F>(0)) == sizeof(char) ); };
これは何ですか これは、コンパイル段階で、指定されたクラス
F
と型のリスト
L = TypeList<Types...>
によって、これらの型の引数で関数
F::serialize
を呼び出すことができるかどうかを決定する構造です。 例えば
can_serialize<IntegerField, TypeList<BitStream&, int> >::value
1に等しい can_serialize<IntegerField, TypeList<BitStream&, char&> >::value
( char&
完全に
int
変換されるため)、しかし、
can_serialize<IntegerField, TypeList<BitStream&> >::value
IntegerField
は、出力ストリームのみを受け入れる
serialize
メソッドを提供しないため、0です。
どのように機能しますか? より微妙な質問、それを理解しましょう。
TypeList
クラスから始めましょう。 ここでは、Benderが約束する可変個引数テンプレート 、つまり、可変個の引数を持つ テンプレートを使用します 。
TypeList
クラスのテンプレートは、任意の数の型引数を受け入れます。これらの引数は、
Types
という名前でパラメーターパックに配置されます 。 ( 以前の記事でパラメーターパックの使用方法について詳しく説明しました。)
TypeList
クラスは何の役にも
TypeList
ませんが、手持ちのパラメーターパックを使って多くのことができます。 たとえば、建設
std::declval<Ts>()...
タイプT1, T2, T3, T4
を含む長さ4のパラメーターパックの場合、
std::declval<T1>(), std::declval<T2>(), std::declval<T3>(), std::declval<T4>()
次。 クラス
F
とタイプ
L
リストを
can_serialize
テンプレートと、リスト内のタイプ自体へのアクセスを可能にする部分的な特殊化があります。 (
can_serialize<F, L>
を要求すると、
L
型のリストで
L
ませんが、コンパイラーは未定義のテンプレートについて文句を言うでしょう。当然のことです。)この部分的な特殊化では、すべての魔法が通じます。
彼女のコードには、
sizeof
内の
func<F>(0)
呼び出しがあります。 コンパイラーは、戻り値のサイズをバイト単位で計算するために、どの
func
関数のオーバーロードが呼び出されるかを決定するように強制されますが、コンパイルは試行されないため、「関数の実装が見つかりません」などのエラー(およびエラー「関数の本体にある種のがらくたの種類」(この本体があった場合)。 最初に、彼は
func
の最初の定義、非常に複雑な外観を使用しようとします。
template <typename U> static char func( decltype( U::serialize( std::declval<Ts>()... ) )* );
decltype
コンストラクト
decltype
括弧で囲まれた式
decltype
タイプを
decltype
。 たとえば、
decltype(10)
は
int
と同じです。 ただし、
sizeof
と同様に、コンパイルしません。 これにより、フォーカスを
std::declval
で使用できます。
std::declval
は、必要な型の右辺値参照を返すふりをする関数です。 それは式
U::serialize( std::declval<Ts>()... )
意味のあるものにし、引数の半分にデフォルトのコンストラクタがなく、単に
U::serialize( Ts()... )
書くことができない場合でも、実際の
U::serialize
呼び出しを模倣します
U::serialize( Ts()... )
(この関数は左辺値参照を必要とすることは言うまでもありません!ちなみに、この場合、
declval
は左辺値参照を与えます。これは、C ++
T& &&
declval
が
T& &&
等しいためです)。 もちろん、実装はありません。 プレーンコードで書く
int a = std::declval<int>();
悪い考えです。
だからここに。
decltype
内での
decltype
不可能な場合(そのようなシグネチャを持つ関数がないか、その置換が何らかの理由でエラーを引き起こす)-コンパイラーは、置換エラーが発生したと見なします。これはご存じのとおり、エラーではありません(SFINAE)。 そして、彼は冷静にさらに進んで、次の
func
定義を使用しようとします。 ただし、別の関数は異なるサイズの結果を返します。これは、
sizeof
を使用して簡単にキャプチャできます。 (実際、それほど簡単ではなく、
sizeof(long)
は、エキゾチックなプラットフォームでは
sizeof(char)
に等しくなる可能性がありますが、これらの詳細を省略します-これはすべて修正可能です。)
自己反映の糧として、
can_deserialize
テンプレートのコードも提供します。これは、もう少し複雑です
F::deserialize
が指定されたタイプの引数で
can_deserialize
かどうかをチェックするだけでなく、結果タイプが
bool
であることも確認します。
template<typename F, typename L> class can_deserialize; template<typename F, typename... Ts> class can_deserialize<F, TypeList<Ts...>> { template <typename U> static char func( typename std::enable_if< std::is_same<decltype(U::deserialize(std::declval<Ts>()...)), bool>::value >::type* ); template <typename U> static long func(...); public: using type = can_deserialize; static const bool value = ( sizeof(func<F>(0)) == sizeof(char) ); };
レンガのパッケージを収集します
最後に、シリアライザのコンテンツに取り組む時が来ました。 つまり、ブリックからアセンブルされた関数を
deserialize
serialize
および
deserialize
serialize
する
Schema
テンプレートクラスを取得する必要があります。
using MyPacket = Schema<IntegerField, IntegerField, FloatField, ArrayField<float>>; MyPacket::serialize(std::cout, 10, 15, 0.3, 0, nullptr); int da, db; float fc; std::vector<float> my_vector; bool success = MyPacket::deserialize(std::cin, da, db, fc, my_vector);
単純なものから始めましょう-テンプレートクラスを宣言します(引数の数が可変、ny!)そして再帰の終わりです。
template<typename... Fields> struct Schema; template<> struct Schema<> { template<typename OutputStream> static void serialize(OutputStream&) { // ! } template<typename InputStream> static bool deserialize(InputStream&) { return true; // -- ! } };
しかし、フィールドの数がゼロでない回路では、
serialize
関数コードはどのように見えるべきでしょうか? 事前に、これらすべてのフィールドの
serialize
関数で受け入れられる型を計算することはできず、それらを連結することもできません。これには、標準にまだ含まれていない呼び出し型特性が必要になります 。 残っているのは、可変数の引数を使用して関数を作成し、各フィールドに食べられる限り多くの引数を送信することです。ここでは、苦痛の中で生まれた
can_serialize
を使用できます。
引数の数の観点からこの再帰を行うには、ヘルパークラスが必要です(メインの
Schema
クラスは、フィールドの数の再帰を処理します)。 引数を制限せずに定義します:
template< typename F, // , serialize typename NextSerializer, // «» typename OS, // typename TL, // , F::serialize bool can_serialize // > struct SchemaSerializer;
次に、
Schema
の部分的な特殊化は、最終的にフィールド数による再帰を実装し、次の形式を取ります
template<typename F, typename... Fields> struct Schema<F, Fields...> { template< typename OutputStream, // typename... Types // > static void serialize(OutputStream& out, Types&&... args) { // serialize : SchemaSerializer< F, // Schema<Fields...>, // OutputStream&, // TypeList<Types...>, // can_serialize<F, TypeList<OutputStream&, Types...>>::value // !!! >::serialize(out, std::forward<Types>(args)...); } // . . . ( deserialize) };
次に、
SchemaSerializer
再帰を
SchemaSerializer
ます。 簡単なものから始めましょう-最後から:
template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, false> { // , . // ( ) F::serialize // . , // -- - , // no such function serialize(...) . }; template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, true> { // -- ! -- F::serialize // ! ( ) template<typename... TailArgs> // static void serialize(OS& out, TailArgs&&... targs) { F::serialize(out); // , // ( out - ) // : NextSerializer::serialize(out, std::forward<TailArgs>(targs)...); } };
ここで、ベンダーが約束した2番目の概念、 つまり完全な転送に進みます。 追加の引数(引数はないかもしれませんが、おそらくない)があり、それらを
NextSerializer::serialize
さらに送信したいです。 テンプレートの場合、これは完全転送の問題として知られる問題です。
完璧な転送
1つの引数を取るテンプレート関数
f
ラッパーを作成するとします。 例えば
template<typename T> void better_f(T arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
見た目は良さそうですが、 f
T&
だけでなく左辺値リンク
T&
を入力として受け入れると、すぐに壊れます。元の関数
f
は、入力として一時オブジェクトへのリンクを受け取ります。これは、タイプTはリンクのないタイプとして推定されるためです。 解決策は簡単です。
template<typename T> void better_f(T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
また、 f
が値で引数を取る場合、すぐに壊れます。リテラルや他の右辺値を元の関数に送信できましたが、新しい関数には送信できませんでした。
コンパイラーが選択でき、両方の場合に完全な互換性が存在するように、両方のオプションを記述する必要があります。
template<typename T> void better_f(T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); } template<typename T> void better_f(const T& arg) { std::cout << "I'm so much better..." << std::endl; f(arg); }
そして、1つの引数を持つ1つの関数のすべてのサーカス。 引数の数が増えると、本格的なラッパーに必要なオーバーロードの数が指数関数的に増えます。
これに対処するために、C ++ 11では右辺値参照と新しい型計算規則が導入されています。 今、あなたは簡単に書くことができます
template<typename T> void better_f(T&& arg) { std::cout << "I'm so much better..." << std::endl; // ? . . }
型計算のコンテキストでの&&修飾子には特別な意味があります(ただし、通常の右辺値参照と混同するのは簡単です)。 タイプtype
オブジェクトへの左辺値参照が関数に渡される場合、タイプTは
type&
として推測されます。 タイプ
type
右辺値が渡される場合、タイプTは
type&&
として推測されます。 既定の引数を不必要にコピーすることなく、完全に完全に転送するために最後に行うことは、
std::forward
を使用すること
std::forward
。
template<typename T> void better_f(T&& arg) { std::cout << "I'm so much better..." << std::endl; f(std::forward<T>(arg)); }
std::forward
は通常のリンクに触れず、値によって渡されたオブジェクトを右辺値リンクに変換します。 したがって、最初のラッパーの後、右辺値リンクはオブジェクト自体の代わりにラッパーのチェーン(存在する場合)をさらに下って、不要なコピーを排除します。
シリアライザーの継続
だから建設
NextSerializer::serialize(out, std::forward<TailArgs>(targs)...);
完全な転送を実装し、すべての「余分な」引数を変更せずにシリアライザーのチェーンの下流に送信します。
SchemaSerializer
再帰を書き続け
SchemaSerializer
。
can_serialize = false
再帰ステップ:
template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, false>: // F::serialize -- // ; , // serialize public SchemaSerializer<F, NextSerializer, OS, typename Head<TypeList<Types...>>::Result, // , can_serialize<F, typename Head<TypeList<OS, Types...>>::Result>::value // !!! > { // ¯\_(ツ)_/¯ };
型リストから最後の要素を切り離すヘルパークラスHeadの実装
template<typename T> struct Head; // ... template<typename... Ts> struct Concatenate; // ! template<> struct Concatenate<> { using Result = EmptyList; }; template<typename... A> struct Concatenate<TypeList<A...>> { using Result = TypeList<A...>; }; template<typename... A, typename... B> struct Concatenate<TypeList<A...>, TypeList<B...>> { using Result = TypeList<A..., B...>; }; template<typename... A, typename... Ts> struct Concatenate<TypeList<A...>, Ts...> { using Result = typename Concatenate< TypeList<A...>, typename Concatenate<Ts...>::Result >::Result; }; // , ++ // template<typename T, typename... Ts> // struct Head<TypeList<Ts..., T>>, // template<typename T, typename... Ts> struct Head<TypeList<T, Ts...>> { using Result = typename Concatenate<TypeList<T>, typename Head<TypeList<Ts...>>::Result>::Result; }; template<typename T, typename Q> struct Head<TypeList<T, Q>> { using Result = TypeList<T>; }; template<typename T> struct Head<TypeList<T>> { using Result = TypeList<>; }; template<> struct Head<TypeList<>> { using Result = TypeList<>; };
can_serialize = true
再帰ステップ:
template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, true> { template<typename... TailTypes> // static void serialize(OS& out, Types... args, TailTypes&&... targs) { F::serialize(out, std::forward<Types>(args)...); // ( out - ) // : NextSerializer::serialize(out, std::forward<TailTypes>(targs)...); } };
III ...以上です! これで、シリアライザー(最も一般的な用語で)の準備が整い、最も簡単なコード
using MyPacket = Schema< IntegerField, IntegerField, CharField >; MyPacket::serialize(std::cout, 777, 6666, 'a');
正常に表示されます
しかし、それをデシリアライズする方法は? それでもスペースを追加する必要があります。 これを行うための適切な(つまり、Tru-C ++に十分な抽象)方法は、フィールドセパレータマニピュレータをファイルすることです。7776666a
template< class CharT, class Traits > std::basic_ostream<CharT, Traits>& delimiter( std::basic_ostream<CharT, Traits>& os ) { return os << CharT(' '); // std::ostream } template< class CharT, class Traits > std::basic_istream<CharT, Traits>& delimiter( std::basic_istream<CharT, Traits>& is ) { return is; // } BitStream& delimiter(BitStream& bs) { return bs; // -- , ! // ( , // ) }
std::basic_ostream
は、それへのリンクを受け入れて返す関数を食べることができるため(
std::endl
、
std::flush
?がどのように構成されていると思いますか)、すべてのシリアル化コードが
serialize(OS& out, ...) { F::serialize(out, ...); out << delimiter; // NextSerializer::serialize(out, ...); }
その後、自然になります(そして、シリアル化解除の準備ができました) しかし、まだ細かい部分が残っています...777 6666 a
ネスティング
私たちの回路は単純なフィールドと同じインターフェースを持っているので、なぜ回路から回路を作成しませんか?
using MyBigPacket = Schema<MyPacket, IntegerField, MyPacket>; MyBigPacket::serialize(std::cout, 11, 22, 'a', 33, 44, 55, 'b');
iiiiをコンパイルします... 'serialize'の呼び出しに一致する関数がありません。 問題は何ですか?
実際、
Schema::serialize
は与えられたすべての引数を使い果たします。 外部回路は、
Schema::serialize
がスローされたすべての引数で呼び出せることを確認します。 コンパイラーはコンパイルし、最後の4つの引数が機能していないことを確認し ( 候補関数テンプレートは実行不可:1つの引数が必要ですが、5つが提供されました )、エラーを報告します。
SFINAEの利点は、欠点としてここにasいました。 コンパイラは、指定された引数で呼び出すことができるかどうかを判断する前に、関数をコンパイルしません。 彼は彼女のタイプを見ているだけです。 この不要な動作を排除
Schema::serialize
には、不適切な引数が渡された場合、
Schema::serialize
強制的に無効な型に
Schema::serialize
必要があります。
これがために直接なりやっ
Schema
と
SchemaSerializer
-それは簡単です。
Schema
これが既に行われ、関数
serialize
に無効な引数を持つ無効な型があると仮定します。クラスの特殊化を変更します
SchemaSerializer
:
template<typename F, typename NextSerializer, typename OS> struct SchemaSerializer<F, NextSerializer, OS, TypeList<>, true> { template<typename... TailArgs> static auto serialize(OS& out, TailArgs&&... targs) -> decltype(NextSerializer::serialize(out, std::forward<TailArgs>(targs)...)) { F::serialize(out); out << delimiter; NextSerializer::serialize(out, std::forward<TailArgs>(targs)...); } }; template<typename F, typename NextSerializer, typename OS, typename... Types> struct SchemaSerializer<F, NextSerializer, OS, TypeList<Types...>, true> { template<typename... TailTypes> static auto serialize(OS& out, Types... args, TailTypes&&... targs) -> decltype(NextSerializer::serialize(out, std::forward<TailTypes>(targs)...)) { F::serialize(out, std::forward<Types>(args)...); out << delimiter; NextSerializer::serialize(out, std::forward<TailTypes>(targs)...); } };
どうした 最初に、新しい構文を使用しました。C ++ 11以降では、関数の結果の型を設定するための次のメソッドは同等です。
type func(...) { ... } auto func(...) -> type { .. }
なぜこれが必要なのですか?場合によっては、より便利です。たとえば、式c
std::declval
の2番目のバージョンの構文では、関数の引数が既に使用可能になっ
type
ており、最初の-no では、フォーカスcを再度使用せずに目的を達成できましたそして、私たちは実際に何を達成しましたか?しかし、次のとおりです。再帰が壊れて、指定された引数で呼び出すことができない場合、呼び出しは置換エラーを引き起こします。戻り値のタイプ(したがって、関数全体のタイプ)は計算できません。このように、私たちを呼び出すと、置換エラーが発生します。エラーは最上部まで上昇し、関数のタイプを決定する段階で、そのような引数で呼び出すことは不可能であることをユーザーに伝えます。
NextSerialize::serialize
NextSerialize::serialize(out, std::forward<TailTypes>(targs)...)
SchemaSerializer::serialize
Schema::serialize
。同様にスペシャライゼーションを変更し
Schema
ます:
template<typename F, typename... Fields> struct Schema<F, Fields...> { // using ( , ++11!) template<class OutputStream, typename... Types> using Serializer = SchemaSerializer< F, // Schema<Fields...>, // OutputStream&, // TypeList<Types...>, // can_serialize<F, TypeList<OutputStream&, Types...>>::value // !!! >; template< typename OS, // typename... Types // > static auto serialize(OS& out, Types&&... args) -> decltype(Serializer<OS, Types...>::serialize(out, std::forward<Types>(args)...) ) { Serializer<OS, Types...>::serialize(out, std::forward<Types>(args)...); } // . . . };
いいね! 少しシンプルなコードになりました
using MyPacket = Schema< IntegerField, IntegerField, CharField >; using MyBigPacket = Schema< MyPacket, IntegerField, MyPacket >; MyBigPacket::serialize(std::cout, 11, 22, 'a', 33, 44, 55, 'b');
コンパイルして楽しく印刷する
11 22 a 33 44 55 b
やった!
おわりに
C ++は大きな進歩を遂げ、C ++ 11標準は特に大きな一歩を踏み出しました。ほぼすべてのイノベーションを体系的に使用して、クリーンで美しいシリアライザーを実装していますが、これはサポートしていません。各フィールドの任意の数の引数を許容し、各フィールドの任意の数のテンプレートと非標準関数のオーバーロード
serialize
を許容します。他のシリアライザーをフィールドとして許容します。私の意見では、主なことは、すべての引数を宛先に正確に持ち込むことにより、型キャストを殺さないことです。
SchemaDeserializer
関数を実装するヘルパークラスを記述する方法を理解するのは簡単です
deserialize
— . — ( , , ..), /.
Github .
() . , ! ご清聴ありがとうございました。