2010-03-10

Variadic Templatesの解説

Variadic Templatesである。Variadic Templatesとは、そもそも何か。

Variadic functionというものがある。最も身近な例は、printfである。

int printf ( char const * format, ... ) ;

printf( "number: %d, string: %s", 123, "hello" ) ;

このように、任意の数の引数をとることができる関数を、Variadic functionという。

Variadic Templatesは、任意の数の、テンプレート引数を取れるテンプレートである。

これまで、型安全に任意の数の引数を取る方法はなかった。もちろん、関数はオーバーロードできる。

template < typename T1 > T f( T1 const & t1 ) ;
template < typename T1, typename T2 > T f( T1 const & t1, T2 const & t2  ) ;
template < typename T1, typename T2, typename T3 > T f( T1 const & t1, T2 const & t2, T3 const & t3 ) ;

しかし、こんな関数はとてもではないが、手動で書きたいとは思わない。では、ソースコードを生成するプログラムを書けばいいのだろうか。しかし、その場合、プログラムのビルドや管理が、非常に複雑になる。

プリプロセッサという手が・・・・・・いや、聞かなかったことにしてくれ。

何とかして、任意の数のテンプレート引数を取れればいいのだ。C++0xには、その機能がある。

template < typename ... Types >
struct Foo {} ;


int main()
{
    Foo<>    a ;// テンプレート引数なし
    Foo<int> b ;
    Foo<int, int, char, short, float, double, Foo<bool, unsigned int> > complicated ;
}

少し文法がわかりにくいが、基本的には、...を使うようになっている。ここで、Typesを、テンプレートパラメーターパック(Template parameter pack)と呼ぶ。最初の例のように、テンプレート引数を一切とらないこともできる。

関数の場合。

template < typename ... Types >
void f( Types ... args )
{
}


int main()
{
    f(0) ;
    f(1,2,3) ;
    f(1,2,3,4,5,6,7,8,9) ;
    f(0.1, 0.2, 0.3) ;
    f('a', 'b', 'c') ;
    f(1, 1u, 1l, 1ul, 1.0f, 1.0) ;

}

この場合、argsを、関数パラメーターパック(function parameter pack)と呼ぶ。

なるほど、しかし、一体どうやって、このパックとやらを使えばいいのか。パックは、以下のようにして、他の関数に渡せる。

template < typename ... Args >
struct Foo
{
    Foo(Args... args) {}
} ;

template < typename T1 >
void g( T1 const & t1 ) {}
template < typename T1, typename T2 >
void g( T1 const & t1, T2 const & t2  ) {}
template < typename T1, typename T2, typename T3 >
void g( T1 const & t1, T2 const & t2, T3 const & t3 ) {}

template < typename ... Args >
void f( Args ... args )
{
// function parameter packを渡す。
    g( args... ) ;

// template parameter packを渡す。
    Foo< Args... > foo( args... ) ;
}


int main()
{
    f(0) ;
    f(1, 2) ;
    f(1, 2, 3) ;
}

このように、...を使うことによって、コンパイル時に、引数を展開できる。

細かいルールは、山ほどあるが、基本的に、Variadic Templateというのは、これだけの機能である。文法が少し分かりにくいが、それほど難解というわけでもない。

しかし、これはどうやって使えばいいのか。いくら任意の数の引数を取れると言っても、ここの引数にアクセス出来ないのでは、意味がないではないか。

それには、メタプログラミングが用いられる。

たとえば、std::minを実装してみよう。

template < typename T >
T min( T const & a, T const & b )
{
    return a < b ? a : b ;
}

template < typename T, typename ... Types >
T min( T const & head, Types ... tail )
{
    return min( head, min( tail... ) ) ;
}

int main()
{
    std::cout << min(3131, 2232, 444, -3213, 2313 ) << std::endl ;
}

何のことはない。単なる再帰である。メタプログラミングというほどのこともないのだ。

ところで、この関数には、一つ欠点がある。もし、異なる型を渡した場合のエラーメッセージが、非常に分かりにくいのだ。

min(1.0, 2) ;

gccは、以下のようなエラーを返す。

In function 'T min(const T&, Types ...) [with T = int, Types = {}]':
47:38:   instantiated from 'T min(const T&, Types ...) [with T = double, Types = {int}]'
52:15:   instantiated from here
47:38: error: no matching function for call to 'min()'

これでは、何が何だかさっぱりわからない。型が違う場合でも比較するという手もある。しかし、型が違う場合は、コンパイルエラーになって欲しい。それに、もっとわかりやすいエラーメッセージを出したい。そこで、メタプログラミングである。

// forward decralations for min()
template < typename T >
T min( T const & a, T const & b ) ;

template < typename T, typename ... Types >
T min( T const & head, Types ... tail ) ;

// metafunction 
// call min if cond is true.
template < bool cond >
struct invoke_min
{
    template < typename T, typename ... Types >
    static T invoke( Types ... args )
    {
        return min( args... ) ;
    }
} ;

template <>
struct invoke_min<false>
{
    template < typename T, typename ... Types >
    static T invoke( Types ... args ) ;// no definition.
} ;


// has_same_types's implementation.
template < typename T, typename Seq, std::size_t I >
struct has_same_types_impl
{
    static bool const value =
        std::is_same< T, typename std::tuple_element< I, Seq >::type> ::value
        ? has_same_types_impl< T, Seq, I - 1 >::value : false ;
} ;

template < typename T, typename Seq >
struct has_same_types_impl< T, Seq, 0 >
{
    static bool const value =
        std::is_same< T, typename std::tuple_element< 0, Seq >::type >::value ;
} ;

// metafunction
// return true if template parameter pack has same types.
// false otherwise.
template < typename T, typename ... Types >
struct has_same_types
    : has_same_types_impl< T, std::tuple< Types... > , sizeof...(Types) -1 >
{ } ;


template < typename T >
T min( T const & a, T const & b )
{
    return a < b ? a : b ;
}

template < typename T, typename ... Types >
T min( T const & head, Types ... tail )
{
    bool const check =  has_same_types< T, Types... >::value ;
    static_assert( check, "error: type of min() arguments are not the same!" ) ;

    return min( head, invoke_min< check >::template invoke<T>( tail... ) ) ;
}


int main()
{
    std::cout << min(1, 1, 13131, -433, 323, /*It's double!*/11.0, -133, 0 ) << std::endl ;
}

gccでのコンパイルメッセージは、以下のようになる。

gcc_test.cpp: In function 'T min(const T&, Types ...) [with T = int, Types = {int, int, double, int, int, int, int}]':
gcc_test.cpp:111:60:   instantiated from here
gcc_test.cpp:103:5: error: static assertion failed: "error: type of min() arguments are not the same!"

もっとあっさり実装できる予定だったのだが、どうも長くなってしまった。invoke_minを使って、コンパイル時分岐を行わないと、延々とコンパイル時にmin()の再帰インスタンス化が起こってしまう。これは、Variadic Templatesが、0個の引数を受け付けるためである。分かりにくいが、仕方がない。

SFINAEを使えば、もっと簡単に書けた。

// has_same_types's implementation.
template < typename T, typename Seq, std::size_t I >
struct has_same_types_impl
{
    static bool const value =
        std::is_same< T, typename std::tuple_element< I, Seq >::type> ::value
        ? has_same_types_impl< T, Seq, I - 1 >::value : false ;
} ;

template < typename T, typename Seq >
struct has_same_types_impl< T, Seq, 0 >
{
    static bool const value =
        std::is_same< T, typename std::tuple_element< 0, Seq >::type >::value ;
} ;

// metafunction
// return true if template parameter pack has same types.
// false otherwise.
template < typename T, typename ... Types >
struct has_same_types
    : has_same_types_impl< T, std::tuple< Types... > , sizeof...(Types) -1 >
{
    static_assert( has_same_types::value, "types of min() argument are not same!" ) ;
} ;

template < typename T >
T min( T const & a, T const & b )
{
    return a < b ? a : b ;
}

template < typename T, typename ... Types >
typename std::enable_if< has_same_types< T, Types... >::value , T >::type
min( T const & head, Types ... tail )
{
    return min( head, min( tail... ) ) ;
}

このメタプログラムの根本的な問題が、ようやくわかった。しかしどうしたものか。ようするに、static_assert後もinstantiationが続くのが問題なのだ。やはり、enable_ifでSFINAEを利用し、オーバーロードの候補から外してしまうのが、一番良い方法か。

No comments: