Boostには、enable_ifというメタ関数がある。このメタ関数の実装は、実はとても短い。とても短いので、分かりやすい。
template < bool B, class T = void > struct enable_if_c { typedef T type; } ; tempate < class T > struct enable_if_c< false, T > {} ; template < class Cond, class T = void > struct enable_if : public enable_if_c< Cond::value, T > {} ;
きわめてシンプルだ。なお、これの逆をする、disable_ifなるメタ関数もある。まず、語るよりも、例を示そうと思う。そのほうが分かりやすいだろう。
例えば、ある関数の呼び出しを、組み込みの整数型に限りたい場合は、どうすればいいだろう。C++では、関数のオーバーロードをサポートしている。
int f(int x) ; unsigned int f(unsigned int x) ; short f(short x) ; unsigned short f(unsinged short x) ; //以下略
何て面倒なんだろう。それぞれの関数ごとに、同じようなコードを何度も何度も書かなければならない。そこで、テンプレート関数というものがある。テンプレートを使えば、このような無駄な記述は省ける。
template < typaname T > T f(T x) { return x << 5 ; //ビット演算を使う。 }
これは、すばらしい。しかし、もしこの関数を整数型の呼び出しに限定したい場合は、どうすればいいのだろう。そこで、enable_ifが役に立つ。
template < typaname T > T f( T x, typename boost::enable_if< boost::is_arithmetic<T> >::type * = 0 ) { return x << 5 ; //ビット演算を使う。 }
enable_ifは、一つ目の型引数のメタ関数の戻り値が真の場合、二つ目の型引数を返す。もし偽であったならば、型は定義されない。しかし、C++の規格では、コンパイルエラーにはならない。なぜならば、C++にはSFINAE(Substitution Failure Is Not An Error)という規格がある。このため、単に関数が、Overload Resolutionの候補から外れるだけだ。ほかには、次のような使い方もある。
//戻り値の型として使う template < typaname T > typename boost::enable_if< boost::is_arithmetic<T>, T >::type f( T x ) { return x << 5 ; //ビット演算を使う。 } template < typename T, typename Enable = void > class Foo ; //整数型にだけ特殊化 template < typename T > class Foo< T, typename boost::enable_if< boost::is_arithmetic<T> >::type > ;
しかし、依然としてis_arithmeticのようなメタ関数を書かなければならないことに変わりはないし、何の利点があるのか、と思うかもしれない。その場合は、STLのvectorを実装してみるといい。
STLのvectorには、いくつかのコンストラクタがあるが、そのうち、イテレータを引数に取るコンストラクタと、要素数を初期値を引数に取るコンストラクタがある。
void f(int * first, int * last) { std::vector<:int> v(first, last) ; // vectorはイテレータで初期化される std::vector<:int> v(10, 123) ; // vectorは要素数10で、初期値が123 }
とても便利だ。さて、早速実装しよう。話を簡単にするために、詳細な実装は省き、コンストラクタだけを定義してみる。
template < typename T > class vector { public : vector( unsigned int n, T val = T() ) { T x = val ; } template < typename Iterator > vector( Iterator first, Iterator last ) { T x = *first ; } } ;
さて、さっそくテストしてみよう。ところが、次のコードがコンパイルできないという文句が、大量に殺到して、君のgmailアカウントが容量オーバーになってしまった。
std::vector<int> v(10, 123) ;
なぜか、イテレータを引数にとるコンストラクタが呼ばれてしまう。この理由を説明するには、Overload Resolutionの規格を説明しなければならない。それを説明しだすと長いので、ここでは説明しないが、とにかく、オーバーロードの解決は、テンプレートの実体化が終わった後に行われるということだ。この場合、次の二つの候補がある。
//イテレータ vector(int first, int last) ; //要素数と初期値 vector(unsigned int n, int val) ;
なぜこうなるかというと、10とか、123などといったリテラルの型は、int型だからだ。さて、いったいどちらの関数が呼ばれるのが、自然だろうか。要素数と初期値をとるコンストラクタは、int型からunsigned int型への変換が必要だ。すると、変換せずとも呼べるほうがよい。そこで、イテレータ版のコンストラクタが呼び出される。めでたしめでたし。
そう、悪いのはクラスを書いた俺じゃない。ライブラリのユーザの、C++の規格について、理解が浅いのが原因だ。次のように呼び出していれば、イテレータの方は呼び出されないのだ。
std::vector<int> v(10u, 123) ;
注意深く観ると、一つ目の引数の後ろに、uがついている。これは、リテラルがunsigned型であることを明示している。テンプレート関数と、普通の関数が重なった場合、普通の関数が優先されるルールがあるので、これで望みの動作が得られる。ユーザは文句を言う前に、ちゃんと型を考えるべきだったのだ。どっとはらい。
と、ここで話は終わらない。相変わらず、君の二つ目のgmailアカウントは容量オーバーのままだ。ここで必要とされているのは、なんとかテンプレート関数が、Overload Resolutionの候補に挙がるのを、制限する方法だ。ユーザがいちいち、引数の型がsignedかunsignedか考えなければならないのは、苦痛極まりない。そこでdisable_ifの出番だ。
template < typename T > class vector { public : vector( unsigned int n, T val = T() ) { T x = val ; } template < typename Iterator > vector( Iterator first, Iterator last , boost::disable_if< boost::is_integral<Iterator> >::type * = 0) { T x = *first ; } } ;
これで、望みどおりの動作が得られる。ユーザは何もする必要がない。いちごさけた
No comments:
Post a Comment