楽しいC ++:コンパイル時間カウンター

__COUNTER__



インラインマクロの安全な代替案を開発することが提案されています。 マクロの最初の出現は0



に、2番目の出現は1



に、というように置き換えられます。 値__COUNTER__



前処理段階で置換されるため、 定数式のコンテキストで使用できます。



残念ながら、 __COUNTER__



マクロ__COUNTER__



ヘッダーファイルで使用するの__COUNTER__



危険です。ヘッダーファイルが異なる方法でオンになっていると、カウンター値が変更されます。 これにより、たとえば、 foo.cpp



ではAWESOME



定数の値が42であるのに対し、 bar.cpp



AWESOME≡33



ある状況が発生する可能性があります。 これは、C ++の世界ではひどい犯罪である1つの定義ルールの原則に違反しています。



1つのグローバルカウンタではなくローカルカウンタを使用する機能が必要です(少なくとも各ヘッダーファイルに対して)。 同時に、 定数式でカウンター値を使用する機能は保持する必要があります。



スタックオーバーフローに関するこの質問に基づいています。



動機付けの例

コンパイル時カウンターはいつ便利ですか?
マクロを実装して、シリアル化をサポートする単純なデータ構造を定義するとします。 次のようになります。



 STRUCT(Point3D) FIELD(x, float) FIELD(y, float) FIELD(z, float) END_STRUCT
      
      





ここでは、 x, y



およびz



フィールドのリストでPoint3D



構造体を定義するだけではありません。 また、シリアル化関数と逆シリアル化関数も自動的に取得します。 新しいフィールドを追加して、そのフィールドのシリアル化サポートを忘れることはできません。 あなたはブーストよりもはるかに少ないことを書かなければならない。



残念ながら、フィールドの定義を生成し、シリアル化関数を生成するために、フィールドのリストを少なくとも2回調べる必要があります。 プリプロセッサのみを使用することはできません。 しかし、ご存知のように、C ++の問題はテンプレートの助けを借りて解決できます(テンプレートの過剰な問題を除く)。



FIELD



マクロを次のように定義します(わかりやすくするために__COUNTER__



を使用します)。



 #define FIELD(name, type) \ type name; //   \ template<> \ void serialize<__COUNTER__/2>(Archive &ar) { \ ar.write(name); \ serialize<(__COUNTER__-1)/2+1>(ar); \ }
      
      





FIELD(x, float)



展開するとFIELD(x, float)



次のようになります



 float x; //   x template<> void serialize<0>(Archive &ar) { ar.write(x); serialize<1>(ar); }
      
      





FIELD(y, float)



デプロイするFIELD(y, float)



判明します



 float y; //   y template<> void serialize<1>(Archive &ar) { ar.write(x); serialize<2>(ar); }
      
      





FIELD()



マクロの以降の各オカレンスは、フィールド定義に加えてserialize<



i >()



関数特殊化>()



i = 0,1,2、... N )に展開されます serialize<i>()



関数はserialize<i+1>()



などを呼び出します。 カウンターは、異なる機能をリンクするのに役立ちます。



リンクは動作するサンプルコードです。




1ビットのコンパイル時カウンター

まず、1ビットカウンターの実装を示します。



 // (1) template<size_t n> struct cn { char data[n+1]; }; // (2) template<size_t n> cn<n> magic(cn<n>); // (3)    sizeof(magic(cn<0>())) - 1; // 0 // (4) «» cn<1> magic(cn<0>); // (5)    sizeof(magic(cn<0>())) - 1; // 1
      
      





  1. テンプレート構造cn<n>



    定義します。 sizeof(cn<n>) ≡ n+1



    ことに注意してください。

  2. テンプレート関数magic



    定義します。

  3. 式に適用されたsizeof



    演算子は、指定された式が持つのサイズを返します。 式は評価されないため、 magic



    関数の本体の定義は不要です。

    現在定義されている唯一のmagic



    関数は、手順2のテンプレートです。したがって、戻り値の型と式全体はcn<0>



    です。

  4. オーバーロードされたmagic



    関数を定義します。 オーバーロードされた関数はテンプレート関数よりも優先されるため、 magic



    呼び出すときにあいまいさが発生しないことに注意してください。

  5. これで、 magic(cn<0>())



    呼び出すときに、関数の別のバリアントが使用されます。 sizeof



    内の式のタイプはcn<



    1 >()



    です。


要約-関数呼び出しを含む式があります。 オーバーロードされた関数の定義を追加します。その結果、コンパイラーは新しい関数を使用します。 したがって、式はテキスト的に同じままですが、関数からの戻り値の型と式全体の型が変更されました。



1ビットカウンターの読み取りと「インクリメント」のためのマクロを定義します。



 #define counter_read(id) \ (sizeof(magic(cn<0>())) - 1) #define counter_inc(id) \ cn<1> magic(cn<0>)
      
      





idパラメーターについて
設計上、 idパラメーターは、一意の型を識別し、それらをidとして使用することにより、いくつかの独立したカウンターを使用できます。 idをサポートするには、 magic



が追加のid



パラメーターを取る必要があります。 オーバーロードされたmagic



関数は特定のidを参照し、他のすべてのidには影響しません。




Nビットコンパイル時間カウンター

Nビットカウンタは、シングルビットカウンタと同じ原理に基づいて構築されています。 sizeof



内の単一のmagic



コールの代わりに、ネストされたコールのチェーンa(b(c(d(e( … ))))).







これが基本的なビルディングブロックです。 これは、タイプT 0の単一引数の関数です。 利用可能なスコープ宣言に応じて、戻り値の型はT 0またはT 1になります。 このデバイスは、鉄道の矢印に似ています。 初期状態では、「矢印」は左に向けられています。 矢印は一度だけ切り替えることができます。



いくつかの基本ブロックを使用して、広範なネットワークを構築できます。



関数の適切なバリアントを検索する場合、C ++コンパイラはパラメータのタイプのみを考慮し、戻り値のタイプを無視します。 式にネストされた関数呼び出しがある場合、コンパイラーは内部から「移動」します。 たとえば、次の式:M 1 (M 2 (M 4 (T 0 ())))では、コンパイラーは最初に関数M 4 (T 0 )の呼び出しを許可(「解決」)します。 次に、関数M 4の戻り値のタイプに応じて、M 2 (T 0 )またはM 2 (T 4 )などの呼び出しを許可します。



鉄道の類推を続けると、コンパイラーは鉄道ネットワークに沿って上から下に移動し、矢印を右または左に「回して」いると言えます。 N個のネストされた関数呼び出しの式は、2 N個の出力を持つネットワークを生成します。 矢印を正しい順序で切り替えると、ネットワークの出力で2 Nのすべての可能なタイプのT iを順番に取得できます。



ネットワークの出力の現在のタイプがT iである場合、次のタイプは矢印M [(i + 1)&〜i、(i + 1)&i]を切り替える必要があることが示されます。



コードの最終バージョンはこちらから入手できます



結論の代わりに

コンパイル時カウンターは、オーバーロードされた関数のメカニズムに完全に基づいています。 Stack Overflowでこの手法を見つけました 。 原則として、C ++での非自明なコンパイル時間の計算はテンプレートに実装されます。これは、提示されたソリューションがテンプレートの代わりに他のメカニズムを使用するため、特に興味深い理由です。



これらのソリューションはどの程度実用的ですか?



単一のC ++ファイルが5分以上コンパイルされ、コンパイラの最新バージョンのみがそれに対処できる場合、これは間違いなく非実用的です。 C ++で言語機能を使用するための多くの「創造的な」オプションは、純粋に学術的な関心の対象です。 原則として、同じタスクは、外部コードジェネレーターを使用するなど、他の方法でより適切に解決できます。 私は言わなければならないが、著者はこの問題にやや偏っており、 精神を断固として認識しておらず、 バイソンに関連していくつかの弱点を経験している。



次のグラフから明らかなように、コンパイル時のカウンターもあまり実用的ではないようです。 x軸はテストプログラムのカウンターインクリメントの絶対値を表し(テストプログラムはcounter_inc(int)



行で構成されます)、 y軸はコンパイル時間を秒単位でcounter_inc(int)



ます。 比較のために、nginx-1.5.2のコンパイル時間も延期されます。










All Articles