2008-01-20

Boostのenable_ifについて

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: