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:
Post a Comment