Was ist ein Lambda-Ausdruck in C++11?

Was ist ein Lambda-Ausdruck in C++11? Wann würde ich einen verwenden? Welche Art von Problemen lösen sie, die vor ihrer Einführung nicht möglich waren?

Ein paar Beispiele und Anwendungsfälle wären hilfreich.

Lösung

Das Problem

C++ enthält nützliche generische Funktionen wie std::for_each und std::transform, die sehr praktisch sein können. Leider können sie auch recht umständlich zu benutzen sein, besonders wenn der functor, den Sie anwenden möchten, nur für die jeweilige Funktion gilt.

#include 
#include 

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Wenn man f nur einmal und an dieser speziellen Stelle verwendet, scheint es übertrieben, eine ganze Klasse zu schreiben, nur um etwas Triviales und Einmaliges zu tun.

In C++03 könnte man versucht sein, etwas wie das Folgende zu schreiben, um den Funktor lokal zu halten:

void func2(std::vector& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

Dies ist jedoch nicht erlaubt, da f in C++03 nicht an eine Template-Funktion übergeben werden kann.

Die neue Lösung

C++11 führt Lambdas ein, die es ermöglichen, einen anonymen Inline-Faktor zu schreiben, der das "Konstrukt f" ersetzt. Für kleine, einfache Beispiele kann dies sauberer zu lesen sein (alles bleibt an einem Ort) und möglicherweise einfacher zu pflegen, zum Beispiel in der einfachsten Form:

void func3(std::vector& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Lambda-Funktionen sind nur syntaktischer Zucker für anonyme Funktoren.

Rückgabetypen

In einfachen Fällen wird der Rückgabetyp des Lambdas für Sie hergeleitet, z.B.:

void func4(std::vector& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

Wenn Sie jedoch anfangen, komplexere Lambdas zu schreiben, werden Sie schnell auf Fälle stoßen, in denen der Rückgabetyp nicht vom Compiler abgeleitet werden kann, z. B.:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Um dieses Problem zu lösen, können Sie explizit einen Rückgabetyp für eine Lambda-Funktion angeben, indem Sie -> T verwenden:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

"Erfassen" von Variablen

Bisher haben wir nur das verwendet, was dem Lambda innerhalb des Lambdas übergeben wurde, aber wir können auch andere Variablen innerhalb des Lambdas verwenden. Wenn Sie auf andere Variablen zugreifen wollen, können Sie die Capture-Klausel (das [] des Ausdrucks) verwenden, die in diesen Beispielen bisher nicht verwendet wurde, z.B.:

void func5(std::vector& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Sie können sowohl über eine Referenz als auch über einen Wert erfassen, die Sie mit & bzw. = angeben können:

  • [&epsilon] erfasst durch Referenz
  • [&] erfasst alle im Lambda verwendeten Variablen per Referenz
  • [=] erfasst alle Variablen, die im Lambda als Wert verwendet werden
  • [&, epsilon] erfasst Variablen wie mit [&], aber epsilon als Wert
  • [=, &epsilon] erfasst Variablen wie mit [=], aber epsilon als Referenz

Der erzeugte operator() ist standardmäßig const, was bedeutet, dass die Captures standardmäßig const sind, wenn man auf sie zugreift. Dies hat den Effekt, dass jeder Aufruf mit der gleichen Eingabe das gleiche Ergebnis liefert. Sie können jedoch das Lambda als mutable markieren, um zu verlangen, dass der erzeugte Operator() nicht const ist.

Kommentare (14)

Was ist eine Lambda-Funktion?

Das C++-Konzept der Lambda-Funktion stammt aus dem Lambda-Kalkül und der funktionalen Programmierung. Ein Lambda ist eine unbenannte Funktion, die (in der tatsächlichen Programmierung, nicht in der Theorie) für kurze Codeschnipsel nützlich ist, die nicht wiederverwendet werden können und keine Benennung wert sind.

In C++ ist eine Lambda-Funktion wie folgt definiert

[]() { } // barebone lambda

oder in seiner ganzen Pracht

[]() mutable -> T { } // T is the return type, still lacking throw()

[] ist die Fangliste, () die Argumentliste und {} der Funktionskörper.

Die Aufnahmeliste

Die Aufnahmeliste legt fest, was von außerhalb des Lambdas im Funktionsrumpf verfügbar sein soll und wie. Sie kann entweder sein:

  1. ein Wert: [x]
  2. eine Referenz [&x]
  3. eine beliebige Variable, die sich gerade im Geltungsbereich befindet, durch Verweis [&]
  4. wie 3, aber nach Wert [=]

Sie können jedes der oben genannten Elemente in einer durch Komma getrennten Liste [x, &y] mischen.

Die Argumentliste

Die Argumentenliste ist die gleiche wie in jeder anderen C++-Funktion.

Der Funktionskörper

Der Code, der ausgeführt wird, wenn das Lambda tatsächlich aufgerufen wird.

Rückgabetyp Abzug

Wenn ein Lambda nur eine Rückgabeanweisung hat, kann der Rückgabetyp weggelassen werden und hat den impliziten Typ von decltype(return_statement).

Veränderlich

Wenn ein Lambda als veränderbar gekennzeichnet ist (z. B. []() mutable { }), darf es die Werte, die durch value erfasst wurden, verändern.

Anwendungsfälle

Die von der ISO-Norm definierte Bibliothek profitiert stark von Lambdas und erhöht die Benutzerfreundlichkeit um ein Vielfaches, da die Benutzer nun nicht mehr ihren Code mit kleinen Funktoren in einem zugänglichen Bereich überladen müssen.

C++14

In C++14 sind Lambdas durch verschiedene Vorschläge erweitert worden.

Initialisierte Lambda-Erfassungen

Ein Element der Capture-Liste kann nun mit = initialisiert werden. Dies ermöglicht die Umbenennung von Variablen und das Capturen durch Verschieben. Ein Beispiel aus dem Standard:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

und eines aus Wikipedia, das zeigt, wie man mit std::move einfangen kann:

auto ptr = std::make_unique(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Generische Lambdas

Lambdas können nun generisch sein (auto wäre hier äquivalent zu T, wenn T" ein Typvorlagenargument irgendwo im umgebenden Bereich wäre):

auto lambda = [](auto x, auto y) {return x + y;};

Verbesserte Return Type Deduction

C++14 erlaubt die Ableitung von Rückgabetypen für jede Funktion und beschränkt sie nicht auf Funktionen der Form Rückgabeausdruck;. Dies wird auch auf Lambdas ausgeweitet.

Kommentare (2)

Lambda-Ausdrücke werden normalerweise verwendet, um Algorithmen zu kapseln, damit sie an eine andere Funktion übergeben werden können. Es ist jedoch möglich, einen Lambda-Ausdruck sofort nach seiner Definition auszuführen:

[&](){ ...your code... }(); // immediately executed lambda expression

ist funktional äquivalent zu

{ ...your code... } // simple code block

Dies macht Lambda-Ausdrücke zu einem mächtigen Werkzeug für das Refactoring komplexer Funktionen. Sie beginnen damit, einen Codeabschnitt in eine Lambda-Funktion zu verpacken, wie oben gezeigt. Der Prozess der expliziten Parametrisierung kann dann schrittweise mit Zwischentests nach jedem Schritt durchgeführt werden. Sobald der Code-Block vollständig parametrisiert ist (wie durch das Entfernen des & demonstriert), können Sie den Code an eine externe Stelle verschieben und ihn zu einer normalen Funktion machen.

In ähnlicher Weise können Sie Lambda-Ausdrücke verwenden, um Variablen auf der Grundlage des Ergebnisses eines Algorithmus zu initialisieren...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

Als Möglichkeit der Partitionierung Ihrer Programmlogik kann es sogar nützlich sein, einen Lambda-Ausdruck als Argument an einen anderen Lambda-Ausdruck zu übergeben...

[&]( std::function algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Mit Lambda-Ausdrücken können Sie auch benannte verschachtelte Funktionen erstellen, was eine bequeme Möglichkeit sein kann, doppelte Logik zu vermeiden. Die Verwendung von benannten Lambdas ist auch etwas übersichtlicher (im Vergleich zu anonymen Inline-Lambdas), wenn eine nicht-triviale Funktion als Parameter an eine andere Funktion übergeben wird. *Hinweis: Vergessen Sie nicht das Semikolon nach der schließenden geschweiften Klammer.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Wenn die anschließende Profilerstellung einen erheblichen Initialisierungs-Overhead für das Funktionsobjekt ergibt, sollten Sie diese Funktion als normale Funktion umschreiben.

Kommentare (18)