2009-08-21

lambdaとdecltypeを組み合わせれば、ユニーク型IDの必要ないc_functionが

追記:n2927の提案により、lambda expressionは、sizeof, alignof, decltypeのオペランドに使えなくなっています。理由は、実装が極端に難しくなるからとのこと。従って、以下の内容はobsolateであり、正しいC++0xではありません。(コンパイル時のユニークなIDとしての型は、需要があると思うのに、残念)

5.1.1 p2

The evaluation of a lambda-expression results in an rvalue temporary (_class.temporary_ 12.2). This temporary is called the closure object. A lambda-expression shall not appear in an unevaluated operand (_expr_ Clause 5). [ Note: A closure object behaves like a function object (_function.objects_ 20.7). —end note ]

c_function.hpp

c_functionは、関数オブジェクトでさえも、Cの関数ポインタにしてくれる、とても便利なライブラリだ。ところが、これには一つ問題がある。どのように使うかのコードを示せば、問題が分かるだろう。

typedef void (*func_ptr_type)( char const * ) ;

void caller( func_ptr_type ptr)
{
    ptr("hello,world") ;
    ptr("My hovercraft is full of eels.") ;
    ptr("") ;
}

struct Printer
{
    void operator () ( char const * ptr ) const
    {
        std::cout << "argument is: " << ptr << std::endl ;
    }
} ;

struct Counter
{
    void operator () ( char const * ptr ) const
    {
        std::cout << "argument has " << strlen(ptr) << " character(s)"<< std::endl ;
    }
} ;

int main()
{
    using redshift::base::make_c_function ;
    func_ptr_type c_printer = make_c_function< struct unique_id_type_0, func_ptr_type >( Printer() ) ;
    func_ptr_type c_counter = make_c_function< struct unique_id_type_1, func_ptr_type >( Counter() ) ;

    caller( c_printer ) ;
    caller( c_counter ) ;
}

ごらんのように、caller()はテンプレートではない、いたって普通の関数ポインタをとる、古くさいコードである。ところが、関数オブジェクトを何の問題もなく呼び出しているではないか。一体どうやっているのか。

c_functionの実装では、static変数を使って、関数オブジェクトのポインタを格納している。テンプレートを使えば、複数のstatic変数を使うことが出来るが、問題は、テンプレートのインスタンス化の際には、ユニークなIDとしての型がひつようなのだ。上記では、struct xxxという形で、ユニークな型を渡している。単にユニークなIDとして必要なので、実際のクラスを、どこか別の場所で宣言しておく必要はない。しかし、ユニークな識別子でなければならない。さもなければ、既存のstatic変数が書き換えられてしまう。これは、人間の手で書くには、問題がある。なぜなら、人間のすることは信用すべきではないからだ。ソフトウェアの少なからぬ数のバグが、typoによって生じているというのは、証明する統計データはないものの、間違ってはいないと思う。

ところで、C++0xには、lambdaとdecltypeが入る。これを使えば、上記の面倒なユニークIDとしての型を手書きする必要がなくなると思う。

まずlambdaだが、lambda expressionは、それぞれ、未規定のユニークな型であると規定されている。つまり、別の場所に書いてさえいれば、それぞれのlambda expressionの型は別で、しかもユニークであることが保証されているのだ。

ところが、lambda expressionはあくまでクロージャーオブジェクトである。型ではない。そこで、decltypeが役に立つ。decltypeは、lambda expressionから、その型を得ることが出来る。どんな型になるかは、環境依存だが、ユニークな型であることは、保証されているので、テンプレート引数に使える。

#define MAKE_C_FUNCTION( FUNCTION_TYPE, CALL) \
redshift::base::make_c_function< decltype([]{}), FUNCTION_TYPE>(CALL)

ただ、やはりプリプロセッサが必要になってしまうのはどうしようもない。それならいっそ、処理系によっては、ユニークな文字列や値に置き換えられる、pre-defined macroを提供しているプリプロセッサがある。処理系依存になってしまうが、そういう機能を使っても、目的は達せられる。

それに、c_functionも万能ではない、ユニークな型が必要なので、ループ内で、後で呼び出されるコールバック関数の為に使うと、問題になる可能性もある。

template < typename Iterater >
void init_callbacks( Iterater first, Iterater last, Iterater funcObjs )
{
    for ( ; first != last ; ++first, ++funcObjs )
    {
        first->set_callback(*funcObjs) ;
    }
}

このような例では、set_callback()メンバ関数が関数ポインタしかとらないので、c_functionでラップしたいとしても、不可能である。

結局、templateを使えと啓蒙運動をする方が、よほどマシだということになる。

ただ、lambdaとdecltypeによるユニークな型の生成は、テンプレートメタプログラミングに何かしら役に立つと思う。

No comments: