テンプレートマジックCallWithTypeパターン

親愛なるハブロフチアン!

この記事では、C ++でコンパイル時データ (型) をランタイムデータ (整数値)に、またはその逆に 変換する方法について説明します。

例:
int nType = ... ;

if boost :: is_base_of < ISettable、 / * ... nTypeによって隠された型をここで魔法のように解決します... * / > :: value
{
//何かをする
}
他に
{
//別のことをする
}

このトピック全体は、「nTypeによって隠された型をここで魔法のように解決する」のではなく、何を書く必要があるかを理解することを目的としています。

結果にのみ興味がある場合は、最後のセクションまでスキップしてください。

ちょっとした歴史


それはすべて、当番では、ほぼ次のように動作するオブジェクトの複雑な工場で作業する必要があるという事実から始まりました:オブジェクトを作成するには、動的データの深byに基づいて、作成されるオブジェクトのタイプのIDを返す特定の関数が呼び出されました このIDはスイッチケースに分類され、実際に次のような必要なオブジェクトが作成されます。
int nObjectType = ResolveObjectType ... ;

boost :: shared_ptr < IObject > pObject = CreateObject nObjectType ;

特定のランタイム条件下で、フォームのラッパーをいくつかのオブジェクトに掛ける必要があることが判明するまで、このシステムではすべてがうまくいきました。
テンプレート < クラス TObject >
クラス CFilter パブリック TObject
{
仮想 ブール FilterValue ... { ... } ;
} ;

このようなラッパーは、特定の条件下で必要な特定の新しい機能をオブジェクトに追加しました。 当然、最初は常にすべてのタイプのオブジェクトに比べて、ラッパーは常に少しだけハングし、少しだけハングしていました(このため、ほんの数行のコードを追加する必要がありましたが、非常に簡単でした)。 ただし、不必要なラッパーは、作業のロジックに害を与えることはありませんが、オブジェクトのサイズを大きくしますが、これは受け入れられませんでした。ファクトリー自体とその助けを借りて作成されたオブジェクトは、パフォーマンスとメモリ消費の点で重要でした。

したがって、ラッパーをオプションで含める必要がありました。 すぐに問題が発生しました:
このすべてを実現し、私は座って考えました。 さらに、GetObjectType(...)によって返される値の背後に隠されている型を描画する方法を学習するだけで、コンパイラーにとって明確になるようになりました。つまり、 精神で物事を書くことができるように:
int nType = ... ;

if boost :: is_base_of < ISettable、 / * ... nTypeによって隠された型をここで魔法のように解決します... * / > :: value
{
//何かをする
}
他に
{
//別のことをする
}

私の最初の考え:「これは不可能です!」

テンプレートの魔法、または不可能は可能です!


もう少し考えた後、次の2つの関数を記述するだけでよいという結論に達しました。
//! TObjectに対応するオブジェクトタイプ記述子を返します。
テンプレート < クラス TObject >
インライン int MakeDescriptor ;

//! nullptrを指定してrcFunctorを呼び出し、nTypeDescriptorによって隠された実際のオブジェクトタイプを呼び出します。
テンプレート < クラス TFunctor >
インライン タイプ名Impl :: ResolveReturnType < TFunctor > :: タイプ CallWithType const TFunctor rcFunctor、 int nTypeDescriptor ;

ここではすべてが明確です:
この方法で使用できます(この例では、インターフェイスからの記述子によってエンコードされた型が継承されるかどうかを判断します)。
テンプレート < クラス TKind >
struct IsKindOfHelper
{
typedef bool
R ;

インライン ブール演算子 ... const
{
falseを 返し ます
}

インライン ブール演算子 TKind * const
{
trueを 返し ます
}
} ;

テンプレート < クラス TObject >
インライン bool IsKindOf int nTypeDescriptor
{
Return CallWithType IsKindOfHelper < TObject > 、nTypeDescriptor ;
}

...

int nType = ... ;

if IsKindOf < ISettable > nType
{
//何かをする
}
他に
{
//別のことをする
}

タイプリストと記述子。

それでは、実装に取り​​かかりましょう。 まず、タイプとタイプの記述子に一致するコンパイルタイムテーブルが必要です。 ここで最も簡単なオプションはLoki :: Typelistです。これは次の形式の構造です。
テンプレート < クラス T、 クラス U >
struct typelist
{
typedef Tヘッド;
typedef U Tail ;
} ;

かつてこの構造を熟考したことで、C ++についての考えが完全に逆さまになりました。 なぜ必要なのか見てみましょう。 すべてが非常に簡単です:任意の長さのタイプのヘルプリストが設定されています:
typedef Loki :: Typelist < int 、Loki :: Typelist < char 、Loki :: Typelist < void 、Loki :: NullType >>>
TMyList ;

ここでは、3つの要素の型のリストが指定されています:int、char、void。 Loki :: NullTypeはリストの終わりを意味します。 このリストから特別なメタ関数を使用して、タイプインデックスとインデックスによるタイプを抽出できます。
// int MyInt;
Loki :: TypeAt < TMyList、 0 > :: Result MyInt ;

// char MyChar;
Loki :: TypeAt < TMyList、 1 > :: Result MyChar ;

int nIndexOfChar = Loki :: IndexOf < TMyList、 char > :: value ;

これらのメタ関数はすべて、コンパイル段階で「呼び出され」、実行時間のオーバーヘッドを必要としません。 Lokiの詳細については、 Wikipediaを参照してください。ライブラリソースへのリンクもあります。 「Modern Design in C ++」(Alexandrescu)の本では、すべてがどのように機能するかを知ることができます。

実際には、 Boost MPLライブラリーを使用しました。 それはより複雑ですが、その可能性ははるかに広いです。 実験により、コンパイラは約2000種類のオブジェクトに耐えることが示され、その後、次の図が観察されます。

コンパルルします..

型リストを介したパターンの実装。

アイデア:
 特定のタイプリスト内のすべての既知のオブジェクトタイプをリストします。 次に、特定の型のインデックスは型記述子になりますが、型記述子を知っている場合は、リストでそれを見ると型自体を表示できます。 唯一の問題は次のとおりです。インデックスによる型推論の場合、インデックスは定数(コンパイル時の値)として表現する必要があります。 数値変数の値を、mpl :: int_ <#value#>という形式の対応する型に変換する方法を学習する必要があります。 

名前空間のブーストを使用し ます

名前空間 Impl
{
//! 既知のオブジェクトタイプのリスト。
/ **現実の世界では、この構造はテンプレートによって構築されています。 * /
typedef mpl :: リスト < TObjectType1、TObjectType2、TObjectType3 >
TKnownObjectTypes ;

//! 既知のオブジェクトタイプの数。
typedef mpl :: サイズ < TKnownAtomTypes > :: タイプ
TKnownObjectTypesCount ;
}

名前空間 Impl
{
//! このメタ関数は、TKnownObjectsからTObjectのインデックスを返します。
/ ** TObjectがTKnownObjectsに存在しない場合、-1を返します* /
テンプレート < クラス TObject >
struct MakeDescriptorImpl
/ * if * / mpl :: eval_if <
/ *(TObject)を見つける== end * /
is_same <
typename mpl :: find < TKnownObjectTypes、TObject > :: type
mpl :: end < TKnownObjectTypes > :: タイプ >
/ * -1を返します* /
mpl :: identity < mpl :: int_ < -1 >>
/ *その他の距離を返す(開始、検索(TObject))* /
mpl :: 適用 <
mpl :: 距離 <
mpl :: begin < TKnownObjectTypes > :: type
mpl :: find < TKnownObjectTypes、_ >>
TObject >> :: タイプ
{
} ;

//! TObjectTypeでTFunctorを呼び出すのに役立ちます*
テンプレート < クラス TFunctor >
struct CallWithObjectTypeHelperPointerBased
{

公開

// typename ResolveReturnType <TFunctor> :: TFunctorに評価される型:: R if
// TFunctor :: R typedefが存在します。それ以外の場合は、voidに評価されます。
typedef typename ResolveReturnType < TFunctor > :: タイプ
R ;

保護された

const TFunctor
m_rcFunctor ;

公開

CallWithObjectTypeHelperPointerBased const TFunctor rcFunctor
m_rcFunctor rcFunctor
{
}

//! この関数は、CallWithInt(...)によって呼び出されます。
テンプレート < クラス TIndex >
R演算子 TIndex const
{
//インデックスでオブジェクトタイプを検索
typedef typename mpl :: < TKnownObjectTypes、TIndex > :: タイプ
TObject ;

//実際のオブジェクト型へのポインタでファンクターを呼び出します
return m_rcFunctor TObject * NULL ;
}

//! この関数は、CallWithInt(...)によって呼び出されます。
R演算子 mpl :: void_ const
{
//記述子が壊れています、特別な値でファンクターを呼び出します
return m_rcFunctor mpl :: void_ ;
}
} ;

}

//! TObjectに対応するオブジェクトタイプ記述子を返します。
テンプレート < クラス TObject >
インライン int MakeDescriptor
{
//不明なオブジェクトタイプのオブジェクトタイプタイプの記述を試みます!
BOOST_STATIC_ASSERT Impl :: MakeDescriptorImpl < TObject > :: value = - 1 ;

//記述子を返します。これは実際にはコンパイル時に生成される定数です。
return Impl :: MakeDescriptorImpl < TObject > :: value ;
}

//! nObjectTypeDescriptorに対応するTObject *でrcFunctorを呼び出します。
テンプレート < クラス TFunctor >
インライン タイプ名Impl :: ResolveReturnType < TFunctor > :: タイプ CallWithType const TFunctor rcFunctor、 int nObjectTypeDescriptor
{
// intで呼び出します
// Impl :: CallWithObjectTypeHelperPointerBased <TFunctor>(rcFunctor)
// mplを持つファンクター:: int_ <N>()引数、Nはコンパイル時定数
// nObjectTypeDescriptorの値に対応。
// nObjectTypeDescriptor <0の場合|| nObjectTypeDescriptor> = TKnownObjectTypesCount
// mpl :: void_()でファンクターが呼び出され、タイプ記述子が壊れていることを示します。
return Impl :: CallWithInt < mpl :: int_ < 0 > 、TKnownObjectTypesCount >
Impl :: CallWithObjectTypeHelperPointerBased < TFunctor > rcFunctor
nObjectTypeDescriptor ;
}

コード全体について詳細にコメントしようとしましたが、まだかなり複雑なので、コメントがいくつかあります。

1。
typename ResolveReturnType :: typeは、コンパイラによってTFunctor :: Rとして解釈されます。 TFunctor :: Rが無効な式である場合(たとえば、R型の定義がTFunctorにない場合)、typename ResolveReturnType :: typeはvoidと解釈されます。 はい、可能です。 いいえ、私は嘘をついていません 。 実装は、CallWithIntの実装を説明するリンクの下に表示できます。

2。
MakeDescriptorImplはboost :: mplで積極的に動作し、威圧的に見えます。 わかりやすくするために、コメントにはstlに同様の式が含まれています(もちろん、コンパイル段階では適用されません)。 インデントスタイルはSchemeから引き裂かれます 。 関数型プログラミング言語に精通している人は、C ++でのメタプログラミング(テンプレートマジック)が関数型言語であることを理解する必要があります。

3。
CallWithIntは、有効な値の特定の領域から実行時整数値をコンパイル時整数値に変換します。 この関数は少し後で実装します。
 例:42はmpl :: int_ <42>に変換されます 

4。
mpl :: listを使用する代わりにバイナリツリーを使用すると、実装は(コンパイル速度の点で)より効率的になります。 残念ながら、そのような構造は見つかりませんでしたが、私自身は長い間書きました。 私たちのプロジェクトでは、これは重要ではなく、500未満のタイプの数がそのように機能します。

5。
実行時のパフォーマンスの観点から、このコードは非常に高速です。 CallWithIntは、以下で説明するように、入れ子になったスイッチケースで機能するため、数値を型に変換するには、オフセット付きの無条件ジャンプを数回行うだけで済みます。 逆変換の場合、何も必要ありません。 MakeDescriptorは定数にインライン化します。

6。
実世界では、既知のオブジェクトのリストは、ほぼ次のようにテンプレートマジックを使用して作成されます。
このアクションは、コンパイル段階でテンプレートを使用して実行され、約200行のコードが必要です。 新しいラッパー(およびラッパーがもたらすすべての新しいタイプ)を追加するには、約10行だけを記述する必要がありますが、新しいタイプは既存のコードによって自動的に取得されます。

7。
上記の関数に加えて、実際には、さらにいくつかのバリアントが実装されています(たとえば、MakeDescriptorNonStrictは、科学に未知の型に適用された場合に-1(通常のコンパイルエラーではない)を返します)。 CallWithTypeには、たとえば2つのポインターを使用してファンクターを呼び出す他のオプションもあります(1つは継承ツリーに特化するために使用し、もう1つは実際の型を決定するために使用できます)。

CallWithIntリリース

 最後の(最も難しい)ステップを実行することは残ります。型mpl :: int_ <N>()のファンクターを呼び出す関数を作成します。ここで、Nはランタイム変数内に格納された値に対応します。 これはおそらく最も難しい部分です。なぜなら、 ランタイム値を型に変換するのは彼女です。 

アイデアは非常に簡単です。
 スイッチ、たとえば100個の要素を持つ関数を作成します。この関数は値mpl :: int <N>を持つファンクターを呼び出す必要があります。ここで、Nは関数に渡される変数の値に対応します。 別の間隔を変換するように求められた場合、単純に追加の不正行為を行います。オフセットとビンへの分割です。 したがって、たとえば、56 ... 156の間隔で数値を型に変換するように求められた場合、56に渡された変数から毎回減算するだけでよく、型に変換した後、56を追加します(ただし、型に!)。 間隔200..400から数値を変換するように求められた場合、最初にそれをセクション「100」に分割し、次にセクションの数とセクション内のオフセットを計算する必要があります。 

私はそれを不可解に説明していると思うので、 ここにたくさんのキラーコードがあります

備考:

0。
多くのブナ:(

1。
実際には、スイッチには100のケースがあります(値は実験的に選択され、コードは2以上の任意の値に対して動作可能です)。

2。
現実の世界では、スイッチケースはマクロによって生成されます。

3。
速く動作します。 とても速い。 このような残忍な表現でこれを支払う必要があります。 コンパイラは、末尾再帰を切り替えて単純な実装を最適化できませんでした:(

申込み


これで次のことができます。

1.オブジェクトを作成するには、オブジェクトのタイプを既知のタイプのリストに追加します。 以前は、新しいタイプのオブジェクトを追加する場合、すべてのスイッチケースをタイプごとに検索する必要がありましたが、スイッチケースはまったくありません。つまり、すべての新しいタイプのオブジェクトが「すぐに」サポートされます。 この例は、スイッチケースの消失を示しています。

それは:
int nObjectType = ResolveObjectType ... ;

スイッチ nObjectType
{
ケース 1
新しい TObject1 )を 返し ます。
ケース 2
新しい TObject2 )を 返し ます。
ケース 3
新しい TObject3 )を 返し ます。
ケース 4
新しい TObject4 )を 返し ます。
/ * ... 100件以上のケースがここに入ります* /
デフォルト
NULLを 返し ます
} ;

次のようになりました:
//! このラッパーにより、実際のオブジェクトタイプを判別できます。
テンプレート < クラス TBase、 クラス TObject = TBase >
struct CTypeWrapper
パブリック TBase
{
//! このインスタンスに対応する型記述子を返します。
仮想 整数 TypeDescriptor const
{
MakeDescriptor < TObject > )を 返し ます。
}
} ;

//! オブジェクトを作成するのに役立ちます。 実際、これはオブジェクト型で呼び出されるファンクターです。
クラス CreateObjectHelper
{

公開

//戻り型
typedef IObject *
R ;

プライベート

テンプレート < クラス TBase、 クラス TObject >
インライン TBase * MakeObject const
{
新しい CObjectTypeWrapper < TBase、TObject > )を 返し ます。
}

公開

//! 一般的なケース
テンプレート < クラス TObject >
インライン TObject *演算子 TObject * 、... const
{
MakeObject < TObject、TObject > )を 返し ます。
}

インライン IObject *演算子 boost :: mpl :: void_ const
{
assert "Type Descriptor Is Broken!Must'n not here here! )) ;

NULLを 返し ます
}

公開

// IObjectType1から派生したオブジェクトの特殊なケース
テンプレート < クラス TObject >
IObject *演算子 TObject * 、IObjectType1 * const
{
// ...
}

// IObjectType2から派生したオブジェクトの特殊なケース
テンプレート < クラス TObject >
IObject *演算子 TObject * 、IObjectType2 * const
{
// ...
}
} ;

...

int nObjectType = ResolveObjectType ... ;

ObjectTraitsを返す:: CallDoublePointerBasedFunctorWithObjectType
CreateObjectHelper
nObjectType ;

2. dynamic_castを使用せずに、オブジェクトの実際のタイプに応じて、継承ツリーの中央にあるクラスの動作を変更します(CallWithTypeを使用したアプローチは、クラス階層で約50倍高速です)。
テンプレート < クラス TKind >
struct IsKindOfHelper
{
typedef bool
R ;

インライン ブール演算子 ... const
{
falseを 返し ます
}

インライン ブール演算子 TKind * const
{
trueを 返し ます
}
} ;

テンプレート < クラス TObject >
インライン bool IsKindOf int nTypeDescriptor
{
Return CallWithType IsKindOfHelper < TObject > 、nTypeDescriptor ;
}

...

// TypeDescriptorは、仮想オブジェクトであり、実際のオブジェクトタイプの記述子を返します。
//前の例で示した実装(この関数を手動で記述する必要はありません)
if IsKindOf < ISettable > this- > TypeDescriptor
{
//何かをする
}
他に
{
//別のことをする
}

3.型として直接型記述子を含む変数を操作する機能。 これは、どの種類のオブジェクトタイプを作成するかを決定する手順で非常に役立ちます(タイプが多くの外部要因に依存する場合)。
int ApplySomeWrapper int nType
{
bool bShouldBeWrapperApplied = ... ;

if bShouldBeWrapperApplied
{
// IsWrapperApplicableは、CallWithTypeを使用してnTypeを実際の型TObjectに変換します
// WrapperTraitsを呼び出します:: CSomeWrapper :: IsApplicable <TObject> ::値メタ関数
if IsWrapperApplicable < WrapperTraits :: CSomeWrapper > nType
{
// IsWrapperApplicableは、CallWithTypeを使用してnTypeを実際の型TObjectに変換します。
// WrapperTraitsを呼び出します:: CSomeWrapper :: MakeWrappedType <TObject> ::型メタ関数
//ラップされた型を解決するために、この型のMakeDescriptorを呼び出します。
MakeWrappedType < WrapperTraits :: CSomeWrapper > nType ;を返します。
}
}

nTypeを返します
}

Source: https://habr.com/ru/post/J108196/


All Articles