Lambdaausdrücke in C++

In C++11 und höher ist ein Lambdaausdruck – häufig als Lambda bezeichnet – eine bequeme Möglichkeit, ein anonymes Funktionsobjekt (eine Schließung) direkt an der Stelle zu definieren, an der es aufgerufen oder als Argument an eine Funktion übergeben wird. In der Regel werden Lambdas verwendet, um einige Codezeilen zu kapseln, die an Algorithmen oder asynchrone Funktionen übergeben werden. In diesem Artikel werden Lambdas definiert und mit anderen Programmiertechniken verglichen. Sie beschreibt ihre Vorteile und enthält einige grundlegende Beispiele.

Teile eines Lambdaausdrucks

Hier ist ein einfaches Lambda, das als drittes Argument an die std::sort() Funktion übergeben wird:

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

Diese Abbildung zeigt die Teile der Lambdasyntax:

Diagramm, das die verschiedenen Teile eines Lambdaausdrucks identifiziert.

Das Lambdaausdrucksbeispiel ist [=]() änderbare throw() -> int { return x+y; } [=] ist die Erfassungsklausel; auch als Lambda-Einführung in der C++-Spezifikation bezeichnet. Die Klammern sind für die Parameterliste. Das veränderbare Schlüsselwort ist optional. throw() ist die optionale Ausnahmespezifikation. -> int ist der optionale nachgestellte Rückgabetyp. Der Lambdatext besteht aus der Anweisung innerhalb der geschweiften Klammern oder gibt x+y zurück; Diese werden im Anschluss an die Abbildung ausführlicher erläutert.

  1. Capture-Klausel (auch als Lambda-Einführung in der C++-Spezifikation bezeichnet.)

  2. Parameterliste Optional. (Auch als Lambda-Deklarator bezeichnet)

  3. veränderliche Spezifikation Optional.

  4. Ausnahmespezifikation Optional.

  5. trailing-return-type Optional.

  6. Lambdatext.

Capture-Klausel

Ein Lambda kann neue Variablen in seinen Textkörper (in C++14) einführen und auch auf Variablen aus dem umgebenden Bereich zugreifen oder diese erfassen. Ein Lambda beginnt mit der Erfassungsklausel. Es gibt an, welche Variablen erfasst werden, und ob die Erfassung nach Wert oder nach Verweis erfolgt. Auf Variablen mit dem Präfix ampersand (&) wird per Verweis zugegriffen, und auf Variablen, für die es nicht vorhanden ist, wird über den Wert zugegriffen.

Eine leere Erfassungsklausel ([ ]) gibt an, dass der Lambda-Text auf keine Variablen im einschließenden Bereich zugreift.

Sie können einen Aufzeichnungsstandardmodus verwenden, um anzugeben, wie alle externen Variablen erfasst werden, auf die im Lambdatext verwiesen wird: [&] Bedeutet, dass alle Variablen, auf die Sie verweisen, als Verweis erfasst werden, und [=] bedeutet, dass sie nach Wert erfasst werden. Sie können einen Standarderfassungsmodus verwenden, und dann den entgegengesetzten Modus explizit für bestimmte Variablen angeben. Wenn beispielsweise der Lambda-Text auf die externe Variable total nach Wert zugreift und auf die externe Variable factor nach Wert, dann sind die folgenden Erfassungsklauseln gleichwertig:

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

Nur Variablen, die im Lambdatext erwähnt werden, werden erfasst, wenn ein Capture-Default verwendet wird.

Wenn eine Capture-Klausel eine Capture-Default-Klausel &enthält, kann kein Bezeichner in einer Erfassung dieser Erfassungsklausel das Format &identifieraufweisen. Wenn die Capture-Klausel eine Capture-Default-Klausel =enthält, kann keine Erfassung dieser Erfassungsklausel das Format =identifieraufweisen. Ein Bezeichner oder this darf nicht mehr als einmal in einer Erfassungsklausel angezeigt werden. Der folgende Codeausschnitt veranschaulicht einige Beispiele:

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
}

Eine Erfassung gefolgt von einer Auslassungspunkte ist eine Packerweiterung, wie in diesem Beispiel für eine variatische Vorlage gezeigt:

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

Um Lambdaausdrücke im Textkörper einer Klassenmemberfunktion zu verwenden, übergeben Sie den this Zeiger auf die Erfassungsklausel, um zugriff auf die Memberfunktionen und Datenmember der einschließenden Klasse zu ermöglichen.

Visual Studio 2017, Version 15.3 und höher (im /std:c++17 Modus und höher verfügbar): Der this Zeiger kann nach Wert erfasst werden, indem in der Erfassungsklausel angegeben *this wird. Capture by value kopiert die gesamte Schließung an jede Aufrufwebsite, an der das Lambda aufgerufen wird. (Eine Schließung ist das anonyme Funktionsobjekt, das den Lambdaausdruck kapselt.) Die Erfassung nach Wert ist nützlich, wenn das Lambda in parallelen oder asynchronen Vorgängen ausgeführt wird. Dies ist besonders nützlich für bestimmte Hardwarearchitekturen, z. B. NUMA.

Ein Beispiel für die Verwendung von Lambdaausdrücken mit Klassenmemberfunktionen finden Sie unter Beispiel: Verwenden eines Lambdaausdrucks in einer Methode unter Beispiele für Lambdaausdrücke.

Wenn Sie die Erfassungsklausel verwenden, sollten Sie diese Punkte beachten, insbesondere wenn Sie Lambdas mit Multithreading verwenden:

  • Referenzerfassungen können verwendet werden, um Variablen außerhalb zu ändern, Werterfassungen jedoch nicht. (mutable Erlaubt das Ändern von Kopien, aber nicht der Originale.)

  • Referenzerfassungen spiegeln Aktualisierungen von Variablen außerhalb wider, Werterfassungen jedoch nicht.

  • Verweiserfassungen führen eine Lebenszeitabhängigkeit ein, während Werterfassungen keine Lebenszeitabhängigkeiten haben. Dies ist besonders wichtig, wenn das Lambda asynchron ausgeführt wird. Wenn Sie einen lokalen Verweis in einem asynchronen Lambda erfassen, kann dieser lokale wert sein, wenn das Lambda ausgeführt wird. Ihr Code kann zur Laufzeit eine Zugriffsverletzung verursachen.

Generalisierte Erfassung (C++14)

In C++14 können Sie neue Variablen in der Erfassungsklausel einführen und initialisieren, ohne dass diese Variablen im eingeschlossenen Bereich der Lambdafunktion vorhanden sein müssen. Die Initialisierung kann mit einem beliebigen Ausdruck ausgedrückt werden; der Typ der neuen Variablen wird von dem durch den Ausdruck genierten Typ abgeleitet. Mit dieser Funktion können Sie Nur-Move-Variablen (z std::unique_ptr. B. ) aus dem umgebenden Bereich erfassen und in einem Lambda verwenden.

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

Parameterliste

Lambdas können Sowohl Variablen erfassen als auch Eingabeparameter akzeptieren. Eine Parameterliste (Lambda-Deklarator in der Standardsyntax) ist optional und ähnelt in den meisten Aspekten der Parameterliste für eine Funktion.

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

Wenn der Parametertyp in C++14 generisch ist, können Sie das auto Schlüsselwort als Typspezifizierer verwenden. Dieses Schlüsselwort weist den Compiler an, den Funktionsaufrufoperator als Vorlage zu erstellen. Jede Instanz von auto in einer Parameterliste entspricht einem eindeutigen Typparameter.

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

Ein Lambdaausdruck kann einen anderen Lambdaausdruck als Argument übernehmen. Weitere Informationen finden Sie unter "Lambdaausdrücke höherer Reihenfolge" im Artikel Beispiele für Lambdaausdrücke.

Da eine Parameterliste optional ist, können Sie die leeren Klammern weglassen, wenn Sie keine Argumente an den Lambdaausdruck übergeben und der Lambda-Deklarator keine Exception-spezifikation, trailing-return-type oder mutableenthält.

Veränderliche Spezifikation

In der Regel ist der Funktionsaufrufoperator eines Lambdas wertkonst-by-Value, aber die Verwendung des mutable Schlüsselworts bricht dies ab. Es werden keine veränderlichen Datenmember erzeugt. Die mutable Spezifikation ermöglicht es dem Textkörper eines Lambdaausdrucks, Variablen zu ändern, die vom Wert erfasst werden. Einige der Beispiele weiter unten in diesem Artikel veranschaulichen die Verwendung mutablevon .

Ausnahmespezifikation

Sie können die noexcept Ausnahmespezifikation verwenden, um anzugeben, dass der Lambdaausdruck keine Ausnahmen auslöst. Wie bei normalen Funktionen generiert der Microsoft C++-Compiler die Warnung C4297 , wenn ein Lambdaausdruck die noexcept Ausnahmespezifikation deklariert und der Lambdatext eine Ausnahme auslöst, wie hier gezeigt:

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

Weitere Informationen finden Sie unter Ausnahmespezifikationen (Throw).

Rückgabetyp

Der Rückgabetyp von Lambda-Ausdrücken wird automatisch hergeleitet. Sie müssen das auto Schlüsselwort nicht verwenden, es sei denn, Sie geben einen nachgestellten Rückgabetyp an. Der nachgestellte Rückgabetyp ähnelt dem Rückgabetypteil einer normalen Funktion oder Memberfunktion. Der Rückgabetyp folgt jedoch auf die Parameterliste, und das Schlüsselwort „trailing-return-type ->“ muss vor dem Rückgabetyp angegeben werden.

Sie können den Rückgabetypteil eines Lambdaausdrucks weglassen, wenn der Lambdatext nur eine return-Anweisung enthält. Oder, wenn der Ausdruck keinen Wert zurückgibt. Wenn der Lambda-Text aus einer einzelnen Rückgabeanweisung besteht, leitet der Compiler den Rückgabetyp vom Typ des Rückgabeausdrucks ab. Andernfalls leitet der Compiler den Rückgabetyp als voidab. Betrachten Sie die folgenden Beispielcodeausschnitte, die dieses Prinzip veranschaulichen:

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

Ein Lambdaausdruck kann einen anderen Lambdaausdruck als Rückgabewert erzeugen. Weitere Informationen finden Sie unter "Lambdaausdrücke höherer Reihenfolge" unter Beispiele für Lambdaausdrücke.

Lambdatext

Der Lambdatext eines Lambdaausdrucks ist eine zusammengesetzte Anweisung. Sie kann alles enthalten, was im Textkörper einer normalen Funktion oder Memberfunktion zulässig ist. Der Text einer gewöhnlichen Funktion und eines Lambdaausdrucks kann auf die folgenden Variablenarten zugreifen:

  • Aus dem einschließenden Bereich erfasste Variablen, wie zuvor beschrieben.

  • Parameter.

  • Lokal deklarierte Variablen.

  • Klassendatenmember, die innerhalb einer Klasse deklariert und this erfasst werden.

  • Jede Variable mit statischer Speicherdauer, z. B. globale Variablen.

Das folgende Beispiel enthält einen Lambdaausdruck, welcher explizit die Variable n nach ihrem Wert erfasst und implizit die Variable m als Verweis erfasst:

// 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

Da die Variable n als Wert erfasst wird, bleibt der Wert 0 nach dem Aufruf des Lambda-Ausdrucks erhalten. Die mutable Spezifikation ermöglicht es n , innerhalb des Lambdas geändert zu werden.

Ein Lambdaausdruck kann nur Variablen erfassen, die über eine automatische Speicherdauer verfügen. Sie können jedoch Variablen verwenden, die eine statische Speicherdauer im Text eines Lambdaausdrucks aufweisen. Im folgenden Beispiel wird die Funktion generate und ein Lambdaausdruck verwendet, um jedem Element in einem vector-Objekt einen Wert zuzuweisen. Der Lambda-Ausdruck ändert die statische Variable, um den Wert des nächsten Elements zu generieren.

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
}

Weitere Informationen finden Sie unter generieren.

Im folgenden Codebeispiel wird die Funktion aus dem vorherigen Beispiel verwendet, und es wird ein Beispiel für einen Lambdaausdruck hinzugefügt, der den C++-Standardbibliotheksalgorithmus generate_nverwendet. Dieser Lambdaausdruck weist ein Element eines vector-Objekts der Summe der vorangehenden zwei Elemente zu. Das mutable Schlüsselwort wird verwendet, damit der Text des Lambdaausdrucks seine Kopien der externen Variablen x und yändern kann, die der Lambdaausdruck nach Wert erfasst. Da der Lambdaausdruck die originalen Variablen x und y als Wert erfasst, bleiben die Werte 1, nachdem das Lambda ausgeführt wird.

// 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

Weitere Informationen finden Sie unter generate_n.

constexpr Lambdaausdrücke

Visual Studio 2017 Version 15.3 und höher (im /std:c++17 Modus und höher verfügbar): Sie können einen Lambdaausdruck als constexpr deklarieren (oder in einem konstanten Ausdruck verwenden), wenn die Initialisierung jedes erfassten oder eingeführten Datenmembers innerhalb eines konstanten Ausdrucks zulässig ist.

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

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

Ein Lambda ist implizit constexpr , wenn sein Ergebnis die Anforderungen einer constexpr Funktion erfüllt:

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

    constexpr int response = answer(10);

Wenn ein Lambda implizit oder explizit constexprist, erzeugt die Konvertierung in einen Funktionszeiger eine constexpr Funktion:

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

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

Microsoft-spezifisch

Lambdas werden in den folgenden verwalteten CLR-Entitäten (Common Language Runtime) nicht unterstützt: ref class, ref struct, value classoder value struct.

Wenn Sie einen Microsoft-spezifischen Modifizierer wie __declspecverwenden, können Sie ihn direkt nach dem parameter-declaration-clausein einen Lambdaausdruck einfügen. Beispiel:

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

Informationen dazu, ob ein bestimmter Modifizierer von Lambdas unterstützt wird, finden Sie im Artikel zum Modifizierer im Abschnitt Microsoft-spezifische Modifizierer .

Visual Studio unterstützt die Standard-Lambdafunktionalität von C++11 und zustandslose Lambdas. Ein zustandsloses Lambda kann in einen Funktionszeiger konvertiert werden, der eine beliebige Aufrufkonvention verwendet.

Siehe auch

C++-Programmiersprachenreferenz
Funktionsobjekte in der C++-Standardbibliothek
Funktionsaufruf
for_each