Andrei Alexandrescu著「Modern C ++ Programming」の本を読んだ人は、テンプレートが変数(以前は不明)の引数を指定する必要がある場合、(テンプレートを使用したメタプログラミングの分野で)広範なタスククラスがあることを知っています。 そのようなタスクの典型的な例:
-タプルの説明
-オプションなどのタイプの説明
-ファンクターの説明(この場合、引数タイプのリストは関数のシグネチャに依存します)
-定義済みセットによるタイプ分類
-など
そのような各タスクで、引数として対応するテンプレートに渡される正確な型の数を事前に決定することは困難です。 そして、一般的に言えば、それは対応するテンプレートクラスを使用するつもりの人の欲求とニーズに依存します。
現在のC ++標準では、このような問題に対する便利な解決策はありません。 テンプレートは、厳密に定義された数のパラメーターのみを使用できます。 A. Alexandrescu(上記の本で)は、いわゆるに基づいた一般的なソリューションを提供します。 「タイプリスト」。タイプは、再帰パターンを介して実装された単一リンクリストとして表されます。 別の解決策(たとえば、boost :: variantおよびboost :: tupleで使用)は、多数のパラメーターを持つテンプレートクラスを宣言することです。 これらのソリューションはどちらも中途半端であり、考えられるすべてのタスクを網羅していません。 したがって、既存のソリューションの欠点を排除し、コードを簡素化するために、新しい標準はC ++を提供します-開発者はテンプレートを宣言するための新しいオプションですか? 「可変数のパラメーターを持つテンプレート」、または元の「可変テンプレート」。
シンプルなユースケース
可変数のパラメーターを持つテンプレート宣言は次のとおりです。
テンプレート <
typename ... Types>
クラス VariadicTemplate
{
};
可変数の非型パラメーターを持つテンプレートは、同じ方法で宣言されます:
テンプレート <
int ... Ints>
クラス VariadicTemplate
{
};
ここで、現在の標準のフレームワークでこれをエミュレートすることは非常に重要なタスクであることに注意する必要があります(不可能と言わない限り)。
テンプレートクラスに加えて、可変数のパラメーターを使用してテンプレート関数を宣言することもできます。 同様の広告は次のようになります。
テンプレート <
typename ... Type>
void printf(
const char * format、Type ... args);
明らかに、この種のテンプレートパラメーター(「パラメーターパック」または「パラメーターパック」と呼ばれます)は、通常の(単一の)テンプレートパラメーターを使用できる場所では使用できません。 パラメーターパッケージは、次のコンテキストで使用できます。
- テンプレートの基本クラスの列挙(base-specifier-list);
- コンストラクターのデータメンバーの初期化リスト(mem-initializer-list)。
- 初期化リスト(initializer-list)
- 他のテンプレートのパラメーターリスト(template-argument-list)。
- 例外の仕様(動的例外仕様);
- 属性リスト(attribute-list);
- ラムダ関数のキャプチャリスト(capture-list)。
パラメータパッケージの使用場所に応じて、このパッケージの要素はそれに応じて解釈されます。 パラメータパッケージ自体の使用は「パッケージ拡張」と呼ばれ、次のようにコードで記述されます。
Types ...
ここで、Typesはパラメーターパッケージの名前です。
たとえば、このようなテンプレート宣言の場合:
テンプレート <
typename ... Types>
クラス VariadicTemplate
{
};
パラメータのパッケージを展開するための可能なオプションは次のようになります。
class VariadicTemplate:
public Types ...
//基本クラスのリストへの展開。 「パブリックタイプ」-パターン
{
// ...
//別のテンプレートのパラメータのリストに展開します。 パターン-タイプ
typedef OtherVariadicTemplate <タイプ...> OtherVT;
//より複雑なオプション。 パターン-タイプ*
typedef OtherVariadicTemplate <Types * ...> SomeOtherVT;
//関数パラメータのリストに展開します。 パターンはタイプで、引数は新しいパラメーターリストです。
void operator ()(Types ... arg)
{
//関数を呼び出すときに引数リストに展開します
foo(&args ...);
}
//コンストラクターの初期化リストを展開します。
VariadicTemplate():タイプ()...
};
ここでの「パターン」という用語は、対応するパッケージが開かれたときに繰り返されるパラメーターのパッケージの名前を囲むコードを指します。 上記の例では、パラメータを手動で展開すると、テンプレートのインスタンス化が次のようになります。
/ * ... * / VariadicTemplate <
int 、
char 、
double >
/ * ... * /
次のように開示されます。
クラス VariadicTemplate:
public int 、
public char 、
public double
{
// ...
typedef OtherVariadicTemplate <
int 、
char 、
double > OtherVT;
typedef OtherVariadicTemplate <
int *、
char *、
double *> SomeOtherVT;
void 演算子 ()(
int args1、
char args2、
double args3)
{
foo(&args1、&args2、&args3);
}
VariadicTemplate():
int ()、
char ()、
double ()
//明らかに、このコードはそのような型のリストに対してコンパイルされていないことが判明します
};
可変数のパラメーターでテンプレートを使用するかなり単純な例として、ファンクターの実装を使用できます。 この実装は次のようになります。
#include <
iostream >
//関数へのポインタを格納するテンプレートの汎用バージョンを宣言します。 この場合、テンプレートに入ることができるすべての可能なタイプ
//インスタンス化プロセスに、パラメータのパッケージにパックします
テンプレート <
typename ... Args>
struct FunctorImpl;
//単純な関数へのポインタのテンプレートを特化します。 同時に、パラメーターパッケージに戻り値の型が含まれていることを示します
//値(R)と引数(Args)。 これらの2つのパラメーター(単純およびバッチ)から、関数シグネチャを形成します
テンプレート <
typename R、
typename ... Args>
struct FunctorImpl <R(引数...)>
{
//目的のシグネチャを持つ関数へのポインタのタイプを記述します。 この場合、パラメーターのパッケージを開きます
typedef R(* FT)(引数...);
FunctorImpl(FT fn):m_fn(fn){;}
//引数と同じ数のパラメーターを受け取るように、関数呼び出し演算子を宣言します
//格納されている関数タイプ。
R
演算子 ()(引数...引数)
{
//受信したすべての引数を渡す関数を呼び出します
return m_fn(args ...);
}
FT m_fn;
};
//汎用ディスパッチパターンを宣言します
テンプレート <
タイプ名 FT>
struct Functor:
public FunctorImpl <FT>
{
Functor():FunctorImpl <FT>(
NULL ){;}
ファンクター(FT fn):FunctorImpl <FT>(fn){;}
};
int plus_fn(
int a、
int b){
return a + b;}
int minus_fn(
int a、
int b){
return a-b;}
int increment(
int &a){++を
返す ;}
int main()
{
ファンクター<
int (
int 、
int )> plus(plus_fn);
ファンクター<
int (
int 、
int )>マイナス(minus_fn);
ファンクター<
int (
int &)> inc(increment);
std ::
cout << plus(
10、20 )<<
"" << minus(
10、20 )<<
std ::
endl ;
int a =
100 ;
std ::
cout << inc(a)<<
"" ;
std ::
cout << a <<
std ::
endl ;
}
このコードの実行結果は非常に期待されています。
30-10
100 101
また、コードはシンプルで簡単です。 比較のために、boost :: functionの実装を含むファイルを見ることができます。
上記のテンプレートは、メンバー関数へのポインターに特化するのは簡単です。
//メンバ関数へのポインタの関数コンテナの特殊化を宣言し、同じパラメータのパッケージを具体化します
テンプレート <
typename T、
typename R、
typename ... Args>
struct FunctorImpl <R(T :: *)(引数...)>
{
typedef R(T :: * FT)(引数...);
typedef T HostType;
FunctorImpl(FT fn =
NULL 、T * obj =
NULL ):m_fn(fn)、m_obj(obj){;}
//関数呼び出し演算子の2つのバリアントを宣言します-ファンクタが「クロージャ」として使用される場合と、オブジェクトが
//メソッドが呼び出され、最初の引数として渡されます
R
演算子 ()(引数...引数)
{
(m_obj-> * m_fn)(args ...);
}
R
演算子 ()(T * obj、Args ... args)
{
(obj-> * m_fn)(args ...);
}
FT m_fn;
T * m_obj;
};
//メンバー関数が呼び出されるコンストラクター内のオブジェクトを受け取るクロージャークラスを宣言します。 とてもシンプルに見えます。
テンプレート <
タイプ名 FT>
struct Closure:
public FunctorImpl <FT>
{
typedef typename FunctorImpl <FT> :: HostType HostType;
クロージャー(HostType * obj、FT fn):FunctorImpl <FT>(fn、obj){;}
};
//使用
クラス A
{
公開 :
A(
int base =
0 ):m_base(base){;}
int foo(
int a){
return a + m_base;}
プライベート :
int m_base
};
A b1(
10 )、b2;
クロージャ<
int (A :: *)(
int )> a_foo(&b1、&A :: foo);
//全体的なファンクターの実装は、メンバー関数へのポインターでも正しく機能することに気付くかもしれません
Functor <
int (A :: *)(
int )> b_foo(&A :: foo);
std ::
cout << a_foo(
20 )<<
"" << b_foo(&b2、20)<<
"" << b_foo(&b1、20)<<
std ::
endl ;
上記の例は非常に単純で、可変数のパラメーターを持つテンプレートの主な機能を明確に示しています。 分析することにより、可変数のパラメーターを持つテンプレートを使用するための次の一般的なパターンを判断できます。
1.最も一般的なテンプレートが宣言され、その最後のパラメーターはパラメーターのパッケージとして記述されます。 例では、これ
テンプレート <
typename ... Args>
struct FunctorImpl;
2.パラメータパッケージの1つまたは別の部分を指定する、このテンプレートの部分的な専門化が決定されます。 上記の例では、この定義
テンプレート <
typename R、
typename ... Args>
struct FunctorImpl <R(引数...)>
3.場合によっては、特化するときに、パラメーターパッケージが空である可能性があることを考慮する必要があります。 これは、一般的に言えば、許容されます。
テンプレートクラスの場合、パッケージにパックされたパラメーターはパッケージの先頭から指定できることに注意してください。 パッケージの末尾からパラメーターを指定することはできません(パラメーターパッケージはテンプレートパラメーターのリストのみを閉じることができるため)。 テンプレート関数に関するそのような制限はありません。
より複雑なケース
上記のように、パラメーターパッケージには型だけでなく、非型も含めることができます。 例:
//可変数の整数を取るテンプレートを宣言します
テンプレート <
int ... Nums>
struct numspack
{
//サイズが実際に渡される引数の数と等しい静的配列を宣言します
static int m_nums [
sizeof ...(Nums)];
//また、配列内の要素の数を格納する列挙を宣言します
enum {nums_count =
sizeof ...(Nums)};
};
//静的配列を初期化します
テンプレート <
int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums ...};
検証コード:
typedef NumsPack <10、20、30、40、50> Nums_5;
std ::
cout << Nums_5 :: nums_count <<
std ::
endl ;
for (
int n =
0 ; n <Nums_5 :: nums_count; ++ n)
std ::
cout << Nums_5 :: m_nums [n] <<
"" ;
std ::
cout <<
std ::
endl ;
コンソールへの印刷が期待されます
5
10 20 30 40 50
この例に示されているsizeof ...(Nums)コンストラクトは、パッケージ内のパラメーターの数を取得するために使用されます。 その中で、Numsはパラメーターパッケージの名前です。 残念ながら、可変数のパラメーターを使用したテンプレートの設計では、これがパラメーターのパッケージを使用して実行できる唯一の方法です(直接的な開示に加えて)。 たとえば、インデックスからパッケージからパラメーターを取得したり、新しい標準プロジェクトのフレームワーク内でより複雑な操作を実行したりすることはできません。
パッケージを開くときに、より複雑なパターンを適用できます。 たとえば、上記のコードでは、次の置換を行うことができます。
テンプレート <
int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums *
10 ...};
これにより、異なるシーケンスが表示されます。
100200300400500
一般に、特定の種類のパターンは、それが明らかにされるコンテキストに依存します。 さらに、パターンには複数のパラメーターパッケージへの参照が含まれる場合があります。 この場合、パターンに記載されているすべてのパッケージは同期的に開かれるため、それらの実際のパラメーターの数は一致する必要があります。
このような状況は、値のタプルを決定する必要がある場合に発生する可能性があります。 ある引数に対する特定の関数の実行結果をある関数に転送することをタスクとする汎用のファンクターコンポーザーを編成する必要があるとします。 特定の機能セットがあるとします。
double fn1(
double a)
{
*
2を 返します。
}
int fn2(
int a)
{
*
3を 返します。
}
int fn3(
int a)
{
*
4を 返します。
}
そして2つの操作:
int test_opr(
int a、
int b)
{
a + bを
返します。
}
int test_opr3(
int a、
int b、
int c)
{
a + b * cを
返します。
}
そのようなコードの実行につながる関数呼び出し操作のアプリケーションである汎用ファンクターを作成する必要があります。
test_opr(f1(x), f2(x));
または
test_opr3(f1(x), f2(x), f3(x));
ファンクターは、操作および関数のリストを入力として受け入れ、その結果を引数としてこの操作に渡す必要があります。 このようなファンクターの定義のフレームワークは次のようになります。
テンプレート <
タイプ名 Op、
タイプ名 ... F>
クラスコンポジター
{
公開 :
コンポジター(Op op、F ... fs);
};
解決する必要がある最初の問題は、転送された関数を保存する方法です。 これを行うには、特定のタイプのデータを直接格納するクラスからの複数の継承を適用できます。
テンプレート <
タイプ名 T>
struct DataHolder
{
T m_data;
};
テンプレート <
タイプ名 Op、
タイプ名 ... F>
クラス Composer:
public DataHolder <F> ...
{
// ...
};
しかし、ここで最初の問題が発生します-転送された関数のリストに型が一致する関数がいくつかある場合、同じクラスが基本クラスのリストに存在するため、コードはコンパイルされません。 このあいまいさを排除するために、パッケージ内の型にインデックスを付けることができます。 このために、0からパラメーターNとして指定されたものまでの数字を含む補助型「整数のタプル」が使用されます。
//タプル自体のクラスを定義します
テンプレート <
int ... Idxs>
struct IndexesTuple
{
};
//タプルの生成に使用されるテンプレートの一般的なビューを定義します
template <
int Num、
typename Tp = IndexesTuple <>>
struct IndexTupleBuilder;
//整数パラメータのパケットの形式で一連の数値を生成する特殊化を定義します。
//このため、タプルの型自体はテンプレート宣言の2番目のパラメーターとしては使用されませんが、以前に生成されたパラメーターとして使用されます
//パッケージ。 最終的なパッケージを取得するには、生成されたテンプレートから継承し、パッケージに新しい番号を追加します
テンプレート <
int Num、
int ... Idxs>
struct IndexTupleBuilder <Num、IndexesTuple <Idxs ... >>:IndexTupleBuilder <Num-
1 、IndexesTuple <Idxs ...、
sizeof ...(Idxs)>>
{
};
//再帰の特殊化の終了。 目的の数値セットを持つタプルを定義する結果のtypedefが含まれます
テンプレート <
int ... Idxs>
struct IndexTupleBuilder <
0 、IndexesTuple <Idxs ... >>
{
typedef IndexesTuple <Idxs ...>インデックス;
};
その結果、このテンプレートを次のように使用できます。
typedef typename IndexTupleBuilder <
6 >インデックス。
この場合、IndexesはIndexesTuple <0、1、2、3、4、5>と同等になります
このクラスをコンポーザの実装で使用するには、データクラスから継承する中間ベースクラスを導入する必要があります。 さらに、各データクラスには固有のインデックスが提供されます。
テンプレート <
int idx、
typename T>
struct DataHolder
{
DataHolder(T
const &data):m_data(データ){;}
T m_data;
};
//まず、入力としてタプルを受け入れる汎用テンプレートを宣言します。 この形式の広告は直接必要ありませんが、
//後続の特殊化に必要です。
template <
typename IdxsTuple、
typename ... F>
struct ComposerBase;
//タプルからパラメータのパッケージを抽出する一般的なテンプレートに特化しています。
//この場合、テンプレートは2つのパラメーターパッケージで宣言されます。 パケットは一意に分割できるため、これは許可されています。
//継承する場合、2つのパラメーターパッケージが同時に言及されるパターンが使用されます。 これにより、一意に一致することができます
//整数タプルの要素と関数型のリスト。
テンプレート <
int ... Idxs、
typename ... F>
struct ComposerBase <IndexesTuple <Idxs ...>、F ...>:
public DataHolder <Idxs、F> ...
{
//そして、このパターンには、インデックス付きのパッケージ、関数型のパッケージ、引数のパッケージの3つのパッケージが同時に含まれています。 これはすべてリストに表示されます。
//コンストラクタの初期化。
ComposerBase(F ... fs):DataHolder <Idxs、F>(fs)... {;}
};
//実際のデータを含む上記のテンプレートから作曲家のテンプレートを継承します
テンプレート <
タイプ名 Op、
タイプ名 ... F>
struct Composer:
public ComposerBase <
typename IndexTupleBuilder <
sizeof ...(F)> :: Indexes、F ...>
{
Op m_op;
公開 :
//コンストラクターを宣言します
作曲家(Op op、F
const &... fs):m_op(op)、Base(fs ...){;}
};
作曲家の実装を完了するには、関数呼び出し演算子を定義する必要があります。 定義の便宜上、最初に戻り値の型が決定されます。
テンプレート <
タイプ名 Op、
タイプ名 ... F>
struct Composer:
/ * ... * /
{
Op m_op;
公開 :
typedef decltype(m_op((*(F *)
NULL )(
0 )...))result_t;
// ...
};
戻り値の型を決定するために、C ++で新たに追加された別の構造-decltypeが使用されます。 アプリケーションの結果(この場合)は、関数によって返される値の型です。 デザインは少し奇妙に見えます。 意味では、それは同等です
decltype(op(fs(0)...))
ただし、パッケージfsはクラスのスコープで定義されていないため、演算子は参照に変換されたNULL関数型に適用されます。
これで、関数呼び出し演算子を決定する準備がすべて整いました。 合成に関与する関数を格納するクラスは、テンプレートパラメータの1つとして整数インデックスを取るため、この演算子は、同じ整数タプルが渡されるヘルパー関数を介して実装されます。
テンプレート <
タイプ名 Op、
タイプ名 ... F>
struct Composer:
/ * ... * /
{
Op m_op;
公開 :
ret_type
演算子 ()(
int x)
const
{
MakeCall(x、インデックス());
}
プライベート :
//これは、ComposerBaseクラスの定義と同じトリックを使用します。 タプルのタイプは「キャッチ」に使用されます
//整数インデックスのパッケージ
テンプレート <
int ... Idxs>
ret_type MakeCall(
int x、IndexesTuple <Idxs ...>
const &)
const
{
return m_op(DataHolder <Idxs、F> :: m_data(x)...);
}
};
このクラスのインスタンスの作成を容易にする関数を定義するだけです。
テンプレート <
タイプ名 Op、
タイプ名 ... F>
作曲家<Op、F ...>作曲(Op op、F ... fs)
{
return Composer <Op、F ...>(op、fs ...);
}
作曲家の準備ができました。 その使用のいくつかの例:
auto f = MakeOp(test_opr、fn1、fn2);
auto ff = MakeOp(test_opr3、fn1、fn2、fn3);
auto ff1 = MakeOp(test_opr3、fn1、fn2、[=](
int x){
return f(x)*
5 ;});
//ここで、作曲家の最後のパラメーターはラムダ関数です。
作曲家テンプレートクラスの完全な定義は次のとおりです。
テンプレート <
int ... Idxs、
typename ... F>
struct ComposerBase <IndexesTuple <Idxs ...>、F ...>:
public DataHolder <Idxs、F> ...
{
ComposerBase(F ... fs):DataHolder <Idxs、F>(fs)... {;}
};
テンプレート <
タイプ名 Op、
タイプ名 ... F>
struct Composer:
public ComposerBase <
typename IndexTupleBuilder <
sizeof ...(F)> :: Indexes、F ...>
{
Op m_op;
公開 :
typedef ComposerBase <
typename IndexTupleBuilder <
sizeof ...(F)> :: Indexes、F ...> Base;
typedef decltype(m_op((*(F *)
NULL )(
0 )...))result_t;
作曲家(Op op、F
const &... fs):m_op(op)、Base(fs ...){;}
result_t
演算子 ()(
int x)
const
{
Return MakeCall(x、
typename IndexTupleBuilder <
sizeof ...(F)> :: Indexes());
}
プライベート :
テンプレート <
int ... Idxs>
result_t MakeCall(
int x、IndexesTuple <Idxs ...>
const &)
const
{
return m_op(DataHolder <Idxs、F> :: m_data(x)...);
}
};
また、このクラスは、STL(std :: tuple)のタプルに基づいて実装できます。 この場合、DataHolderクラスは必要ありません。 この場合、作曲家の実装は次のようになります。
テンプレート <
タイプ名 Op、
タイプ名 ... F>
クラス TupleComposer
{
Op m_op;
std :: tuple <F ...> m_fs;
公開 :
typedef decltype(m_op((*(F *)
NULL )(
0 )...))result_t;
TupleComposer(Op op、F ... fs):m_op(op)、m_fs(fs ...){;}
result_t
演算子 ()(
int x)
const
{
Return MakeCall(x、
typename IndexTupleBuilder <
sizeof ...(F)> :: Indexes());
}
プライベート :
テンプレート <
int ... Idxs>
result_t MakeCall(
int x、IndexesTuple <Idxs ...>
const &)
const
{
return m_op(
std :: get <Idxs>(m_fs)
(x)...);
}
};
このオプションは少し単純に見えます。
さらにいくつかのトリック
「初期化リスト」のコンテキストでパラメータのパッケージを拡張すると、この場合、完全な式がパターンになる可能性があるため、十分に大きなアクションの自由がプログラマに提供されます。 たとえば、引数として渡された数値の合計は、次のように計算できます。
テンプレート <
typename ... T>
void ignore(T ...){;}
テンプレート <
typename ... T>
int CalcSum(T ... nums)
{
int ret_val =
0 ;
ignore(ret_val + = nums ...);
return ret_val;
}
送信された番号の中に正の番号があるかどうかを確認します。
テンプレート <
typename ... T>
bool HasPositives(T ... nums)
{
bool ret_val =
true ;
無視(ret_val = ret_val && nums> =
0 ...);
return ret_val;
}
しかし、この方法を使用する場合、厳密に言えば、引数の計算の順序が定義されておらず、操作が実行される順序を事前に言えないことを忘れてはなりません。
要約すると、可変数のパラメーターを持つテンプレートは、C ++に表示される非常に強力なツールであると言えます。 現在存在する型リスト(または同様の動作の他のエミュレーション)の明らかな欠点はなく、比較的少量のコードでかなり複雑な概念を表現できます。 この記事で紹介する構成は、現在の標準のフレームワーク内で実行される同様の構成と比較できます(これについては、ソースファイルboost :: bind、boost :: function、boost :: tupleを参照できます)。 しかし、それらにはいくつかの欠点がないわけではありません。 主なものは、パラメータパッケージを展開できるコンテキストの数が限られていることです。 特に、パッケージをラムダ関数内で展開することはできません(対応する要求は標準化委員会に送信されますが、この要求は満たされますか?)、パッケージを式に展開して、次のように記述することはできません:
自動結果= args + ...;
パッケージ要素にはインデックスでアクセスできません。