C++ 中的 Lambda 運算式

在 C++11 和更新版本中,Lambda 運算式通常稱為 Lambda,是定義匿名函式物件( 關閉)的便利方式,位於其叫用或傳遞為函式自變數的位置。 通常 Lambda 是用來封裝幾行程式代碼,這些程式代碼會傳遞至演算法或異步函式。 本文會定義什麼是 Lambda,並將其與其他程式設計技術進行比較。 其描述其優點,並提供一些基本範例。

Lambda 表達式的元件

以下是作為第三個自變數傳遞至函式的 std::sort() 簡單 Lambda:

#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
    );
}

此圖顯示 Lambda 語法的部分:

Diagram that identifies the various parts of a lambda expression.

Lambda 表達式範例為 [=]() 可變 throw() -> int { return x+y; }[=] 是擷取子句;也稱為 C++ 規格中的 Lambda 引進器。 括號適用於參數清單。 可變動關鍵詞是選擇性的。 throw() 是選擇性的例外狀況規格。 -> int 是選擇性的尾端傳回型別。 Lambda 主體包含大括弧內的 語句,或傳回 x+y;這些會在影像之後更詳細地說明。

  1. capture 子句 (也稱為 C++ 規格中的 lambda-introducer

  2. 參數清單 選用。 (也稱為 Lambda 宣告子

  3. 可變動規格 選用。

  4. exception-specification 選用。

  5. trailing-return-type 選擇性。

  6. Lambda 主體

擷取

Lambda 可以在其主體中引進新的變數(在 C++14 中),也可以從周圍範圍存取或 擷取變數。 Lambda 會從擷取子句開始。 它會指定擷取哪些變數,以及擷取是依值還是以傳址方式擷取。 具有 ampersand (&) 前置詞的變數會依傳址方式存取,而沒有該前置詞的變數則依值存取。

空白的擷取子句 [ ] 表示 Lambda 運算式主體不存取封閉範圍中的任何變數。

您可以使用擷取預設模式來指出如何擷取 Lambda 主體中所參考的任何外部變數: [&] 表示您參考的所有變數都會依傳址方式擷取,並 [=] 表示這些變數是以傳值方式擷取。 您可以使用預設擷取模式,然後明確指定特定變數的相反模式。 例如,如果 Lambda 主體以傳址方式存取外部變數 total,並以傳值方式存取外部變數 factor,則下列擷取子句相等:

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

使用擷取預設值時,只會擷取 Lambda 主體中提及的變數。

如果擷取子句包含擷取預設值 &,則擷取子句中沒有任何標識碼可以有 格式 &identifier。 同樣地,如果擷取子句包含擷取預設值 =,則該擷取子句的擷取格式不能有 =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
}

擷取後面接著省略號是套件擴充,如下列 variadic 範 例所示:

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

若要在類別成員函式主體中使用 Lambda 運算式,請傳遞 this 擷取子句的指標,以提供封入類別成員函式和數據成員的存取權。

Visual Studio 2017 15.3 版和更新版本(適用於/std:c++17模式和更新版本):this指標可藉由在擷取子句中指定*this來擷取。 依值擷取會將整個關閉複製到叫用 Lambda 的每個呼叫月臺。 (關閉是封裝 Lambda 表達式的匿名函式物件。當 Lambda 以平行或異步操作執行時,依值擷取會很有用。 它特別適用於某些硬體架構,例如 NUMA。

如需示範如何搭配類別成員函式使用 Lambda 表達式的範例,請參閱 Lambda 運算式範例中的 <範例:在方法中使用 lambda="">>。

當您使用擷取子句時,建議您記住這些點,特別是當您搭配多線程使用 Lambda 時:

  • 參考擷取可用來修改外部變數,但值擷取無法。 (mutable 允許修改複本,但不允許原始。

  • 參考擷取會反映外部變數的更新,但值擷取不會。

  • 傳址擷取方式採用存留期相依性,但傳值擷取方式沒有存留期相依性。 當 Lambda 以異步方式執行時,特別重要。 如果您在異步 Lambda 中以傳址方式擷取本機,該本機可能會在 Lambda 執行時輕鬆消失。 您的程式代碼可能會在運行時間造成存取違規。

一般化擷取 (C++14)

在 C++14 中,您可以在擷取子句中引進和初始化新的變數,而不需要在 Lambda 函式的封入範圍中存在這些變數。 初始化可以表示為任何任意運算式;新變數的類型是透過運算式所產生的類型推斷而來。 這項功能可讓您從周圍範圍擷取僅限移動變數(例如 std::unique_ptr),並在 Lambda 中使用它們。

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

參數清單

Lambda 可以同時擷取變數並接受輸入參數。 參數清單(Standard 語法中的 Lambda 宣告子 )是選擇性的,而且在大部分方面類似於函式的參數清單。

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

C++14 中,如果參數類型為泛型,您可以使用 auto 關鍵詞作為類型規範。 這個關鍵詞會指示編譯程式建立函數調用運算子做為範本。 參數清單中的 每個實例 auto 都相當於相異型別參數。

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

Lambda 運算式可接受另一個 Lambda 運算式當做其引數。 如需詳細資訊,請參閱 Lambda 運算式範例一文 中的<較高順序 lambda="">>。

因為參數清單是選擇性的,如果您未將自變數傳遞至 Lambda 運算式,且其 Lambda 宣告子不包含 exception-specificationtrailing-return-typemutable,則可以省略空括號。

可變動規格

一般而言,Lambda 的函式呼叫運算符是 const-by-value,但使用 mutable 關鍵詞會取消這項作業。它不會產生可變的數據成員。 此 mutable 規格可讓 Lambda 表達式的主體修改值所擷取的變數。 本文稍後的一些範例示範如何使用 mutable

例外狀況規格

您可以使用 noexcept 例外狀況規格來指出 Lambda 表達式不會擲回任何例外狀況。 如同一般函式,如果 Lambda 表達式宣告noexcept例外狀況規格,且 Lambda 主體擲回例外狀況,Microsoft C++ 編譯程式會產生警告 C4297,如下所示:

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

如需詳細資訊,請參閱 例外狀況規格 (throw)

傳回類型

Lambda 運算式的傳回型別會自動推算出來。 除非您指定尾端傳回型別,否則不需要使用 auto 關鍵詞。 尾端傳回型別類似於一般函式或成員函式的傳回類型部分。 不過,傳回型別必須接在參數清單後面,而且您必須在傳回型別前面包含 trailing-return-type 關鍵字 ->

如果 Lambda 主體只包含一個 return 語句,則可以省略 Lambda 表達式的傳回類型部分。 或者,如果表示式未傳回值,則為 。 如果 Lambda 主體包含單一 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

Lambda 運算式可能會產生另一個 Lambda 運算式當做其傳回值。 如需詳細資訊,請參閱 Lambda 運算式範例中的 <較高順序 lambda="">>。

Lambda 主體

Lambda 表達式的 Lambda 主體是複合語句。 它可以包含一般函式或成員函式主體中允許的任何專案。 一般函式和 Lambda 運算式的主體都可以存取下列類型的變數:

  • 擷取自封閉範圍的變數 (如先前所述)。

  • 參數。

  • 本機宣告的變數。

  • 類別數據成員,在類別內宣告並 this 擷取時。

  • 任何具有靜態儲存持續時間的變數,例如全域變數。

下列範例包含 Lambda 運算式,這個運算式會以傳值方式明確擷取變數 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 是以傳值方式擷取,其值在 Lambda 運算式呼叫之後會保持為 0。 規格 mutable 允許 n 在 Lambda 內修改。

Lambda 運算式只能擷取具有自動儲存持續時間的變數。 不過,您可以在 Lambda 表達式主體中使用具有靜態儲存持續時間的變數。 下列範例使用 generate 函式和 Lambda 運算式,將值指派給 vector 物件的每個項目。 Lambda 運算式會修改這個靜態變數以產生下一個項目的值。

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
}

如需詳細資訊,請參閱 產生

下列程式代碼範例會使用上一個範例中的 函式,並新增使用 C++ 標準連結庫演算法 generate_n的 Lambda 運算式範例。 此 Lambda 運算式會將 vector 物件的元素指派給前兩個元素的總和。 使用 mutable 關鍵詞,讓 Lambda 表達式的主體可以修改其外部變數和 的複本,而 Lambda 運算式會依值擷取這些變數xy。 由於 Lambda 運算式會以傳值方式擷取原始變數 xy,在 Lambda 執行後,它們的值仍然保持 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 Lambda 表達式

Visual Studio 2017 15.3 版和更新版本 (可在 /std:c++17 模式和更新版本中使用):您可以在常數表達式中允許每個擷取或引進的數據成員初始化時,將 Lambda 表達式宣告為 constexpr (或在常數運算式中使用)。

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

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

如果 Lambda 的結果符合函式的需求,constexpr則為隱含constexpr方式:

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

    constexpr int response = answer(10);

如果 Lambda 隱含或明確 constexpr,則轉換成函式指標會產生函 constexpr 式:

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

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

Microsoft 專有

下列 Common Language Runtime (CLR) Managed 實體不支援 Lambda:ref class、、 ref structvalue classvalue struct

如果您使用 Microsoft 特定的修飾詞,例如 __declspec,您可以在 之後 parameter-declaration-clause立即將它插入 Lambda 運算式中。 例如:

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

若要判斷 Lambda 是否支援特定修飾詞,請參閱 Microsoft 特定修飾詞一節中的 修飾 詞一文。

Visual Studio 支援 C++11 標準 Lambda 功能,以及 無狀態 Lambda。 無狀態 Lambda 可轉換成使用任意呼叫慣例的函式指標。

另請參閱

C++ 語言參考
C++ 標準程式庫的函式物件
函式呼叫
for_each