ラムダ式

ラムダ式とは

ラムダ式とは、関数オブジェクトを定義するための新しい文法です。
通常の関数は関数外のグローバルな領域(またはクラス内)に「戻り値の型」「関数名」「引数リスト」「処理ブロック」を記述することで定義しますが、ラムダ式を用いると関数内に簡易的な関数を作ることができます。

文章だけではわかりにくいと思うので以下を見てください。


#include <iostream>

int main()
{
    auto func = [] { std::cout << "hello" << std::endl; };

    func();

    std::cin.get();
}
hello

5行目で、main関数内にラムダ式によりfuncという名前の関数オブジェクトを作成しています。
その関数オブジェクトを実際に呼び出しているのは7行目です。

最小のラムダ式

最小の、「何もしない」ラムダ式は以下のようになります。


[]{};

[]はラムダ式の定義に使用する記号で、ラムダ導入子と言います。
これについてはひとまず横に置いておきます。
{}はおなじみのブロック記号で、この中に実際の処理を記述します。

通常の関数でもラムダ式による関数でも、関数を呼び出す際には関数呼び出し演算子()を使用します。
これは引数がない関数であっても省略することはできません。

よって、コンソール画面に文字を表示する関数オブジェクトを定義し、呼び出すための最小のコードは以下のようになります。


[]{ std::cout << "hello" << std::endl; }();

この一行をmain関数中に記述すれば「hello」と出力されます。
丸括弧よりも手前がラムダ式による関数の定義で、丸括弧によって今定義した関数を呼び出す、という処理になります。

このような、関数オブジェクトを識別・アクセスする名前を持たない関数を無名関数と言います。

名前を付ける

無名関数は使いどころが限られますが、名前を付ければ通常の関数に近い使い方が可能になります。
名前を付けるにはautoを使用します。


auto func = [] { std::cout << "hello" << std::endl; };
func();

これは最初に例示したコードと同じです。
ラムダ式そのものをautoで定義した変数に代入することで、式に名前を付けることができます。
autoは型推論で変数を定義するだけの機能ですから、呼び出せるのはその変数のスコープ内からに限られます。

引数

ラムダ式に引数を指定するには、[]の後に()を記述し、そこに引数リストを記述します。


//無名関数
[](int x, int y){ std::cout << (x + y) << std::endl; }(5, 7);

//名前付き
auto func = [](int x, int y){ std::cout << (x + y) << std::endl; };
func(5, 7);

記述方法も呼び出し方法も通常の関数と同じです。

ちなみに、引数がないラムダ式でも()を付けて定義することができます。


[](){ std::cout << "hello" << std::endl; }();

戻り値

ラムダ式は戻り値を指定することもできます。


//戻り値の型推論
auto addA = [](int x, int y) { return x + y; };

//戻り値の型を明示的に指定
auto addB = [](int x, int y) -> short { return x + y; };

std::cout << addA(5, 7) << std::endl;
std::cout << addB(2, 3) << std::endl;

return文に戻り値を指定すれば、呼び出し元でその値を得ることができます。
引数リスト(引数の指定の丸括弧)の後にアロー演算子(->)とデータ型を指定することで、戻り値の型を明示的に指定することもできます。

変数のキャプチャ

ラムダ式は任意の関数内に記述されますが、ラムダ式のブロック内は独立したスコープとなっており、ラムダ式外の変数にアクセスすることはできません。


int main()
{
    int number = 10;
	
    //変数numberにアクセスできないためエラー
    [] { std::cout << number << std::endl; }();
}

ブロック外の変数にアクセスするには引数で受け取るほか、変数のキャプチャという機能を使用することもできます。


#include <iostream>

int main()
{
    int number = 10;

    //コピー
    [=] { std::cout << number << std::endl; }();

    //参照
    [&] { number *= 2; }();

    std::cout << number << std::endl;

    std::cin.get();
}
10
20

ラムダ導入子の[]の中に=記号を指定すると、その時点までに宣言されている変数をラムダ式内で使用することができます。
この時、変数のコピーがラムダ式内で得られます。

[]の中に&記号を指定すると、変数への参照を得ることができます。
参照ですから、ラムダ式内で値を書き換えると元の変数にも影響します。

キャプチャのタイミング

変数のキャプチャは、コピーと参照とでは値をキャプチャするタイミングが異なります。

コピーの場合は「ラムダ式を定義した時点での値」がキャプチャされます。
参照の場合は「ラムダ式を実行する直前の値」がキャプチャされます。
(というより参照なので、常に元の変数と同じ値になる)


#include <iostream>

int main()
{
    int number = 0;

    auto f1 = [=] { std::cout << "コピー: " << number << std::endl; };
    auto f2 = [&] { std::cout << "参照 : " << number << std::endl; };

    number = 10;

    f1();
    f2();

    std::cin.get();
}
コピー: 0
参照 : 10

mutable

変数のキャプチャに[=]を指定した時、コピーされた値を得られますが、この変数は書き換えることはできません。
変数を書き換える場合は引数の後にmutableを指定します。

コピーであることには変わりはないので、ラムダ式内で書き換えても元の変数には影響しません。


int number = 10;

[=]() {
	//number *= 2; //これはエラー
	std::cout << number << std::endl;
}();

//mutable指定
[=]() mutable { 
	number *= 2; //書き換え可能
	std::cout << number << std::endl; //20
}();

//ラムダ式外には影響しない
std::cout << number << std::endl; //10

mutableを指定する場合、引数がないラムダ式であっても引数リスト指定の()を省略することはできないので注意してください。


//エラー
[=] mutable {}();

キャプチャする変数の指定

キャプチャは、それまでに宣言されているすべての変数が対象になりますが、個別に指定することもできます。


class TestClass
{
    int private_num = 10;
public:
    int public_num = 20;

    void func()
    {
        //自分自身のインスタンスを受け取る
        //メンバ変数にアクセス可能
        [this]
        {
            //privateメンバにもアクセス可能
            std::cout << private_num << std::endl;
            std::cout << public_num << std::endl;
        }();
    }
};

int main()
{
    int a = 10, b = 20, c = 30;

    //aをコピー、bを参照で受け取る
    //cにはアクセスできない
    [a, &b]() {}();

    //cのみ参照で受け取る
    //その他はコピー
    [=, &c]() {}();

    //cのみコピーで受け取る
    //その他は参照
    [&, c]() {}();
}

クラス内でのthisはポインタであるため、参照でもコピーでも値を書き換えるとメンバ変数が書き換わります。

ラムダ式を引数で受け取る

テンプレートを使用することで、ラムダ式を引数として受け取る関数を作ることができます。


template<typename Func>
void test(Func f)
{
    f(); //「hello」を表示
}

int main()
{
    //引数にラムダ式を指定
	//ここでは無名関数を指定している
    test([] {std::cout << "hello" << std::endl; });
}

関数ポインタへの変換

ラムダ式は、同じ引数と戻り値を持つ関数ポインタに変換することができます。
ただし変換できるのは変数をキャプチャしないラムダ式に限られます。


#include <iostream>

int main()
{
    //引数にint型ひとつ、戻り値がint型の
    //fpという名前の関数ポインタ
    int (*fp)(int);

	//同じシグネチャのラムダ式を代入
    fp = [](int a) { return a * a; };

    int r = fp(2);

    std::cout << r << std::endl;

    std::cin.get();
}
4

これを利用することでも関数の引数にラムダ式を直接指定することが可能です。


void test(int(*fp)(int))
{
    int r = fp(2);
    std::cout << r << std::endl;
}

int main()
{
    test([](int a) { return a * a; });
    std::cin.get();
}

std::functionで受け取る

std::functionを使用することでもラムダ式を変数や引数に格納することができます。
使用するには#include <functional>が必要です。


#include <iostream>
#include <functional>

//short型引数ひとつ、戻り値はint型
void test(std::function<int(short)> f)
{
    int r = f(2);

    //20
    std::cout << r << std::endl;
}

int main()
{
    int a = 10;
    
    //キャプチャ可能
    test([=](short x) { return a * x; });

    std::cin.get();
}

std::functionは関数ポインタでもラムダ式でも格納できるのでとても便利です。