はじめに
備忘録を兼ねて、C++の「テンプレートの部分特殊化」とその周辺知識をまとめます。
基本的な使い方と、最後の方に応用例(コンパイル時にVectorの深さを検出)を紹介しています。
テンプレートの特殊化
あるクラステンプレートの設計を考える場合に、テンプレート仮引数に当てはめられる型の種類によっては、実装やパフォーマンスの理由から、都合が悪いケースがあります。そういった場合にテンプレートの特殊化を行い、テンプレート仮引数に特定の型が当てはめられたときにだけ使われる、特別な実装を定義することができます。
※特殊化していない、通常のテンプレートを一次テンプレートといいます。
テンプレートの特殊化には、「完全特殊化(または、明示的特殊化)」と「部分特殊化」の2パターンが存在します。
以下の例のように、完全特殊化では、全てのテンプレートパラメータが指定され、部分特殊化では、一部のパラメータを指定します。
// 一次テンプレート
template<class T, class U> class A {...};
// 完全特殊化
template<> class A<int, double> {...};
// 部分特殊化 1
// 最初のパラメータがint型の場合
template<class U> class A<int> {...};
// 部分特殊化 2
// 最初のパラメータが任意のポインタ型の場合
template<class T, class U> class A<T*> {...};
// 部分特殊化 3
// 最初のパラメータが任意のポインタへのポインタ型の場合
template<class T> class A<T**, char> {...};
// 部分特殊化 4
// 最初のパラメータが任意の配列の場合
template<class T> class A<T[], char> {...};
// 部分特殊化 5
// 最初のパラメータがサイズNの配列の場合
// 呼び出すときは、A<int[SIZE]>などとする
template<class T, std::size_t N> class A<T[N], char> {...};
上記の例全てに共通して言えることですが、テンプレートの特殊化を使ったとしても、クラスをインスタンス化する時のインターフェイスが変化することはありません。あくまで、ユーザ側が渡すテンプレート仮引数の型に応じて処理を変化させるだけです。
参考として、テンプレートの部分特殊化(Wikipedia)では、テンプレートに関して、以下のような説明がされています。
テンプレートはメタクラスである。つまりコンパイラに対してどのようなクラスを作るかを指示したある種の抽象データ型であると言える。たとえばテンプレートであるvector(動的配列)をプログラマが使うときには、vector<int>、vector<string>などのようにデータ型を指定して実体化する。実体化されたvectorは、コンパイラの生成したオブジェクトコードの中ではそれぞれ別のコードが生成され、それぞれ別のクラスとして扱われる。
結局、テンプレートの特殊化がやっていることは、コンパイル時に、どのデータ型に対して、どのオブジェクトコードを生み出すかの指示を定義していると考えることができます。
Detection Idiom
Detection Idiomとは、テンプレートの部分特殊化とvoid_tを利用して、ある型が持つ特性や適用可能な操作をコンパイル時に検出する手法です。
下記の例では、同じ型を引数とする==(operator)を実装しているかを検出しています。
template<class, class = std::void_t>
struct is_equality_comparable : std::false_type {};
template<class T>
struct is_equality_comparable<T, std::void_t<decltype(std::declval<const T&>() == std::declval<const T&>())>
: std::true_type {};
以下のことを理解していれば、この例が何をしているかも理解できます。
- std::false_typeとstd::true_typeは、コンパイル時の条件式を扱うために使用される型です。この例では、一次テンプレートが呼び出された場合(==が実装されていない場合)にfalseとなり、部分特殊化されたテンプレートが呼ばれた場合にtrueとなるように定義されています。
※std::false_typeとstd::true_typeもそれぞれ、integral_constantという、基本となる整数型と定数を合わせ,型として整数定数を表すものにエイリアステンプレート(using)で別名を付けたものです。(C++14までであれば) - void_tは、その型引数が全てエラー無く評価できればvoidに、エラーが出たならvoid_tもエラーとなり、SFINAEを起動します。部分特殊化されたテンプレートがエラーとなった場合、そのテンプレートはSFINAEによって候補から除外されます。
- std::declvalは、実際には評価されない文脈(decltypeなど)で使用することで、指定された型に対する特定の演算が可能かどうかを調べるためだけの「型の値」が返ります。
- decltypeは、オペランドで指定した式の型を取得する機能です。
- 複数のテンプレートがある場合は、最も特殊化されているものが選択されるため、上記の例では部分特殊化されたテンプレートが、条件さえ満たせば優先的に選択されます。
応用例1(コンパイル時にVectorのDepthを取得する)
実際の応用例として、こちらのstackoverflowのコードを紹介します。
Question:
多次元のstd::vector配列が与えられたときに、その深さをテンプレートパラメータとして受け取れる関数を作る方法はありますか?
Answer:
これまでの解説を踏まえると、以下のコードの意味がわかるはずです。
再帰関数的な実装となっています。
template<class T, typename = void>
struct depth : std::integral_constant<std::size_t, 0> {};
// C-style arrays
template<class T>
struct depth<T[], void>
: std::integral_constant<std::size_t, 1 + depth<T>::value> {};
template<class T, std::size_t n>
struct depth<T[n], void>
: std::integral_constant<std::size_t, 1 + depth<T>::value> {};
// Standard containers
template<class T>
struct depth<T, std::void_t<typename T::iterator, typename T::value_type>>
:std::integral_constant<std::size_t, 1 + depth<typename T::value_type>::value> {};
int main() {
using T1 = std::list<std::set<std::array<std::vector<int>, 4>>>;
using T2 = std::list<std::set<std::vector<int>[4]>>;
std::cout << depth<T1>(); // Output : 4
std::cout << depth<T2>(); // Output : 4
}
テンプレートの仮引数がvalue_typeとiteratorメソッドを持つクラスの場合には、再帰的にdepthが呼び出され、そうでない場合は一次テンプレートが呼ばれ、value=0が設定されるようになっています。最終的に(depthが呼ばれた回数-1)がvalueに入ることになり、コンパイル時に配列のDepthを知ることができます。
応用例2(コンパイル時に対象クラスがiteratorとvalue_typeのメンバ型を有するかを確認する)
以下のように書けます。
template<class T, class = void>
struct is_container : std::false_type {};
template<class T>
struct is_container<T, std::void_t<typename T::iterator, typename T::value_type>>
: std::true_type {};
おまけ1(テンプレートテンプレート)
テンプレートの仮引数に対して、メタ関数(テンプレートを含む関数)を渡すことができます。
template < template < typename > class C >
struct test {};
template < typename T >
struct A {};
test< A< int > >; //Error
test< A >; //OK
おまけ2(関数テンプレート)
template<class T>
void size_check1(T a, T b){
std::cout << "vec" << std::endl;
size_check(a[0], b[0]);
return;
};
template<>
void size_check1<float>(float a, float b){
std::cout << "scolar" << std::endl;
return;
};
template <typename T, size_t N>
void printDimensions(const T (&a)[N])
{
std::cout << N;
}
template <typename T, size_t N, size_t M>
void printDimensions(const T (&a)[N][M])
{
std::cout << N << "x";
printDimensions(a[0]);
}
テンプレートテンプレートパラメータは、テンプレート型を受け取るが、A<int>を渡した場合、それはテンプレート型ではなくA<int>型になっているのでエラーとなります。
参考
テンプレートの部分特殊化(wikipedia)
Sun Studio 12: C++ ユーザーズガイド
Programming Place Plus C++編【言語解説】 第23章
How can I get the depth of a multidimensional std::vector at compile time?(stackoverflow)
[C++]void_tとその周辺
cppreference.com
cpprefjp - C++日本語リファレンス