いまさらVariadic Templatesをまとめてみる

この記事はC++ Advent Calendar 2012の13日目です。

Variadic Templatesとは

C++11ではテンプレートで"."を3つ付けることが出来るようになりました、
こんな感じで。

template <class... Ts>

これで何が出来るのかというと、0個以上の任意のテンプレート引数を受け取れるようになります。このとき、Tsをテンプレートパックと呼びます。
Variadic Templateはテンプレートクラスと関数テンプレートの両方で使うことが出来ます。

  • テンプレートクラスの場合
template <class... Ts>
class hoge;

このように書かかれていれば、

hoge<>
hoge< long >
hoge< int, short >

こんな感じで型を渡すことができるようになります。

また、class...のように型だけでなくint...のような整数を取る事もできて、

template <int... Values>
class huga;

のように書かれていれば、

huga<>
huga< 128 >
huga< 2, 42 >

こういう風に渡すことができます。

  • テンプレート関数の場合
template <class... Ts>
T foo(Ts... args)        // ここでのTは任意の戻り値の型

テンプレート関数で使うと、任意の型で任意の数の引数を取ることが出来るようになります。
例えば、

int i;
double d;
std::string str;

foo( i, d, str );

と、こんな事が出来るようになります。

テンプレートパックの展開

  • テンプレートパックの型を展開

テンプレート引数にclass... Tsがあって、別のテンプレート引数を持つクラスや関数などに渡すときはTs...のように"..."をつけます。テンプレートパックの最初の要素の型を返すメタ関数headとVariadic Templatesだけの関数hogeを例にします。

// using typedefはC++11で導入されたもので、
// typedef A B;の代わりにusing B = A;と書けます。
// 詳しくはググってね。

template <class T, class... Rest>
struct head
{
    using type = T;
};

template <class... Ts>
struct hoge
{
    using type = typename head< Ts... >::type;
};

hogeのTs...の最初の要素がheadのTに渡され、残りがRest...に渡されます。

テンプレートパックを展開するときに別のテンプレートクラス等を適用することもできます。

template <class... Ts>
struct type_list;

template <class T>
struct a_t;

template <class... Ts>
struct hoge
{
   
    using type = type_list< a_t< Ts >... >;
};

としたとき、

std::is_same< 
    typename hoge< int, short >::type, 
    type_list< a_t< int >, a_t< short > > 
>::value;

これがtrueになります。
hogeのtypeがtype_list< a_t< Tsの1つ目の要素の型 >, a_t< Tsの2つ目の要素の型 > >となっていて、a_t< Ts >...がどう展開されるのかがわかると思います。

  • テンプレートパックの値を展開

C言語の可変長引数とは違って、va_listのようなマクロは必要ありません。

テンプレート引数にclass... Ts、関数の引数にTs... argsがあって、別の関数に渡すときはargs...と引数名のほうに"..."をつけます。テンプレートパックの最初の要素の値を返す関数headとVariadic Templatesだけの関数fooを例にします。

template <class T, class... Rest>
T head(T value, Rest...)
{
    return value;
}

// C++11で導入された戻り値の型の後置とdecltypeを知らない場合はググってね
template <class... Ts>
auto foo(Ts... args) -> decltype( head( args... ) )
{
    return head( args... );
}

fooからheadが呼び出され、argsの最初の要素がT valueに渡され、残りはRest...に渡されます。

もちろん既存の関数にテンプレートパックを展開して呼び出すことは可能です。
ただし、テンプレートパックの要素の数と既存の関数の引数の数が一致していることと、テンプレートパックの要素の型と引数の型が一致しているか変換可能でなければなりません。
以下にstd::sqrtを用いた例を示します。

#include <cmath>

template <class... Ts>
double sqrt_ts(Ts... args)
{
    return std::sqrt( args... );
}

int main()
{
    sqrt_ts( 4.0 ); // OK
    sqrt_ts( 4.0, 9.0 ); // NG 数が合ってない
    sqrt_ts( "done-n" ); // NG std::sqrtの引数の型と合ってない
}

テンプレートパックを展開するとき、要素ごとに別の関数を適用することも可能です。

int plus1(int x)
{
    return x + 1;
}

template <class... Ts>
void func(Ts... args)
{
    // printはうまく全部出力してくれる関数
    print( plus1( args )... );
}

このとき、func( 2, 4, 8 );と呼びだすと、func内のplus1( args )...はplus1( 2 ), plus1( 4 ), plus1( 8 )とほぼ一緒となります。plus1(argsの1つ目の要素の値), plus1(argsの2つ目の要素の値), plus1(argsの3つ目の要素の値)という風になっていますね。

さらにテンプレート関数を要素ごとに適用することも可能です。

template <class... Ts>
auto func(Ts... args) -> decltype( f( std::forward< Ts >( args )... ) )
{
    // 打つのが面倒なのでfは適当に定義された関数
    return f( std::forward< Ts >( args )... ); 
}

std::forward<argsの1つ目の要素の型>( argsの1つ目の要素の値 ), std::forward<argsの2つ目の要素の型>( argsの2つ目の要素の値 ),・・・とTsとargsが要素ごとにちゃんと展開されて渡されます。

テンプレートパックの要素の数の取得

sizeof...演算子を用いてテンプレートパックの要素の数を取得できます。

template <class... Ts>
class foo
{
    static constexpr std::size_t value = sizeof...( Ts );
};

template <class... Ts>
void f(Ts... args)
{
    // sizeof演算子と同様に型でも引数名でも
    sizeof...( Ts );
    sizeof...( args );
}

テンプレート・テンプレート・パラメータでVariadic Template

template <template <class...> class T>
struct hoge;

これができます。
Tには、Variadic Templateではないテンプレートクラスを渡すことも可能です。

以下の例では、fooの特殊化でVariadic Templatesなテンプレート・テンプレート・パラメータを用いています。テンプレートクラスならtrue_type、でなければfalse_typeとなります。

#include <iostream>
#include <type_traits>

template <class T>
struct foo :
    public std::false_type
{ };

template <template <class...> class T, class... Ts>
struct foo< T< Ts... > > :
    public std::true_type
{ };

template <class... Args>
struct a_t;

template <class T>
struct b_t;

int main()
{
    std::cout << std::boolalpha << foo< int >::value << std::endl;
    // false

    std::cout << std::boolalpha << foo< a_t< int, short > >::value << std::endl;
    // true

    std::cout << std::boolalpha << foo< b_t< double > >::value << std::endl;
    // true
}

テンプレートパックの要素を順番に処理する

  • 関数の再帰でテンプレートパックの要素の値を順番に

テンプレートパックの要素を順番に処理したいときにどうするかを、
まず以下に示すテンプレートパックの要素の値を順番に足していった合計を返すsum関数を例にして説明します。

#include <iostream>

int sum_impl(int result)
{
    return result;
}

template <class T, class... Rest>
int sum_impl(int result, T value, Rest... rest)
{
    return sum_impl( result + value, rest... );
}

template <class... Ts>
int sum(Ts... args)
{
    return sum_impl( 0, args... );
}

int main()
{
    std::cout << sum( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) << std::endl;
    // 55
}

実際の主役はsum_impl関数です。
再帰を使ってsum関数からresultに0を、argsをsum_impl関数に渡してargsの最初の要素をvalueに渡して、残りをrestに渡します。次に、resultにvalueを足したものを次のsum_impl関数のresultに渡し、rest...でテンプレートパックを展開してrestの最初の要素を次のsum_impl関数のvalueに、restの残りを次のsum_impl関数のrestに渡します。こうすることでsum_impl関数を呼び出すたびにrestの要素が1つずつ減っていき、最終的になくなります。なくなったら上から2番目のsum_impl関数のT valueに入るものはなくなって呼び出せないので、一番上のsum_impl関数が呼び出されて合計を返します。
このように再帰を使うことでテンプレートパックの要素の値を順番に処理することができます。

  • 型だけを順番に

Targetで指定した型と一致する型がいくつTsにあるかをstd::integral_constantで返すcountメタ関数を例に。

#include <type_traits>
#include <iostream>

template <std::size_t Result, class Target, class... Ts>
struct count_impl;

template <std::size_t Result, class Target, class T, class... Rest>
struct count_impl< Result, Target, T, Rest... >
{
    using type = typename count_impl< Result, Target, Rest... >::type;
};

template <std::size_t Result, class Target, class... Rest>
struct count_impl< Result, Target, Target, Rest... >
{
    using type = typename count_impl< Result + 1, Target, Rest... >::type;
};

template <std::size_t Result, class Target>
struct count_impl< Result, Target >
{
    using type = std::integral_constant< std::size_t, Result >;
};

template <class Target, class... Ts>
struct count
{
    using type = typename count_impl< 0, Target, Ts... >::type;
};

int main()
{
    // intがいくつあるか
    std::cout 
    << count<
           int,
           int, short, int, long, int, int, int 
       >::type::value
    << std::endl;
    // 5
}

上から2番目のcount_implはTargetと残りのテンプレートパックの最初の要素が一致しないとき、上から3番目のcount_implは一致したとき、上から4番目のcount_implはテンプレートパックの要素がなくなったときを特殊化していて、count_implの内部でcount_impl::typeすることで再帰になって順番に処理できます。

テンプレートパックの要素をインデックスで

Variadic Templatesを使っているとインデックスでアクセスしたいと思うようになるはずです。

型のリストからインデックスで型を取得するというのは、Boost.MPLのatやat_cが有名ですし、Variadic TemplatesのテンプレートパックをBoost.MPLのコンテナに突っ込めるようにすればBoost.MPLが利用できます。

しかし、ここではあえて自分で作ってみます。
at_cはTsのIndexに指定したインデックスにある型をat_c::typeで返します。

#include <type_traits>
#include <iostream>

template <std::size_t Index, std::size_t I, class... Ts>
struct at_c_impl;

template <std::size_t Index, std::size_t I, class T, class... Rest>
struct at_c_impl< Index, I, T, Rest... >
{
    using type = typename at_c_impl< Index, I + 1, Rest... >::type;
};

template <std::size_t Index, class T, class... Rest>
struct at_c_impl< Index, Index, T, Rest... >
{
    using type = T;
};

template <std::size_t Index, class... Ts>
struct at_c
{
    using type = typename at_c_impl< Index, 0, Ts... >::type;
};

int main()
{
    static_assert( std::is_same< at_c< 2, int, short, long, float, double >::type, long >::value, "" );
}

at_c_implを再帰することでIndexと一致するまでIを1ずつ増やして、Restを1つずつ減らしていくことで実現しています。
もしIndexがTs...の範囲を超えていたらコンパイルエラーとなります。コンパイルエラーにしたくなければ、次のような特殊化を加えれば良いでしょう。

struct null_type;

template <std::size_t Index, std::size_t I>
struct at_c_impl< Index, I >
{
    using type = null_type;
};

null_typeは型がないというのを表すために適当に作った型です。

テンプレートパックの要素の値をインデックスで取得してみましょう。

簡単にするためにTsの要素にはint型の値しかないということにします。
get関数はargsのインデックスnにある値を返すものです。
指定したインデックスがテンプレートパックの範囲を超えていた場合は0を返すとします。

#include <iostream>

int get_impl(std::size_t, std::size_t)
{
    return 0;
}

template <class... Ts>
int get_impl(std::size_t n, std::size_t i, int value, Ts... rest)
{
    return n == i ? value : get_impl( n, i + 1, rest... );
}

template <class... Ts>
int get(std::size_t n, Ts... args)
{
    return get_impl( n, 0, args... );
}

int main()
{
    std::cout << get( 2, 2, 4, 6, 8, 10 ) << std::endl;
    // 6    
}

型の場合と同じく、再帰でnと一致するまでiを1ずつ増やして、restを1つずつ減らしていくことで実現しています。

おわりに

だらだらとVariadic Templatesの説明を行って来ました。
一応Variadic Templatesの基礎の部分は網羅しているはずです。
Variadic Templatesを知っている人は面白くない内容だったと思いますが、私は謝らない。再確認ぐらいに使ってあげてください。

インデックスアクセスの例では直接Variadic Templatesを使っていますが、

at_c< type_list< int, short, long >, 1 >::type // short

のようにインデックスを後ろにしたり別のテンプレートクラスを使って使いやすくするなどの改良が考えられますし、
これに限らずご自身でいろいろお試しください。