そういえば昔、初めて可変長テンプレート引数を持つクラスを作れます。と本で読んで、さっそく作ってみたはいいものの、その扱いに困った覚えがあります。
今回はそれを扱うための簡単な方法をつらつらと書いていこうと思います。
さて、可変長テンプレート引数を受け取るクラスと言えば基本的にこうなりますね。
template< class... Args >
class something
{
enum { value = sizeof...(Args) };
// using type1 = Ty1;
// using type2 = Ty2;
// ...
};
可変長テンプレート引数に対して行える直接的な操作は、sizeof...演算子によるいくつの型が入っているのか調べることくらいでしょうか。
つまり、具体的な型がそのままでは得られないのです。そして、簡単に得るためのものもSTLにはないのです。
<utility>のstd::tuple_elementとstd::tuple_sizeは、std::tupleに対してしか使えません。リファレンスには、tupleとみなせる型と書いてあるのですが。
つまり、可変長テンプレート引数に対して操作を加えるためには、ほとんどの場合、それ用のメタ関数を定義してやらなければならないのです。
最も基本的な操作は、可変長テンプレート引数の減数です。
すなわち、特殊化によって最初の型とそれ以降を分けることですね。
以下のようにすれば一番最初の型を得ることができます。
// get the first argument
template< class... >
struct first_type // first_type <>
{ // Args is empty
};
template< class Head, class... Body >
struct first_type < Head, Body... > // 最初の1つはHeadに、残りはBodyに入れられる
{
using type = Head;
};
first_type構造体は0個か1個以上かで特殊化されています。
これが最も基本的で簡単な操作である減数です。
もちろん重要なのは特殊化されているほうで、特殊化の方のBody...
はfirst_typeメタ関数が受け取った型の最初の型を除いたものになっています。最初の型はHead
が受け取りますからね。これを減数と呼んでいます。
typename first_type< something< char, short, int > >::type; // char
// typename first_type< something<> >::type; // type is not defined
typename はその後に来る識別子が型であることを明示するためのキーワードです。
template< typename Ty >
という使い方とは全く異なる意味です。ひとつのキーワードに複数の意味があるのはC++の悪習ですね。
hoge::type
が型なのか値なのか、コンパイラには想像がつかないということです。
毎回 typename ~~ ::type
とかくのは面倒でややこしいので、よく使われるものには_tを末尾につけてエイリアステンプレートに包むのが一般的です。
tmeplate< class Ty >
using first_type_t = typename first_type< Ty >::type;
これで最初の型を得ることができるようになりました。
template< class... Args >
class something
{
enum { value = sizeof...( Args ) };
using first_type = first_type_t< Args... >;
};
using sth_1st_type = typename something< char, short, int >::first_type; // char
こんな感じです。
なんとなく見えてきたでしょうか。
ここで、first_typeは文字通り最初の型しか得ることができません。
これを2つ目の型でも3つ目の型でも得られるようにしましょう。
途端に複雑になりますので、ここまででつまずいた方は読み返されることをおすすめします。
覚悟はいいですね。
//
// extended tuple_element
template< std::size_t, class >
struct _type_element
{ // Ty has no template-parameter
};
template<
template< class... > class Ty,
class Head, class... Body >
struct _type_element< 0, Ty< Head, Body... > >
{
using type = Head;
};
template<
std::size_t N,
template< class... > class Ty,
class Head, class... Body >
struct _type_element< N, Ty< Head, Body... > >
{
using type = typename _type_element< N - 1, Ty< Body... > >::type;
};
template< std::size_t N, class Ty >
using type_element = _type_element< N, typename std::remove_reference< typename std::remove_cv< Ty >::type >::type >;
template< std::size_t N, class Ty >
using type_element_t = typename type_element< N, Ty >::type;
まずは使い方を見て頂いて、それからひとつひとつ説明していきます。
using first = type_element_t < 0, something < char, short, int > >; // char
using second = type_element_t < 1, something < char, short, int > >; // short
using third = type_element_t < 2, something < char, short, int > >; // int
ご覧の通り、第一引数に指定した箇所の型を取ってきてくれます。
first_typeを作りたければ
template < class Ty >
using first = type_element_t < 0, Ty >;
とすれば良いのです。
まずは全体の構成から説明させて頂きます。
実装は_type_elementが担っています。
これには少し実用する上での訳がありまして、constやvolatile(cv修飾子)、参照などがあると特殊化が働かず、typelistだと認められないのです。ですが、const something< char, short >
も型リストとみなしても問題はありません。なので、その差を吸収するためのクッションが欲しいので、そのために実装とインターフェースを分け、その間にクッションを挟む形にしています。
ちなみに、remove_***系のメタ関数は<type_traits>ヘッダに入っています。
C++14ではstd::remove_***_tが<type_traits>に追加されるのでtypename ***::typeがいらなくなります。
template < std::size_t N, class Ty >
using type_element = _type_element< N, remove_reference_t < remove_cv_t < Ty > > >;
これによってcv修飾子、参照関係なくこのメタ関数を適用することができます。
続いて、お約束の_t型です。
template< std::size_t N, class Ty >
using type_element_t = typename type_element< N, Ty >::type;
では、細部に入ります。
まず最初、最も基本的な定義です。
これは第二引数が型リストではない場合です。
type_element< 1, char >などとされても困りますからね。
この定義が実体化される場合には基本的にsubstitution faultを起こします。
template< std::size_t, class >
struct _type_element
{ // Ty has no template-parameter
};
2つめです。
ここから特殊化になります。
まず、N==0の場合、つまりfirst_typeの場合ですね。
最初の型が求められているので、ArgsをHeadとBodyに分解し、Headの型を定義します。
first_typeとまるっきり変わらないことがわかります。
template<
template< class... > class Ty,
class Head, class... Body >
struct _type_element< 0, Ty< Head, Body... > >
{
using type = Head;
};
3つめです。
特殊化としては、2つめです。しかも、これで最後です。
N番目の型が求められています。この場合には、どうしたら良いでしょうか。
まず与えられたArgsをHeadとBodyに分解します。
そして、N - 1とBodyを引数として自分自身を再帰的に呼びます。
そうすると、いつかは絶対にNは1になります。
Nが1になると、上の特殊化が呼ばれます。
これで、N番目の型を定義することができます。
template<
std::size_t N,
template< class... > class Ty,
class Head, class... Body >
struct _type_element< N, Ty< Head, Body... > >
{
using type = typename _type_element< N - 1, Ty< Body... > >::type;
};
お疲れ様でした。
最後のは結構解読が大変かと思います。
あまり見慣れないかもしれないテンプレートテンプレートパラメータを使っています。
これについてはテンプレートテンプレートパラメータをお読み頂けると幸いです。
ちなみに、型操作の基本が特殊化による分解なので、ふつうは前から可変長テンプレート引数を処理することになります。
ところが、後ろから可変長テンプレート引数を処理することもできますし、渡されたテンプレート引数の中に特定の型が存在するか調べたり、あるいは渡されたテンプレート引数にcv修飾子を足したりすることもできます。こうしてみると、最初は何もできないと思いがちな可変長テンプレート引数ですが、結構なんでもできることがわかります。