2010-06-15

lambdaでデータメンバーのキャプチャー

まず始めに明言しておくと、lambda式でデータメンバーはキャプチャーできない。lambda式は、ローカル変数しかキャプチャできない。従って、このブログのタイトルは、すでに間違っている。ではなぜこんなタイトルなのか。それは。lambda式では、データメンバーを使うことが出来るからだ。これは、誤解されやすい。

struct C
{
    int x ;
    void f()
    {
        [=]{ return x ; } ;
    }
} ;

これは、完璧にwell-formedなC++0xのコードである。ご覧のように、データメンバーが使える。「ほれ見ろ使えるじゃねぇか。するてぇと、データメンバーもキャプチャーできるに違ぇねぇ」と思われるかもしれない。しかし、データメンバーは、キャプチャーできない。

では、このlambda式は、何をキャプチャーしているのか。thisである。上の例は、以下のように書くこともできる。

struct C
{
    int x ;
    void f()
    {
        [this]{ return this->x ; } ;
    }
} ;

thisは、必ずコピーキャプチャーしなければならない。最も、thisはポインターなので、参照キャプチャーしたとしても、あまり使い道はない。ともかく、デフォルトキャプチャーを指定したlambda式の中で、非staticなデータメンバーを使うと、自動的にthisがキャプチャーされる。これは、[&]でも、同様である。thisを介してのアクセスなので、データメンバーは、参照される。コピーはされない。thisは、必ずコピーキャプチャーされる。しかし、ご存知のように、thisはポインターなので、データメンバーは、リファレンスキャプチャーしたように振舞う。

これは、非常に危険である。皆、[=]は、コピーキャプチャーであり、クロージャーオブジェクトを保持しても安全だと考えている。確かに、コピーされる。何がコピーされるかというと、thisである。

struct C
{
    int x ;
    std::function< int (void) > f()
    {
        return [=]{ return x ; } ;
    }
} ;

int main()
{
    std::function< int (void) > f ;
    {
        C c ;
        f = f.f() ;
    }// cの寿命終了のお知らせ

    f() ;// 実行時エラー、オブジェクトはすでに死んでいる
}

このコードは、cの寿命が尽きた後に、クロージャーオブジェクトを呼び出しているので、実行時エラーになる。

これは、例外の少ない一番シンプルな設計である。この設計に嘘はない。lambda式は、基本的に、ローカル変数しかキャプチャーできない。データメンバーはキャプチャーできない。ただし、thisは特別にキャプチャーできる。したがって、データメンバーは、常に参照でアクセスされる。

では、thisではなく、どうしてもデータメンバー自体をキャプチャーしたい場合はどうするのか。これは、明示的にローカル変数を作ってやるしかない。

struct C
{
    int x ;
    void f()
    {
        auto x = this->x ;
        [=]{ x ; } ;
    }
} ;

もちろん、紛らわしければ、別の名前にしてもいい。

No comments: