2008-07-16

Boost.Preprocessor Meta Programmingについて

いままで、C++の中で、どうしても避けていたものがある。それはプリプロセッサメタプログラミングだ。理屈としてはこうだ。ご存知のとおり、テンプレートメタプログラミングは、コンパイル時にプログラミングをする、すなわちメタプログラムだ。しかし、ふと気づいてみれば、C++にはプリプロセッサと言うものがある。これはコンパイルの前段階に、単純なテキストとしての置換などを行う機能を提供する。うん、置換? それはひょっとして、利用できないだろうか。いやそんなはずはない。出来る。

とはいえ、プリプロセッサを使って単なる置換以上の意味のあるメタプログラミングをするのは、とても難しい。そこで、簡単にメタプログラミングを行うため、ライブラリを作る必要性が生じる。それがBoostのプリプロセッサライブラリだ。

テンプレートは、我々をして同じようなコードを何度も書かせるという、非人間的な作業から解放してくれる。我々は、vector<char>やvector<int>、と書けばよく、ほとんど記述が同じようなクラス、vector_intやvector_charを書かずにすむ。しかし、ある種のコードは、いまだにこの手のコピペを必要とする。例えば、型としての関数を取るテンプレートクラスを実装したいとする。典型的な例は、Boost.Function や、Boost.FunctionTypes などだ。ここでは、問題を分かりやすくするため、function_traitsなるものを実装したいとする。function_traitsはblobで汚いメタ関数で、型として関数を渡すと、戻り値の型、引数の数、各引数の型(Arg0~ArgN-1まで)を返す。次のように使う。

function_traits< int(char, short, int) >::return_type ; //int
function_traits< int(char, short, int) >::number_of_aritys ; //3
function_traits< int(char, short, int) >::Arg1 ; // short

早速実装しよう。このメタ関数は、普通のメタプログラマならば誰でも書ける、とても簡単で初歩的なものだ。

template < typename Signature >
struct function_traits ;

template < typename R >
struct function_traits< R() >
{
    typedef R return_type ;
    static int const number_of_aritys = 0 ;
} ;

template < typename R, typename T0 >
struct function_traits< R(T0) >
{
    typedef R return_type ;
    static int const number_of_aritys = 1 ;
    typedef T0 Arg0 ;
} ;

template < typename R, typename T0, typename T1 >
struct function_traits< R(T0, T1) >
{
    typedef R return_type ;
    static int const number_of_aritys = 2 ;
    typedef T0 Arg0 ;
    typedef T1 Arg1 ;
} ;

template < typename R, typename T0, typename T1, typename T2 >
struct function_traits< R(T0, T1, T2) >
{
    typedef R return_type ;
    static int const number_of_aritys = 3 ;
    typedef T0 Arg0 ;
    typedef T1 Arg1 ;
    typedef T2 Arg2 ;
} ;

うむ、このコードは非人間的である。まるで機械生成したようなコードだ。何故機械にやらせないのか。

機械的にこのコードを生成するためには、いくつか方法がある。まずは賢いエディタを使って生成する方法がある。あるいは、このコードを生成するためのコードを書く方法もある。しかしこれらは、C++の範囲外の話であり、例えばあらかじめ生成されたコードが、引数を30個までサポートしていたとしても、誰かが引数を31個取る関数を書かない保証は無い。引数の数を設定可能なコードを生成するツールを書いたとしても、ユーザはそのツールを使いこなさなければならない。とても面倒だ。

よろしい、ではプリプロセッサだ。すべてのC++コンパイラはプリプロセッサを持っており、コンパイラ間のプリプロセッサの互換性は、テンプレートに比べればかなり高い。それに、プリプロセッサは、C++コードの中に、直接埋め込める。実に理想的だ。プリプロセッサを使えば、上記のコードは次のように書ける。

#ifndef HITO_FUNCTION_TRAITS
#define HITO_FUNCTION_TRAITS

#include <boost/mpl/int.hpp>
#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/repetition/repeat.hpp>
#include <boost/preprocessor/repetition/enum_params.hpp>
#include <boost/preprocessor/repetition/enum_trailing_params.hpp>

#ifndef FUNCTION_MAX_ARITY
#define FUNCTION_MAX_ARITY 15
#endif // #ifndef FUNCTION_MAX_ARITY

template < typename Signature >
struct function_traits ;

#define HITO_function_typedef(z, n, unused) \
typedef BOOST_PP_CAT(T, n) BOOST_PP_CAT(Arg, n) ;

#define HITO_function_traits(z, n, unused) \
template < typename R BOOST_PP_ENUM_TRAILING_PARAMS(n, typename T) > \
struct function_traits< R ( BOOST_PP_ENUM_PARAMS(n, T) ) > \
{ \
    typedef R return_type ; \
    static int const number_of_aritys = n ; \
    BOOST_PP_REPEAT(n, HITO_function_typedef, ~) \
} ;

BOOST_PP_REPEAT(FUNCTION_MAX_ARITY, HITO_function_traits, ~)

#undef HITO_function_traits
#undef HITO_function_typedef

#endif //#ifndef HITO_FUNCTION_TRAITS

素晴らしい。こんなに短いコードで、引数を15個もサポートしている。もし引数がもっと必要なのであれば、FUNCTION_MAX_ARITYを定義するだけで、引数をいくらでも増やせる。

実際のプリプロセッサの実装はとても面倒なのだが、このように、Boostのプリプロセッサライブラリを使えば、実装を気にする必要がなく、意味に集中してメタプログラミングができる。

とは言ったものの、このコードはとても読みにくい。というより、このまま人間が読むのは不可能である。であるからして、どのコンパイラにもついているであろう、プリプロセスの結果を吐く機能を使えば、どんなコードなのかが分かる。がしかし、このままではわかりづらい。というのも、このプリプロセッサメタコードは、あくまでプリプロセッサであるので、吐くコードは、こんな感じだからだ。VC9の場合。

template < typename R > struct function_traits< R ( ) > { typedef R return_type ; static int const number_of_aritys = 0 ; } ; template < typename R , typename T0 > struct function_traits< R ( T0 ) > { typedef R return_type ; static int const number_of_aritys = 1 ; typedef T0 Arg0 ; } ; template < typename R , typename T0 , typename T1 > struct function_traits< R ( T0 , T1 ) > { typedef R return_type ; static int const number_of_aritys = 2 ; typedef T0 Arg0 ; typedef T1 Arg1 ; } ; template < typename R , typename T0 , typename T1 , typename T2 > struct function_traits< R ( T0 , T1 , T2 ) > { typedef R return_type ; static int const number_of_aritys = 3 ; typedef T0 Arg0 ; typedef T1 Arg1 ; typedef T2 Arg2 ; } ;

これではなにがなんだか分からない。手動でインデントするか、あるいはインデントしてくれるツールに頼らなければならない。これを解決するには、何とかインデントや改行を保つ方法が欲しい。それはまた気が向いたら解説する。どうしても続きが読みたい人はコメントで催促してくれればやる気が出るかもしれない。

追記:
実は、number_of_aritysは後から付け足したもので、まったくテストしていなかった。そのため、プリプロセッサメタプログラミングを使ったコードは正しい結果を出力するが、手動のコピペコードは間違えたままであった。また、プリプロセスされたコードも、実はズルをして、手動で書いたコードの空白と改行を消しただけだったので、そのミスがそのまま残ってしまった。プリプロセス済みのコードも、実際にコンパイラに吐かせた。

2 comments:

Anonymous said...

非人間的なコードを多少なりとも血の通ったものとし、プリプロセッサの威力をより鮮明にするために、number_of_aritys をわざわざ 1 のままにするとは。恐るべき演出だ!

江添亮 said...

素で間違えました。
やはりコードのコピペはいけませんね。