テンプレートのおかげで、我々は型をパラメーター化できる。テンプレートは、普通のプログラマーならば、当然使える機能である。
template < typename T > void f( T ) { } struct MyType { } ; int main() { f( 0 ) ; f( 0.0 ) ; f( MyType() ) ; }
また、Variadic Templatesのおかげで、我々は任意の型を任意個、パラメーター化できる。Variadic Templatesは、向上心のあるプログラマーなら、当然使える機能である。
template < typename ... Types > void f( Types ... args ) { } struct MyType { } ; int main() { f( ) ; f( 1, 2, 3, 4, 5) ; f( 0, 0.0, MyType() ) ; }
果たしてしからば、Variadic TemplatesのVariadic化は可能だろうか。つまり、Variadic Variadic Templatesである。
何故そんなものが必要なのか。例えば、任意の型の関数ポインタ―を任意個とる場合を考えてみる。
template < typename ... Types > void pack( Types ... args ) { } void f( int ) { } void g( int, int ) { } void h( float, double ) { } int main() { f( &f, &g, &h ) ; }
これはもちろん動くが、面倒である。それに、この方法では、関数ポインタ―以外にのあらゆる型も、実引数として渡せてしまう。これを何とかしたい。
これは、できるC++プログラマーなら、たやすいことだ。単に型を制限してやればいいのだ。
template < typename ... Types, typename ... ParamTypes > void pack( Types (* ... args)( ParamTypes ... ) ) { }
しかし、これには問題がある。これは、同じ仮引数リストと任意の戻り値の型の関数ポインタ―を任意個とる関数である。
void f( int ) { } void g( int, int ) { } void h( float, double ) { } int main() { pack( &f, &g, &h ) ; // エラー }
関数ポインタ―の、仮引数リストの型が違っていれば、この関数には渡すことができない。
つまり、ここで必要なのは、Variadic Variadic Templatesである。残念ながら、C++0xには、そのような機能はない。
機能がなければ、ユーザーコードでなんとかするしかない。それがC++の精神である。そういうわけで、ちょっと頭のいいC++プログラマーならば、以下の様なコードは簡単に書ける。
template < typename Type0, typename ... ParamTypes0, typename Type1, typename ... ParamTypes1, typename Type2, typename ... ParamTypes2 > void pack ( Type0 ( * arg0 )( ParamTypes0 ... ), Type1 ( * arg1 )( ParamTypes1 ... ), Type1 ( * arg2 )( ParamTypes2 ... ) ) { }
さらに頭のいいメタプログラマーは、このコードに規則性を見出す。規則性のあるコードは、自動で生成することが可能である。とすれば、以下の様なコードが頭に浮かぶのは、メタプログラマーとして当然である。
#include <boost/preprocessor/cat.hpp> #include <boost/preprocessor/enum.hpp> #define EZOE_PP_PARAM_MAX 10 #define EZOE_PP_TEMPLATE_PARAMETER( z, n, data ) \ typename BOOST_PP_CAT( Type, n ), typename ... BOOST_PP_CAT( ParamTypes, n ) #define EZOE_PP_PARAMETER( z, n, data ) \ BOOST_PP_CAT( Type, n ) (* BOOST_PP_CAT( arg, n ) ) ( BOOST_PP_CAT( ParamTypes, n ) ... ) template < BOOST_PP_ENUM( EZOE_PP_PARAM_MAX, EZOE_PP_TEMPLATE_PARAMETER, ~ ) > void pack( BOOST_PP_ENUM( EZOE_PP_PARAM_MAX, EZOE_PP_PARAMETER, ~ ) ) { } #undef EZOE_PP_TEMPLATE_PARAMETER #undef EZOE_PP_PARAMETER
メタコードによって、仮引数の数もコンパイル時に調整できる。しかし、このコードには根本的な問題がある。必ず10個の関数ポインタ―を実引数として渡さなければならないのだ。
デフォルト実引数は使えない。なぜならば、パラメーターパックにデフォルト実引数は使えないからだ。
何ということだ。プログラマーの中でも最上級のメタプログラマーは、ここで敗北せねばならぬのか。
真のプログラマーは、もうそろそろ気がついているであろう。こんな馬鹿げたコードは、もとより必要ないのだ。まず、関数ポインターかどうかのチェックは、static_assertを使えば、こんなに簡単に書ける
template < typename T > struct is_function_pointer : std::conditional< std::is_pointer<T>::value && std::is_function< typename std::remove_pointer<T>::type >::value , std::true_type, std::false_type >::type { } ; constexpr bool check( bool b ) { return b ; } template < typename ... Types > constexpr bool check( bool b, Types ... rest ) { return b && check( rest... ) ; } template < typename ... Types > void pack( Types ... args ) { static_assert( check( is_function_pointer<Types>::value... ), "all argument shall be a type of function pointer" ) ; } void f( int ) { } void g( int, int, int ) { } void h( double, float ) { } int main() { pack( &f, &g, &h ) ; // OK pack( &f, &g, &h, 0 ) ; // static_assertによるコンパイルエラー、intは関数ポインターではない }
また、仮引数の型を知る方法としても、メタプログラミングが使える。ただし、この実装はつまらない規則性のあるコードの羅列になってしまうので、自分で実装する必要はない。Boostの実装を使えばよい。Boostの実装には、is_function_pointerもあるので、これまた自分で実装する必要はない。
教訓としては、真に優れたプログラマーとは、車輪の再発明をしないプログラマーのことである。ましてや、その再発明された車輪は、往々にして輪ではなかったりするのだ。
No comments:
Post a Comment