2014-09-30

C++14の新機能: 初期化lambdaキャプチャー

C++14で追加された新機能を解説していくシリーズ第四弾。今回は、初期化lambdaキャプチャーを解説する。これは、提案では、汎用lambdaキャプチャーと呼ばれていたが、どうやら今は、もっとわかりやすい、初期化lambdaキャプチャーとも呼ばれているようだ。

C++14の初期化lambdaキャプチャーについて解説する前に、まず、C++11のlambdaキャプチャーと、その問題点について解説しなければならない。

lambda式は、自動ストレージ上にあるオブジェクトか、thisをキャプチャできる。

void f()
{
    int x = 0 ;

    // コピーキャプチャ
    [=]{ return x ; } ;
    // リファレンスキャプチャ
    [&]{ x = 1 ; } ;
}

lambda式は、クロージャーオブジェクトという未規定の型のオブジェクトを生成する。lambda式がローカル変数をキャプチャするのは、魔法ではなく、単なる関数オブジェクトのシンタックスシュガーに過ぎない。

たとえば、以下のようなlambda式には、

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

以下のようなクロージャーオブジェクトが生成される。

class closure_object
{
    int const x ;
public :
    closure_object( int x )
        x(x) { }
    int operator()
    {
        return x ;
    }
} ;

以下のようなlambda式には、

void f()
{
    int x = 0 ;
    [&]{ return x ; } ;
}

以下のようなクロージャーオブジェクトが生成される。

class closure_object
{
    int & x ;
public :
    closure_object( int & x )
        x(x) { }
    int operator()
    {
        return x ;
    }
} ;

これをみると、lambda式が魔法でも何でもなく、従来のC++でもできたことの、シンタックスシュガーに過ぎないことが分かるだろう。

では、何が問題なのか。C++11のlambdaキャプチャーには、二つの問題がある。

データメンバーのキャプチャーができない。

C++11のlambda式は、クラスのデータメンバーのキャプチャーができない。と、こう書くと、読者は以下のようなコードが手持ちのコンパイラーでコンパイルでき、その実行結果も意図通りであることを持って反論するかもしれない。

struct X
{
    int data_member = 0 ;

    void f()
    {
        auto l = [=](){ return data_member ; } ; 
        l() ;
    }

} ;

int main()
{
    X x ;
    x.f() ;    
}

なるほど、たしかにこのコードを読むと、lambda式がデータメンバーをキャプチャーしているように見える。しかし、実際にはdata_memberをキャプチャーしているわけではない。lambda式がキャプチャーしているのは、thisである。data_memberはthis経由で使われているに過ぎない。上のlambda式によって生成されるクロージャーオブジェクトは、以下のようになる。

class closure_object
{
    X * this_ptr ;
public :
    closure_object( X * this_ptr )
        : this_ptr(this_ptr) { }

    int operator ()
    {
        return this_ptr->data_member ;
    }
} ;

data_memberはthisポインター経由でアクセスしているだけで、data_memberをキャプチャーしているわけではない。data_memberはthisポインター経由の間接参照をされるため、コピーされていない。したがって、以下のコードは正しく動かない。

struct X
{
    int data = 0 ;
    void get_lambda()
    {   // コピーキャプチャーしているつもり
        return [=](){ return data ; }
    }
} ;

int main()
{
    std::function< int() > f ;

    {
        X x ;
        f = x.get_lambda ;
    }// xは破棄されている

    f() ; // 挙動は未定義
}

データメンバーはthisポインターを経由して間接参照されているため、thisの参照先が破棄された後では、挙動は未定義となる。

したがって、データメンバーをコピーキャプチャーするには、以下のようにマヌケで冗長な記述をしなければならない。

struct X
{
    int data = 0 ;
    void get_lambda()
    {
        int data = this->data ;
        return [=](){ return data ; }
    }
} ;

ムーブキャプチャーができない。

C++11のlambda式では、変数をムーブしてキャプチャーすることができない。

auto f()
{
    std::vector<int> v(1000) ;

    return [=]() { return std::make_pair( v.begin(), v.end() ) ; }
}

この例で、vは関数fから呼び出し元に戻れば破棄されるオブジェクトである。しかし、lambda式のクロージャーオブジェクトは、関数の呼び出し元に返さなければならないので、リファレンスキャプチャーすることはできない。コピーをしなければならないが、どうせ関数はすぐ呼び出し元に帰るので、変数vはもう必要ないのに、コピーが発生してしまう。ムーブをしたいところだが、C++11のlambda式には、そのための文法がない。

ムーブキャプチャーできない制限は、C++11の策定時に認識されていたが、適切な文法の議論が必要なために、C++11では後回しにされた。データメンバーをコピーキャプチャーできない問題は、C++11を教育する際に、学習者からわかりにくい挙動だと指摘された。

C++14では、この問題を解決するために、初期化lambdaキャプチャーを追加した。これは、lambdaキャプチャーに初期化子を書くことができる機能だ。


void f()
{
    int data = 0 ;

    // コピーキャプチャー
    [ data = data ]{} ;
    // リファレンスキャプチャー
    [ &data = data ]{ } ;
}

このように、キャプチャーの識別子に初期化子を書くことができる。

初期化lambdaキャプチャーは、あたかも、"auto 初期化lambdaキャプチャー"と書いたかのように振る舞う。リファレンスキャプチャーをする場合は、名前の前に&を書く。

初期化キャプチャーでは、名前を好きにつけることができる。


void f()
{
    int very_long_name = 0 ;
    [ i = very_long_name ] { } ;
}

また、初期化子は初期化子なので、好きな式を書ける。そのため、キャプチャーというよりは、クロージャーオブジェクトのデータメンバーの宣言に近い。


// 自動ストレージ上にはないオブジェクト
int a = 0 ;

int get_value() ;

void f()
{
    int b = 0 ;

    [ a = a, b = b + 1, c = 1, d = get_value() ] { } ;
}

この機能を使うと、C++11のlambdaキャプチャーにある二つの問題を解決できる。

データメンバーのコピーキャプチャーは、簡単にできる。

struct X
{
    int data = 0 ;
    void get_lambda()
    {   // コピーキャプチャー
        return [ data = data ](){ return data ; }
    }
} ;

int main()
{
    std::function< int() > f ;

    {
        X x ;
        f = x.get_lambda ;
    }// xは破棄されている

    f() ; // OK、
}

また、初期化子を書けるので、ムーブをするコピーキャプチャーも書ける。

auto f()
{
    std::vector<int> v(1000) ;

    return [ v = std::move(v) ]() { return std::make_pair( v.begin(), v.end() ) ; }
}

初期化lambdaキャプチャーは、lambda式をより便利に使うことができる新機能だ。

なおこの機能はGCC 4.9とClang 3.4で実装されている。

Clang - C++1z, C++14, C++11 and C++98 Status

C++1y/C++14 Support in GCC - GNU Project - Free Software Foundation (FSF)

See Also:

本の虫: C++14の新機能: 2進数リテラル

本の虫: C++14の新機能: decltype(auto)

本の虫: C++14の新機能: 関数の戻り値の型推定

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

最近、ドワンゴ社内で売られている昼食の質が向上した。好きなだけ取ってグラム単位で会計する仕組みの昼食販売が導入されたからだ。

野菜が取り放題で、しかも安いので、すばらしい。

さて、筆者が十分に多く盛りつけたサラダを食べて満足していると、いかにもよく肉を食べそうな図体のドワンゴ社員が、「僕、肉しか食べないんで」と言いながら、実際に肉を大量に盛り付けて満足そうにしていた。有限実行の徒である。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

1 comment:

Anonymous said...

クロージャオブジェクトは不勉強で使いどころがわからないので参照キャプチャばかり使っています。
一つのクロージャでマルチタスクするにはいろいろ条件分岐を書かないといけませんしちょっとわからないですね。
今のところファンクタの代わりにしかなってません。
なんか面白い使い方無いですかね。