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


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

Ein paar Beispiele und Anwendungsfälle wären nützlich.


1195
2017-10-02 14:58


Ursprung


Antworten:


Das Problem

C ++ enthält nützliche generische Funktionen wie std::for_each und std::transform, was sehr praktisch sein kann. Leider können sie auch ziemlich umständlich zu bedienen sein, vor allem wenn die Funktor Sie möchten nur anwenden, ist einzigartig für die jeweilige Funktion.

#include <algorithm>
#include <vector>

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

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

Wenn Sie f nur einmal an diesem bestimmten Ort verwenden, erscheint es übertrieben, eine ganze Klasse zu schreiben, nur um etwas Triviales und Einmaliges zu tun.

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

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

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

Die neue Lösung

C ++ 11 stellt vor, dass lambdas Ihnen erlauben, einen inline, anonymen Funktor zu schreiben, der den struct f. Für kleine, einfache Beispiele kann dies sauberer zu lesen sein (es hält alles an einem Ort) und möglicherweise einfacher zu warten, zum Beispiel in der einfachsten Form:

void func3(std::vector<int>& 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 Lambda für Sie abgeleitet, z.

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

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

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

Um dies zu beheben, können Sie explizit einen Rückgabetyp für eine Lambda-Funktion angeben -> T:

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

"Erfassen" von Variablen

Bis jetzt haben wir nichts anderes als das verwendet, was an das Lambda weitergegeben wurde, aber wir können auch andere Variablen innerhalb des Lambda verwenden. Wenn Sie auf andere Variablen zugreifen möchten, können Sie die capture - Klausel (die [] des Ausdrucks), der in diesen Beispielen bisher nicht verwendet wurde, z.

void func5(std::vector<double>& 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 nach Referenz als auch nach Wert erfassen, den Sie mit angeben können & und = beziehungsweise:

  • [&epsilon] erfassen durch Verweis
  • [&] erfasst alle Variablen, die im Lambda als Referenz verwendet werden
  • [=] erfasst alle im Lambda verwendeten Variablen nach Wert
  • [&, epsilon] Erfasst Variablen wie bei [&], aber Epsilon nach Wert
  • [=, &epsilon] Erfasst Variablen wie mit [=], aber Epsilon als Referenz

Das erzeugte operator() ist const Standardmäßig mit der Implikation, dass Captures sein werden const wenn Sie standardmäßig auf sie zugreifen. Dies hat zur Folge, dass jeder Aufruf mit der gleichen Eingabe das gleiche Ergebnis liefert, wie auch immer Sie es können Markiere das Lambda als mutable verlangen, dass die operator() das ist nicht produziert const.


1206
2017-10-02 15:21



Was ist eine Lambda-Funktion?

Das C ++ - Konzept einer Lambda-Funktion stammt aus dem Lambda-Kalkül und der funktionalen Programmierung. Ein Lambda ist eine unbenannte Funktion, die (in der eigentlichen 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 Aufnahmeliste, () die Argumentliste und {} der Funktionskörper.

Die Aufnahmeliste

Die Aufnahmeliste definiert, was von außerhalb des Lambda innerhalb des Funktionskörpers und wie verfügbar sein sollte. Es kann entweder sein:

  1. ein Wert: [x]
  2. eine Referenz [& x]
  3. jede Variable, die sich derzeit im Referenzbereich befindet [&]
  4. Wie 3, aber nach Wert [=]

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

Die Argumentliste

Die Argumentliste ist dieselbe 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 { }) Es ist erlaubt, die Werte, die durch Wert erfasst wurden, zu mutieren.

Anwendungsfälle

Die durch den ISO-Standard definierte Bibliothek profitiert stark von lambdas und erhöht die Verwendbarkeit um mehrere Balken, da Benutzer ihren Code nicht mit kleinen Funktoren in einem zugänglichen Bereich überladen müssen.

C ++ 14

In C ++ wurden 14 Lambdas um verschiedene Vorschläge erweitert.

Initialisierte Lambda-Captures

Ein Element der Erfassungsliste kann jetzt mit initialisiert werden =. Dies ermöglicht das Umbenennen von Variablen und das Erfassen 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 eine aus Wikipedia entnommen, die zeigt, wie man mit fängt std::move:

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

Generische Lambdas

Lambdas können jetzt generisch sein (auto wäre gleichbedeutend mit T hier, wenn T War ein Typvorlagenargument irgendwo im umgebenden Umfang):

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

Verbesserte Rückgabetypabsetzung

C ++ 14 ermöglicht abgeleitete Rückgabetypen für jede Funktion und beschränkt sie nicht auf Funktionen des Formulars return expression;. Dies gilt auch für Lambdas.


718
2017-10-02 15:43



Lambda-Ausdrücke werden typischerweise zum Verkapseln von Algorithmen verwendet, so dass sie an eine andere Funktion übergeben werden können. Jedoch, Es ist möglich, ein Lambda sofort nach der Definition auszuführen:

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

ist funktional äquivalent zu

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

Dies macht Lambda-Ausdrücke Ein leistungsstarkes Werkzeug zum Refactoring komplexer Funktionen. Sie beginnen mit dem Umbruch eines Codeabschnitts in einer Lambda-Funktion, wie oben gezeigt. Der Prozess der expliziten Parametrisierung kann dann schrittweise mit Zwischenprüfungen nach jedem Schritt durchgeführt werden. Sobald Sie den Codeblock vollständig parametrisiert haben (wie durch das Entfernen des &), können Sie den Code an einen externen Speicherort verschieben und zu einer normalen Funktion machen.

In ähnlicher Weise können Sie Lambda-Ausdrücke verwenden initialisieren Variablen basierend auf dem Ergebnis eines Algorithmus...

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

Wie eine Möglichkeit, Ihre Programmlogik zu partitionierenVielleicht finden Sie es sogar nützlich, einen Lambda-Ausdruck als Argument an einen anderen Lambda-Ausdruck zu übergeben ...

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

Mit Lambda-Ausdrücken können Sie auch named erstellen verschachtelte FunktionenDies kann eine bequeme Möglichkeit sein, doppelte Logik zu vermeiden. Die Verwendung von Named Lambdas ist auch etwas einfacher für die Augen (verglichen mit 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 bei der nachfolgenden Profilerstellung ein signifikanter Initialisierungsaufwand für das Funktionsobjekt festgestellt wird, können Sie dies möglicherweise als normale Funktion neu schreiben.


151
2018-03-01 08:08



Antworten

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

A: Unter der Haube ist es das Objekt einer automatisch generierten Klasse mit Überladung operator () const. Ein solches Objekt wird aufgerufen Schließung und erstellt von Compiler. Dieses "Closure" -Konzept ist nahe am Bind-Konzept von C ++ 11. Aber lambdas erzeugen normalerweise besseren Code. Und Anrufe durch Schließungen ermöglichen vollständige Inlining.

F: Wann würde ich einen benutzen?

A: Um "einfache und kleine Logik" zu definieren und den Compiler zu veranlassen, die Generierung von der vorherigen Frage durchzuführen. Sie geben einem Compiler einige Ausdrücke, die Sie innerhalb von operator () haben wollen. Alle anderen Stuff Compiler wird für Sie generieren.

F: Welche Klasse von Problemen lösen sie, die vor ihrer Einführung nicht möglich war?

A: Es ist eine Art von Syntax Sugar wie Operatoren überladen statt Funktionen für benutzerdefinierte hinzufügen, subrtact Operationen ... Aber es speichern mehr Zeilen von nicht benötigtem Code, um 1-3 Zeilen echte Logik in einige Klassen und etc. zu verpacken! Einige Ingenieure denken, dass, wenn die Anzahl der Zeilen kleiner ist, es eine geringere Chance gibt, Fehler zu machen (ich denke auch)

Anwendungsbeispiel

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Extras über Lambdas, nicht von Frage abgedeckt. Ignorieren Sie diesen Abschnitt, wenn Sie nicht interessiert sind

1. Erfasste Werte Was Sie zu erfassen haben

1.1. Sie können in lambdas auf eine Variable mit statischer Speicherdauer verweisen. Sie alle sind gefangen.

1.2. Sie können Lambda für Capture-Werte "nach Wert" verwenden. In diesem Fall werden die erfassten Variablen in das Funktionsobjekt (Closure) kopiert.

[captureVar1,captureVar2](int arg1){}

1.3. Sie können Referenz aufnehmen. & - in diesem Zusammenhang bedeuten Referenz, nicht Zeiger.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Es existiert eine Notation, um alle nicht-statischen Variablen nach Wert oder durch Referenz zu erfassen

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Es existiert eine Notation, um alle nicht-statischen Variablen nach Wert oder durch Bezugnahme zu erfassen und etw anzugeben. Mehr. Beispiele: Erfassen Sie alle nicht-statischen Variablen nach Wert, aber durch Referenzerfassung von Param2

[=,&Param2](int arg1){} 

Erfassen Sie alle nicht-statischen Variablen als Referenz, aber durch Wert erfassen Param2

[&,Param2](int arg1){} 

2. Rückgabetyp Abzug

2.1. Der Lambda-Rückgabetyp kann abgeleitet werden, wenn Lambda ein Ausdruck ist. Oder Sie können es explizit angeben.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Wenn Lambda mehr als einen Ausdruck hat, muss der Rückgabetyp über den folgenden Rückgabetyp angegeben werden.   Eine ähnliche Syntax kann auch auf automatische Funktionen und Elementfunktionen angewendet werden

3. Erfasste Werte Was du nicht erfassen kannst

3.1. Sie können nur lokale Variablen und keine Elementvariable des Objekts erfassen.

4. Konversionen

4.1. Lambda ist kein Funktionszeiger und keine anonyme Funktion, kann aber implizit in einen Funktionszeiger umgewandelt werden.

p.s. 

  1. Weitere Informationen zur Lambda-Grammatik finden Sie im Arbeitsentwurf für Programmiersprache C ++ # 337, 2012-01-16, 5.1.2. Lambda-Ausdrücke, S.88

  2. In C ++ 14 wurde die zusätzliche Funktion hinzugefügt, die als "init capture" bezeichnet wurde. Es erlaubt, willkürlich Deklaration von Schließungsdatenmitgliedern durchzuführen:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    

30
2018-06-03 16:40



Eine Lambda-Funktion ist eine anonyme Funktion, die Sie in-line erstellen. Es kann Variablen erfassen, wie einige erläutert haben (z. http://www.stroustrup.com/C++11FAQ.html#Lambda) aber es gibt einige Einschränkungen. Zum Beispiel, wenn es eine Callback-Schnittstelle wie diese gibt,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

Sie können eine Funktion an Ort und Stelle schreiben, um sie wie die unten angegebene zu verwenden:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Aber du kannst das nicht tun:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

wegen der Beschränkungen im C ++ 11 Standard. Wenn Sie Captures verwenden möchten, müssen Sie sich auf die Bibliothek und

#include <functional> 

(oder ein anderer STL-Bibliothek-ähnlicher Algorithmus, um es indirekt zu erhalten) und dann mit der std :: -Funktion arbeiten, anstatt normale Funktionen als Parameter wie folgt zu übergeben:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

12
2018-03-10 22:36



Eine der besten Erklärungen von lambda expression wird vom Autor von C ++ gegeben Bjarne Stroustrup in seinem Buch ***The C++ Programming Language*** Kapitel 11 (ISBN-13: 978-0321563842):

What is a lambda expression? 

EIN Lambda-Ausdruck, manchmal auch als a bezeichnet Lambda   Funktion oder (genau genommen falsch, aber umgangssprachlich) als    Lambda, ist eine vereinfachte Notation zum Definieren und Verwenden eines anonymes Funktionsobjekt. Anstatt eine benannte Klasse mit einem Operator () zu definieren, wird später ein Objekt dieser Klasse erstellt und schließlich   wenn wir es aufrufen, können wir eine Kurzschrift verwenden.

When would I use one?

Dies ist besonders nützlich, wenn Sie eine Operation als übergeben möchten   Argument zu einem Algorithmus. Im Kontext von grafischen Benutzeroberflächen   (und anderswo) werden solche Operationen oft als bezeichnet Rückrufe.

What class of problem do they solve that wasn't possible prior to their introduction?

Hier kann ich sagen, dass jede Aktion mit Lambda-Ausdruck ohne sie gelöst werden kann, aber mit viel mehr Code und viel größerer Komplexität. Lambda Ausdruck Dies ist die Art der Optimierung für Ihren Code und eine Möglichkeit, sie attraktiver zu machen. Wie traurig von Stroustup:

effektive Möglichkeiten der Optimierung

Some examples

über Lambda-Ausdruck

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

oder über Funktion

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

oder auch

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

Wenn du brauchst, kannst du es benennen lambda expression Wie unten:

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

Oder nehmen Sie ein anderes einfaches Beispiel an

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

wird als nächstes generieren

0

1

0

1

0

1

0

1

0

1

0 sortiertex - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - Das ist Aufnahmeliste oder lambda introducer: ob lambdas benötigen keinen Zugriff auf ihre lokale Umgebung, wir können es verwenden.

Zitat aus dem Buch:

Das erste Zeichen eines Lambda-Ausdrucks ist immer [. Ein Lambda   Einführer kann verschiedene Formen annehmen:

[]: eine leere Erfassungsliste Dies   Impliziert, dass keine lokalen Namen aus dem umgebenden Kontext verwendet werden können   im Lambda-Körper. Für solche Lambda-Ausdrücke werden Daten von erhalten   Argumente oder von nicht lokalen Variablen.

[&]: implizit erfassen von   Referenz. Alle lokalen Namen können verwendet werden. Alle lokalen Variablen sind   Zugriff über Referenz

[=]: implizit durch Wert erfassen. Alles lokal   Namen können verwendet werden. Alle Namen beziehen sich auf Kopien der lokalen Variablen   zum Zeitpunkt des Aufrufs des Lambda-Ausdrucks genommen.

[Capture-Liste]:  explizite Erfassung; die Erfassungsliste ist die Liste von Namen von lokalen Variablen, die durch Bezugnahme oder nach Wert erfasst (d. h. in dem Objekt gespeichert) werden sollen. Variablen mit Namen, denen & vorangestellt sind, werden von erfasst   Referenz. Andere Variablen werden durch Wert erfasst. Eine Aufnahmeliste kann   enthalten Sie auch diese und Namen gefolgt von ... als Elemente.

[&, Capture-Liste]: Implizit erfassen Sie alle lokalen Variablen mit Namen, die in der Liste nicht erwähnt sind. Die Aufnahmeliste kann dies enthalten. Aufgelistete Namen dürfen nicht vor & stehen. Variablen, die in der   Aufnahmeliste werden nach Wert erfasst.

[=, Aufnahmeliste]: Implizit erfassen Sie alle lokalen Variablen mit Namen, die nicht in der Liste aufgeführt sind. Die Erfassungsliste kann dies nicht enthalten. Den aufgelisteten Namen muss & vorangestellt sein. Die in der Erfassungsliste genannten Variablen werden durch Referenz erfasst.

Beachten Sie, dass ein lokaler Name, dem & vorangestellt ist, immer von erfasst wird   Referenz und ein lokaler Name, der nicht von & vorgegeben wird, wird immer von erfasst   Wert. Nur durch Referenz erfassen erlaubt die Modifikation von Variablen in   die aufrufende Umgebung.

Additional

Lambda expression Format

enter image description here

Zusätzliche Referenzen:


6
2017-11-09 11:02



Ein Problem, das es löst: Code einfacher als Lambda für einen Aufruf im Konstruktor, der eine Ausgabeparameterfunktion zum Initialisieren eines Konstanten verwendet

Sie können ein const-Mitglied Ihrer Klasse mit einem Aufruf einer Funktion initialisieren, die ihren Wert festlegt, indem sie ihre Ausgabe als Ausgabeparameter zurückgibt.


1
2018-06-27 00:38



Nun, ein praktischer Nutzen, den ich herausgefunden habe, ist die Reduzierung des Kessels. Beispielsweise:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

Ohne Lambda müssen Sie möglicherweise etwas für etwas anderes tun bsize Fälle. Natürlich könnten Sie eine Funktion erstellen, aber was, wenn Sie die Verwendung im Rahmen der Soul-Benutzerfunktion einschränken möchten? Die Natur von Lambda erfüllt diese Anforderung und ich benutze sie für diesen Fall.


1
2017-11-23 09:16