2011-03-05

Variadic Variadic Templates

テンプレートのおかげで、我々は型をパラメーター化できる。テンプレートは、普通のプログラマーならば、当然使える機能である。


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もあるので、これまた自分で実装する必要はない。

Boost.FunctionTypes

教訓としては、真に優れたプログラマーとは、車輪の再発明をしないプログラマーのことである。ましてや、その再発明された車輪は、往々にして輪ではなかったりするのだ。

No comments: