読者です 読者をやめる 読者になる 読者になる

C++0x ラムダ式

C++

【2015年12月24日記載 : C++11 ラムダ式の正式なまとめは、こちらのページ「C++11 ラムダ式 - cpprefjp C++日本語リファレンス」を参照してください】

今まではラムダ式のリビジョンが上がるたびに差分だけ書いてたので
C++0xに採択されたラムダ式の解説をあらためて書きます。


C++03までの問題点】
C++03では、関数をその場で作成する、ということができなかったため
関数を受け取るSTLアルゴリズムを使用する場合、以下のように、アルゴリズムに渡す用の関数を作成しなければなりませんでした。

struct is_even {
    typedef bool result_type;

    bool operator()(int x) const
    {
        return x % 2 == 0;
    }
};

vector<int> v;
find_if(v.begin(), v.end(), is_even());


C++0xでの解決策】
C++0x では、ラムダ式という、匿名関数オブジェクトを生成するための式が提供されます。
先ほどのis_even関数オブジェクトを使用した例は、以下のように書き直すことができます。

vector<int> v;
find_if(v.begin(), v.end(), [](int x) -> bool { return x % 2 == 0; });

この例では、パラメータでintを受け取り、boolを返す関数オブジェクトをその場で作成してアルゴリズムに渡しています。


【戻り値型の推論】
ラムダ式は戻り値の型を省略することができるため、以下のように書くこともできます。

[](int x) { return x % 2 == 0; }

戻り値の型を省略した場合、戻り値の型は、return文をdecltypeした型となるため、
上記のラムダ式は、以下のラムダ式と同じ意味になります。

[](int x) -> decltype(x % 2 == 0) { return x % 2 == 0; }

return文を書かない場合、戻り値の型はvoidになります。

vector<int> v;
for_each(v.begin(), v.end(), [](int x) { cout << x << endl; });


【変数のキャプチャ】
ラムダ式には、変数のキャプチャという機能があります。
これを使用すると、指定した変数をラムダ式内で使用することができます。

たとえば、以下はrate変数をラムダ式で使用した例です。

vector<int> v;
int rate = 2;
transform(v.begin(), v.end(), v.begin(), [rate](int x) { return x * rate; });

上記のラムダ式では、rate変数をラムダ式内で使用するために、 [ ] の中にrate変数を渡しています。
[ ] は、ラムダ導入子(lambda-introducer)といいます。
この場合、rate変数のコピーがラムダ式に渡されます。

変数をコピーではなく参照でキャプチャするには、変数の先頭に&を付けます。

vector<int> v;
int sum = 0;
for_each(v.begin(), v.end(), [&sum](int x) { sum += x; });

ここでは、変数ごとにキャプチャ方法(コピーか参照)を指定して変数をキャプチャしていますが
変数のキャプチャには、個別指定の他に、デフォルトのキャプチャ方法を指定することもできます。

vector<int> v;
int rate = 2;
transform(v.begin(), v.end(), v.begin(), [=](int x) { return x * rate; });
vector<int> v;
int sum = 0;
for_each(v.begin(), v.end(), [&](int x) { sum += x; });

ここでは、デフォルトのキャプチャ方法を指定するために、=と&を指定しています。
=は変数をデフォルトでコピーキャプチャします。
&は変数をデフォルトで参照キャプチャします。

また、デフォルト指定と個別指定を組み合わせることもできます。
その場合、デフォルトキャプチャ方法を先頭に書き、その次に個別にキャプチャ方法を指定します。

vector<int> v;
int rate = 2;
int sum = 0;

for_each(v.begin(), v.end(), [&, rate](int x) { sum += x * rate; });

上記のラムダ式では、デフォルトで参照キャプチャし、rate変数をコピーキャプチャしています。


【thisのキャプチャ】
メンバ関数内では、thisをキャプチャすることでラムダ式内でメンバ変数、メンバ関数を使用することができます。

class hoge {
    vector<int> vec_;

    void disp(int i) const
    {
        cout << i << endl;
    }

public:
    void call() const
    {
        for_each(vec_.begin(), vec_.end(), [this](int i){ disp(i); });
    }
};

上記のラムダ式では、thisをキャプチャして、vectorの各要素をhogeクラスのprivateメンバ関数dispに渡して実行しています。
メンバ関数内のラムダ式は、そのクラスの friend と見なされるので、privateメンバを使用することができるのです。


ラムダ式によって生成される匿名関数オブジェクト】
ラムダ式を書くことで、その場に匿名関数オブジェクトが生成されます。
たとえば、以下のラムダ式では

int rate = 2;
[rate](int x) -> int { return x * rate; }

以下のような関数オブジェクトが生成されます。

class F {
    int rate;
public:
    F(int rate)
        : rate(rate) {}

    F(const F&) = default;

    F(F&& other)
        : rate(static_cast<int&&>(other.rate)) {}

    F& operator=(const F&) = delete;

    int operator()(int x) const
    {
        return x * rate;
    }
};

ここでは、rate変数をコピーキャプチャしているため、ラムダ式によって生成される匿名関数オブジェクトは、rate変数をメンバ変数として持ちます。
ラムダ式によって生成される匿名関数オブジェクトは、キャプチャによってそのスコープの環境を持つため、クロージャオブジェクトと呼ばれます。

クロージャオブジェクトは、以下のような特徴があります。


ラムダ式のmutable修飾】
クロージャオブジェクトの関数呼び出し演算子constメンバ関数のため、コピーキャプチャした変数を書き換えることはできません。

int rate = 2;
[rate](int x) -> int { return x * ++rate; } // エラー!rate変数を書き換えることはできない

コピーキャプチャした変数をどうしてもラムダ式内で書き換えたい場合は、ラムダ式をmutable修飾します。

[rate](int x) mutable -> int { return x * ++rate; } // OK

【追記】std::reference_closureクラスは削除されました。

【参照ラムダ】
ラムダ式が参照環境のみを持つ場合、クロージャオブジェクトはstd::reference_closureというクラスを継承した関数オブジェクトになります。
その場合、キャプチャした変数がクロージャオブジェクトのメンバ変数になるのではなく、
フレームポインタというものを使用してスコープをのぞき見る、ということをするようになります。

また、ラムダ式のキャプチャセットが1つ以上参照を含む場合、コンテキストブロックを抜けたあとのクロージャオブジェクトの実行とコピーの動作は未定義です。

#include <functional>

std::reference_closure<int(int)> foo()
{
    int n = 3;
    return [&n](int i) -> int { return n + i; };
}

foo()(2); // 未定義の振る舞い

std::reference_closureは、ラムダ式を関数に渡す際に、参照環境のみを持つラムダ式とそれ以外のラムダ式オーバーロードするために使用することもできます。

void foo(std::reference_closure<void(void)>);
void foo(std::function<void(void)>);

foo([&]{}); // reference_closureの方が呼ばれる
foo([=]{}); // functionの方が呼ばれる


N2550 Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4)

N2658 Constness of Lambda Functions (Revision 1)

C++0x言語拡張まとめ