2011-04-13

C++0xにおけるenable_ifの新しい使い方

現在、BoostのML上で、C++0x上における、興味深いenable_ifの使い方が示されている。簡単にいうと、こうなる。

// Never defined
extern void * enabler ;

template < typename T,
    typename std::enable_if< std::is_arithmetic<T>::value >::type *& = enabler >
void f( T )
{ std::cout << "T is arithmetic" << std::endl ;}

template < typename T,
    typename std::enable_if< std::is_pointer<T>::value >::type *& = enabler >
void f( T )
{ std::cout << "T is pointer" << std::endl ; }

int main()
{
    int arithmetic = 0 ;
    f( arithmetic ) ; // T is arithmetic
    int * pointer = nullptr ;
    f( pointer ) ; // T is pointer
}

このように、enable_ifをNon-type template parameterとして使い、enablerというstaticストレージ上の変数をデフォルト引数に渡してやるのだ。このenablerは、定義する必要はない。これによって、仮引数や戻り値の型といった、どうも使いたくない部分にenable_ifを使わずにすむ。また、以前はenable_ifが使えなかった場合でも、使えるようになる。

しかし、なぜenablerが必要なのか。以下の形ではだめなのか?

typename Reserved = typename std::enable_if<条件式>::type >::type

実は、この形では、オーバーロード出来ないという問題がある。実際に試すと分かりやすい。

template < typename T,
    typename Reserved = typename std::enable_if< std::is_arithmetic<T>::value >::type >
void f( T ) { }

template < typename T,
    typename Reserved = typename std::enable_if< std::is_pointer<T>::value >::type >
void f( T ) { } // エラー、再定義

この二つの関数は、どちらも同じ宣言として扱われる。そのため、オーバーロードはできない。

オーバーロードがふたつだけならば、ゼロ個のテンプレートパラメーターパックを使うという手もあるが、三つ以上のオーバーロード関数があれば、その手は使えない。このenablerを使うテクニックならば、オーバーロードがいくつになっても対応できる。

しかし、なぜvoidへのポインターへのリファレンスなのか。以下の形ではだめなのか。

std::enable_if<条件, int>::type = 0 

これは動くが、ユーザーが誤ってテンプレート実引数を明示的に渡してしまうかもしれないというおそれがある。その点、void &&という型は、まず現実に使われないので、安全である。およそ、void *&が一体どういう型であるかを理解できるようなユーザーであれば、このenable_ifのテクニックも理解できるほどのC++上級者なので、やはり間違えることはない。

さて、このテクニックを応用することで、コンストラクターや型変換関数にも、enable_ifを使うことができる。

// Never defined
extern void * enabler ;

class X
{
public :
    // 引数を10個だけ受け付けるコンストラクター
    template < typename ... Types,
        typename std::enable_if< sizeof...(Types) == 10 >::type *& = enabler >
    X( Types ... args ) { }

    // 数値型へのみ、型変換をする変換関数
    template < typename T,
        typename std::enable_if< std::is_arithmetic<T>::value >::type *& = enabler >    
    operator T() { return static_cast<T>(0) ; }
} ;

int main()
{
    // OK
    X x1( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) ; 
    // エラー、引数が10個ではない
    X x2( 1, 2, 3 ) ;

    int i = x1 ; // OK
    double d = x1 ; // OK

    int * ptr = x1 ; // エラー、数値型ではない
}

現在、このテクニックをBoostで公式にサポートするべきか、議論が行われている。問題は、boost::enablerという識別子を導入するかどうかだ。やはり、

typename boost::enable_if_c< ... >type *& = boost::enabler

という形は、どうも見慣れない。もちろん、およそenable_ifを使うほどの人間は、この意味を理解できるほどのC++上級者であろうから、問題はないのだが、やはり公式にサポートするべきかどうかは議論が分かれる。

このイディオムをサポートするならば、そのための特別なメタ関数、enable_whenを提供するべきだという意見もある。これは、単にnested typeがvoid *&になるだけのものだ。

しかし、Boostのドキュメントでこのテクニックを紹介することには、みな同意しているようだ。公式にenablerを導入するかどうかはともかく、このテクニック自体は、近々ドキュメントに載ると思われる。

2 comments:

SubaruG said...

ゼロ個のテンプレートパラメータパックに関しては、
http://d.hatena.ne.jp/gintenlabo/20110304/1299252248
で書いたように、 * を使えば 任意の個数のオーバーロードが書けますね。

もちろん、これだと本来の引数が Variadic Templates の場合に対応できないですし、
明示的にテンプレート引数を渡された場合は依然として困るので、
紹介された方法のほうが優れているのは確かです。

a said...

>その点、void &&という型
誤記。正しくは void* &