2008-01-16

BoostのMPLへのいざない part 2

 Boostのチュートリアルでは、STLのiter_swapを実装しているけれど、同じにするのでは能がない。ここでは、STLのcopyのようなものの実装を通して、型計算を学ぶことにしよう 2 型計算 2.1 君に仕事だ  君は、とあるプロジェクトで働くことになった。君に割り当てられた仕事は、copy_and_sumという関数を実装することだ。これは、STLのcopyのように振舞いつつ、コピーする値をすべて加算して、合計を返すという関数だ。次のように使う。
int main() { std::vector<int> src, dest(3) ; src.push_back(1) ; src.push_back(2) ; src.push_back(3) ; int ret = copy_and_sum( src.begin(), src.end(), dest.begin() ) ; std::cout << ret << std::endl ; // 6 std::cout << dest[0] << dest[1] << dest[2] << std::endl ; // 123 }
2.2 早速実装しよう  優秀な君ならば、ものの5分で書けるだろう。ちょろい仕事だ。さて、さっそくにも実装しようとしたが、問題がある。
template < typename InputIterator, typename OutputIterator > T copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) ;
 いったい、どうやって戻り値の型、Tを指定すればいいのだろう。引数の型は、イテレータなのだ。値とは、イテレータを参照したときの型だ。しかし君は迷わない。ド素人ならいざ知らず、君はSTLを呼吸している男だ。だからこそ、このプロジェクトにも抜てきされたのだ。君は、すべてのイテレータには、value_typeという型がネストされていることを知っている。ソレを使えばよい。
template < typename InputIterator, typename OutputIterator > typename InputIterator::value_type copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename InputIterator::value_type value_type ; value_type ret = value_type() ; while ( first != last) { ret += *first ; *result = *first ; ++first ; ++result ; } return ret ; }
 C++を呼吸している優秀な君は、Dependant Nameを型として扱う場合には、typenameをつけなければならないということをも、当然知っている。君はこの美しいまでに完璧なコードをコミットした。 2.3 問題発生  ところが、君の同僚が、次のような文句をつけてきた。以下のコードがコンパイルできないというのだ。
void f( int * first, int * last, int * dest) { int ret = copy_and_sum(first, last, dest) ; }
 これは問題だ。intのポインタ型には、ネストされた型など存在しない。一体どうすればいいのだろう。 2.4 長い長い道のり  優秀なプログラマであり、現在はMS社で働いている、Butler Lampsonは言った。   "We can solve any problem by introducing an extra level of indirection."   「あらゆる問題は、ラッパをかませば解決する」と。  この言葉は、かのAndrew Koenigも、この考えが好きで、彼の著作、RUMINATIONS on C++では、難しい問題をクラスで包み、参照カウントを使って解決している。  int型のポインタにvalue_typeなどというネストされた型を付け加えることはできないが、そのようなテンプレートクラスを作ることはできる。幸いにも、この考え方は実に一般的で、STLには、iterator_traitsというクラスがある。コレを使えば、次のようにコードを書き直すことができる。
template < typename InputIterator, typename OutputIterator > typename std::iterator_traits<InputIterator>::value_type copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; while ( first != last) { ret += *first ; *result = *first ; ++first ; ++result ; } return ret ; }
 これがいわゆる、ラッパだ。ネストされたvalue_typeを直接使う代わりに、iterator_traitsで包み、差を許容する。iterator_traitsは、部分的特殊化(partial specialization)をつかい、すべてのポインタに対して、value_typeなどを提供している。また、そのほかにも、 2.5 メタ関数という概念  ここでは概念の説明をするが、すこし眠くなりそうな話なので、コーヒーなどをすすりつつ読んでほしい。  ここで、前回のbinaryやiterator_traitsと、普通の関数との間に、何か似たような性質を感じないだろうか。普通の関数は、実行時に、引数とともに呼び出され、値を返す。一方、iterator_traitsは、コンパイル時に、型を引数として渡され、型を返す。binaryでは、コンパイル時の定数として、値を返すことができた。これらの性質から、関数に似ているといえる。このテンプレートクラスのことを、メタ関数(Metafunction)と呼ぶことにしよう。メタ関数、コレは重要な概念である。メタ関数には、次のような特色がある。  ・特殊化(Specialization)    ある型に対して、別定義のクラスを用意することができる。一種の条件分岐だ。とくに、部分的特殊化(partial specialization)をつかえば、すべてのポインタに対する条件分岐をさせることができる。  ・多値を返す(Multiple "return values")    C++では、普通の関数は、ひとつしか値を返せないが、メタ関数は、複数の値を返すことができる。単に、複数のtypedefを作ればいい。  Boostでは、多値を返すメタ関数を、Blob(ある映画由来の造語)として敬遠している。なぜならば、多くの戻り値があるということは、それだけテンプレートの実体化(instantiate)が面倒になり、コンパイルが遅くなるからだ。必要な戻り値だけを定義すべきだ。  また、ポリモーフィズムの観点から言っても、アンチパターンである。そこで、Boostのライブラリでは、メタ関数は次のようになっている。
metafunction-name<type-arguments ...>::type
 メタ関数とは、明示的に呼び出す(invoke)必要がある。呼び出す(invoke)とは、テンプレートを実体化(instanciate)することである。実体化(instanciate)するためには、クラス内部の型や定数を参照しなければならない。
// Point of Declaration template < typename T > struct metafuncion ; template < typename T > struct metafunction { typedef T type ; } ;//Point of Definition void f() { typedef metafunction<int> mf ; // ここでは実体化されない typedef metafunction<mf> mmf ; //ここでも実体化されない typedef mmf::type::type type ; // Point of Instanciation }
 Boostでは、メタ関数とは、ネストされた型typeによって呼び出す(invoke)ことができるものをさす。 2.6 数値メタ関数  メタ関数は、数値を返すこともできる。文法は、次のとおりである。
metafunction-name<type-arguments ...>::type::value
 いくつかのメタ関数は、typeでメタ関数を呼び出さなくても、直接valueを持っている。コレは主に、コードを簡潔にするためだ。 2.7 コンパイル時の選択  さて、仕事に戻ろう。iterator_traitsを使った、copy_and_sumをコミットした君の元へ、ベンチマークを取っている同僚から、バグ票があがってくる。なんと、copy_and_sumが遅いというのだ。話を聞くと、int型の配列で、まず合計値を計算し、つぎにmemcpyを使ってコピーした場合と、copy_and_sum関数を使った場合を比較すると、copy_and_sum関数が1.5倍ほど遅いらしい。  コピーするコードは、この上もなくシンプルなので、これ以上改良しようがない。おそらく、コンパイラベンダーは、memcpyに特殊な最適化を施しているのだろう。しかし、copy_and_sumの実装で、memcpyを使うわけには行かない。なぜならば、memcpyを使えるのは、ビットワイズコピーが許される、POD(Plain old data)型に限られているからだ。  ここで、コンパイル時に、選択をする必要に迫られる。もし、InputIteratorとOutputIteratorの型が同じで、int *型や float *型といった型であれば、memcpyを使ってもよいだろう。それ以外は、依然と同じように処理するのだ。この型を判断するには、テンプレートの特殊化を使い、int型やfloat型などのときだけ、trueを返すようなメタ関数を作ってやればよい。
template < typename T > struct is_it_ok_to_memcpy { static bool const value = false ; } ; template < > struct is_it_ok_to_memcpy< int > { static bool const value = true ; } ; template < > struct is_it_ok_to_memcpy< unsigned int > { static bool const value = true ; } ; template < > struct is_it_ok_to_memcpy< float > { static bool const value = true ; } ; void f() { is_it_ok_to_memcpy< int >::value ; // true is_it_ok_to_memcpy< float >::value ; // true is_it_ok_to_memcpy< std::vector<int> >::value ; // false }
 しかし、必要になるたびに、いちいち作っていくのは面倒だ。幸いにして、Boostには、type_traitsという便利なライブラリがある。コレを使おう。すぐにもこのように書きたいところだが、それはできない。
template < typename InputIterator, typename OutputIterator > typename std::iterator_traits<InputIterator>::value_type copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; typedef boost::remove_cv<InputIterator>::type itype ; typedef boost::remove_cv<OutputIterator>::type otype ; typedef boost::remove_pointer<InputIterator>::type irptype ; if ( boost::is_same<itype, otype>::value && boost::is_arithmetic< irptype >::value ) { memcpy(result, first, sizeof(irptype) * static_cast<size_t>(last - first) ) ; while ( first != last ) { ret += *first ; ++first ; } } else { while ( first != last ) { ret += *first ; *result = *first ; ++first ; ++result ; } } return ret ; }
 何が悪いのか。このコードでは、if文の両方の分岐がコンパイルされてしまう。普通のイテレータは、void *型に変換できないので、コンパイルエラーとなる。ここでは、片方だけがコンパイルされるような工夫が必要だ。そこで、次のようになる。
template < bool b > struct copy_and_sum_impl { template < typename InputIterator, typename OutputIterator > static typename std::iterator_traits<InputIterator>::value_type do_it ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; while ( first != last ) { ret += *first ; *result = *first ; ++first ; ++result ; } return ret ; } } ; template < > struct copy_and_sum_impl< true > { template < typename InputIterator, typename OutputIterator > static typename std::iterator_traits<InputIterator>::value_type do_it ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; typedef boost::remove_pointer<InputIterator>::type irptype ; memcpy(result, first, sizeof(irptype) * static_cast<size_t>(last - first) ) ; while ( first != last ) { ret += *first ; ++first ; } return ret ; } } ; template < typename InputIterator, typename OutputIterator > typename std::iterator_traits<InputIterator>::value_type copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) { typedef boost::remove_cv<InputIterator>::type itype ; typedef boost::remove_cv<OutputIterator>::type otype ; typedef boost::remove_pointer<InputIterator>::type irptype ; bool const b = boost::is_same<itype, otype>::value && boost::is_arithmetic< irptype >::value ; return copy_and_sum_impl< b >::do_it(first, last, result) ; }
 分かりにくい? それならば、これならどうか。
struct oridinary_impl { template < typename InputIterator, typename OutputIterator > static typename std::iterator_traits<InputIterator>::value_type do_it ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; while ( first != last ) { ret += *first ; *result = *first ; ++first ; ++result ; } return ret ; } } ; struct memcpy_impl { template < typename InputIterator, typename OutputIterator > static typename std::iterator_traits<InputIterator>::value_type do_it ( InputIterator first, InputIterator last , OutputIterator result ) { typedef typename std::iterator_traits<InputIterator>::value_type value_type ; value_type ret = value_type() ; typedef boost::remove_pointer<InputIterator>::type irptype ; memcpy(result, first, sizeof(irptype) * static_cast<size_t>(last - first) ) ; while ( first != last ) { ret += *first ; ++first ; } return ret ; } } ; template < typename InputIterator, typename OutputIterator > typename std::iterator_traits<InputIterator>::value_type copy_and_sum ( InputIterator first, InputIterator last , OutputIterator result ) { typedef boost::remove_cv<InputIterator>::type itype ; typedef boost::remove_cv<OutputIterator>::type otype ; typedef boost::remove_pointer<InputIterator>::type irptype ; return boost::mpl::eval_if < boost::mpl::and_< boost::is_same<itype, otype> , boost::is_arithmetic< irptype > > , memcpy_impl , oridinary_impl >::do_it(first, last, result) ; }
 MPLを使えば、このような記述が可能だ。

No comments: