C++ でのラムダ式

C++ 11 では、ラムダ式 (ラムダと呼ばれることがよくあります) は、匿名の関数オブジェクト (closure) が呼び出されたり、引数として関数に渡されたりする場所でこのオブジェクトを定義する便利な方法です。 一般に、ラムダは、アルゴリズムまたは非同期の関数に渡される数行のコードをカプセル化するために使用されます。 この記事では、ラムダの定義と他のプログラミング手法との比較を行います。 その利点について説明し、いくつかの基本的な例を示します。

ラムダ式のパーツ

関数に 3 番目の引数として渡される単純なラムダを次に std::sort() 示します。

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

次の図は、ラムダ構文の部分を示しています。

Diagram that identifies the various parts of a lambda expression.

ラムダ式の例は [=]() mutable throw() -> int { return x+y; } です。[=] は capture 句です。C++ 仕様ではラムダ導入機能とも呼ばれます。 かっこはパラメーター リスト用です。 変更可能なキーワード (keyword)は省略可能です。 throw() は省略可能な例外仕様です。 -> int は、省略可能な末尾の戻り値の型です。 ラムダ本体は、中かっこ内のステートメントで構成されるか、x + y を返します。これらは、画像の後で詳しく説明します。

  1. キャプチャ句 (C++ の仕様では ラムダ紹介子 としても知られています。)

  2. パラメーター リスト (省略可能)。 (ラムダ 宣言子 としても知られています)

  3. 変更可能な指定 (省略可能)。

  4. 例外仕様 (省略可能)。

  5. 後続の戻り値の型 (省略可能)。

  6. ラムダ式の本体

キャプチャ句

ラムダは、本体に新しい変数を導入できます (C++14)。また、周辺のスコープから変数にアクセス (または変数を キャプチャ) することができます。 ラムダはキャプチャ句で始まります。 キャプチャする変数と、キャプチャが値別か参照型かを指定します。 アンパサンド (&) プレフィックス付きの変数は参照でアクセスされ、アンパサンド プレフィックスなしの変数は値でアクセスされます。

空のキャプチャ句 [ ] は、ラムダ式の本体が外側のスコープ内の変数にアクセスしないことを示します。

キャプチャ既定モードを使用すると、ラムダ本体で参照されている外部変数をキャプチャする方法を示します。つまり、[&] では参照する変数はすべて参照によってキャプチャされ、[=] では値によってキャプチャされます。 既定のキャプチャ モードを使用してから、特定の変数には明示的に反対のモードを指定することができます。 たとえば、ラムダ式の本体が参照によって外部変数 total にアクセスし、値によって外部変数 factor にアクセスする場合、次の capture 句は同じ結果になります。

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

キャプチャの既定値が使用されている場合、ラムダ本体に記載されている変数だけがキャプチャされます。

キャプチャ句にキャプチャの既定の & が含まれる場合、そのキャプチャ句のキャプチャ内の識別子に &identifier 形式を指定することはできません。 同様に、capture 句に capture-default = が含まれる場合、そのキャプチャ句のキャプチャに =identifier 形式を設定することはできません。 識別子または this をキャプチャ句で複数回使用することはできません。 次のコードでは、そのいくつかの例を示しています。

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

この可変個引数テンプレートの例のように、キャプチャの後に省略記号が続くとパック展開となります。

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

クラスのメンバー関数の本体でラムダ式を使用するには、this ポインターをキャプチャ句に渡して、メンバー関数と外側のクラスのデータ メンバーにアクセスできるようにします。

Visual Studio 2017 バージョン 15.3 以降 (/std:c++17 モード以降で使用可能): capture 句で *this を指定することで、this ポインターを値でキャプチャできます。 値でキャプチャすると、ラムダが呼び出されるすべての呼び出しサイトにクロージャ全体がコピーされます。 (クロージャは、ラムダ式をカプセル化する匿名関数オブジェクトです)。値によるキャプチャは、ラムダが並列または非同期の操作で実行される場合に便利です。 これは、NUMA などの特定のハードウェア アーキテクチャで特に役立ちます。

クラス メソッドでラムダ式を使用する方法の例については、「ラムダ式の例」にある " 例: メソッドでのラムダ式の使用 " を参照してください。

キャプチャ句を使用するとき、特にマルチスレッドでラムダを使用するときは、次の重要点に注意することをお勧めします。

  • 参照キャプチャは外部の変数を変更するために使用できますが、値キャプチャはその目的には使用できません (mutable ではそのコピーを変更できても元の変数は変更できません。)

  • 参照キャプチャでは更新が外部の変数に反映されますが、値キャプチャでは反映されません。

  • 参照キャプチャは有効期間に依存しますが、値キャプチャは依存しません。 これは、ラムダを非同期的に実行する場合は特に重要です。 非同期ラムダでローカルを参照によってキャプチャすると、ラムダが実行されるまでにそのローカルが簡単になくなる可能性があります。 コードによって実行時にアクセス違反が発生する可能性があります。

一般化されたキャプチャ (C++14)

C++14 では、ラムダ関数の外側のスコープにこれらの変数が存在する必要なく、capture 句で新しい変数を導入および初期化できます。 初期化は、任意の式として表現できます。新しい変数の型は、式によって生成される型から推測されます。 この機能を使用すると、移動専用変数 (std::unique_ptr など) を周囲のスコープからキャプチャし、ラムダで使用できます。

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

パラメーター リスト

ラムダは、変数をキャプチャし、入力パラメーターを受け取る両方を実行できます。 パラメーター リスト (標準構文では ラムダ宣言子) は省略可能で、ほとんどの面で関数のパラメーター リストと似ています。

auto y = [] (int first, int second)
{
    return first + second;
};

C++14 では、パラメーターの型がジェネリックの場合、型指定子として auto キーワードを使用できます。 このキーワードにより、コンパイラにテンプレートとして関数呼び出し演算子を作成するように指示します。 パラメーター リストの auto の各インスタンスは、別個の型パラメーターと同等です。

auto y = [] (auto first, auto second)
{
    return first + second;
};

ラムダ式は、引数として別のラムダ式を受け取ることができます。 詳細については、アーティクル「ラムダ式の例」の " 高次のラムダ式 " を参照してください。

パラメーター リストはオプションなので、ラムダ式に引数を渡さず、に、その lambda-declarator が exception-specificationtrailing-return-type、または mutable を含まない場合、空のかっこを省略できます。

変更可能な指定

通常、ラムダの関数呼び出し演算子は const 値ですが、mutable キーワードを使用することで無効になります。変更可能なデータ メンバーは生成できません。 mutable の指定により、ラムダ式の本体で値キャプチャされる変数を変更できるようになります。 この記事の後半にあるいくつかの例で mutable の使用方法を示しています。

例外の指定

noexcept 例外の指定を使用して、ラムダ式が例外をスローしないことを示すことができます。 通常の関数の場合と同様、次の例に示すように、ラムダ式で noexcept 例外の指定を宣言し、ラムダの本体で例外をスローする場合、Microsoft C++ コンパイラは警告 C4297 を生成します。

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

詳細については、「例外指定 (スロー)」を参照してください。

返り値の種類

ラムダ式の戻り値の型は自動的に推測されます。 trailing-return-type を指定しなければ、auto キーワードを使う必要はありません。 trailing-return-type は、通常の関数またはメンバー関数の戻り値の型の部分に似ています。 ただし、戻り値の型はパラメーター リストに従い、戻り値の型の前に trailing-return-type キーワード -> を含める必要があります。

ラムダ式の本体に return ステートメントが 1 つだけ含まれる場合は、ラムダ式の return-type 部分を省略できます。 または、式が値を返しない場合です。 ラムダの本体が単一の return ステートメントで構成される場合、コンパイラは return 式の型から戻り値の型を推測します。 それ以外の場合、コンパイラは戻り値の型が void であると推測します。 この原理を説明する次のコード例について考えてみましょう。

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

ラムダ式は、戻り値として別のラムダ式を作成できます。 詳細については、「ラムダ式の例」の " 高次のラムダ式 " を参照してください。

ラムダ式の本体

ラムダ式のラムダ本体は複合ステートメントです。 通常の関数またはメンバー関数の本体で許可されているものを含むことができます。 通常の関数の本体もラムダ式の本体も次の種類の変数にアクセスできます。

  • 前に説明したように、外側のスコープから変数がキャプチャされました。

  • パラメーター。

  • ローカル宣言変数

  • クラスのデータ メンバー。クラス内で宣言され、this がキャプチャされる場合。

  • 静的ストレージ存続期間の任意の変数 (たとえば、グローバル変数)。

次の例には、変数 n を明示的に値でキャプチャし、変数 m を暗黙的に参照でキャプチャするラムダ式が含まれています。

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

変数 n は値でキャプチャされるため、ラムダ式への呼び出しの後も値は 0 のまま残ります。 mutable の指定により、n をラムダ内で変更できるようになります。

ラムダ式では、自動ストレージ期間を持つ変数のみをキャプチャできます。 ただし、ラムダ式の本体で静的ストレージ期間を持つ変数を使用できます。 次の例では、generate 関数とラムダ式を使用して、vector オブジェクトの各要素に値を代入します。 ラムダ式は、静的変数を変更して次の要素の値を生成します。

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

詳細については、「generate」を参照してください。

次のコード例では、前の例の関数を使用し、C++ 標準ライブラリ アルゴリズム generate_n を使用するラムダ式の例を追加しています。 このラムダ式は vector オブジェクトの要素を前の 2 つの要素の合計に代入します。 ラムダ式が値でキャプチャする外部変数 xy のコピーをラムダ式が変更できるように、mutable キーワードを使用しています。 ラムダ式は元の変数 x および y を値でキャプチャするため、それらの値はラムダの実行後も 1 のまま残ります。

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

詳細については、「generate_n」を参照してください。

constexpr ラムダ式

Visual Studio 2017 バージョン 15.3 以降 (/std:c++17 モード以降で使用可能): キャプチャまたは導入された各データ メンバーの初期化が定数式内で許可されている場合は、ラムダ式を constexpr として宣言 (または定数式で使用) できます。

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

ラムダは、結果が constexpr 関数の要件を満たす場合は、暗黙的 constexpr です。

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

ラムダが暗黙的または明示的 constexprの場合、constexpr 関数ポインターへの変換によって関数が生成されます。

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

Microsoft 固有の仕様

ラムダは、共通言語ランタイム (CLR) によって管理されるエンティティである ref classref structvalue class、または value struct ではサポートされていません。

__declspec などの Microsoft 固有の修飾子を使用している場合は、parameter-declaration-clause の直後にラムダ式に挿入できます。 次に例を示します。

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

特定の修飾子がラムダでサポートされているかどうかを判断するには、「Microsoft 固有の修飾子」セクションの修飾子に関する記事を参照してください。

Visual Studio C++11 標準のラムダ機能とステートレス ラムダがサポートされています。 ステートレス ラムダは、任意の呼び出し規則を使用する関数ポインターに変換できます。

関連項目

C++ 言語リファレンス
C++ 標準ライブラリの関数オブジェクト
関数呼び出し
for_each