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




lambda funktionen c++ (6)

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.


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 verwenden sein, besonders wenn der functor Sie anwenden möchten, einzigartig für die jeweilige Funktion ist.

#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 nicht an eine Template-Funktion in C ++ 03 übergeben werden.

Die neue Lösung

C ++ 11 stellt vor, dass Lambdas Ihnen erlauben, einen inline, anonymen Funktor zu schreiben, um die struct f zu ersetzen. 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, zB:

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, indem Sie -> 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 wollen, können Sie die capture-Klausel (das [] des Ausdrucks) verwenden, die in diesen Beispielen bisher nicht verwendet wurde, zB:

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, die Sie mit & bzw. angeben können:

  • [&epsilon] Erfassung durch Referenz
  • [&] erfasst alle im Lambda verwendeten Variablen als Referenz
  • [=] 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

Der generierte operator() ist standardmäßig const mit der Implikation, dass Captures const wenn Sie standardmäßig darauf zugreifen. Dies hat zur Folge, dass jeder Aufruf mit der gleichen Eingabe das gleiche Ergebnis liefert, jedoch können Sie das Lambda als mutable markieren, um zu fordern, dass der erzeugte operator() nicht const .


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 Capture-Liste, () 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 [x, &y] mischen.

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 return-Anweisung hat, kann der Rückgabetyp weggelassen werden und hat den impliziten Typ decltype(return_statement) .

Veränderlich

Wenn ein Lambda als änderbar gekennzeichnet ist (zB []() mutable { } ), dürfen die Werte, die durch den Wert erfasst wurden, mutiert werden.

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 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 äquivalent zu T , wenn T ein Typ-Template-Argument irgendwo im umgebenden Bereich wäre):

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

Verbesserte Rückgabetypabsetzung

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


Eine Lambda-Funktion ist eine anonyme Funktion, die Sie in-line erstellen. Es kann Variablen erfassen, wie einige erklärt haben (zB 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');
    });
}

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

What is a lambda expression?

Ein Lambda-Ausdruck , manchmal auch als Lambda- Funktion oder (genau genommen fälschlicherweise, aber umgangssprachlich) als Lambda bezeichnet , ist eine vereinfachte Notation zur Definition und Verwendung eines anonymen Funktionsobjekts . Anstatt eine benannte Klasse mit einem Operator () zu definieren, später ein Objekt dieser Klasse zu erstellen und schließlich aufzurufen, können wir eine Kurzschrift verwenden.

When would I use one?

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

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 Sie brauchen, können Sie den lambda expression wie folgt benennen:

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 Capture List oder lambda introducer : Wenn lambdas keinen Zugriff auf ihre lokale Umgebung benötigen, können wir sie verwenden.

Zitat aus dem Buch:

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

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

[&] : implizit durch Referenz erfassen. Alle lokalen Namen können verwendet werden. Auf alle lokalen Variablen wird als Referenz zugegriffen.

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

[capture-list]: explizite Erfassung; Die Capture-Liste ist die Liste der Namen der lokalen Variablen, die durch Referenz oder Wert erfasst (dh im Objekt gespeichert) werden. Variablen mit Namen, die von & stammen, werden als Referenz erfasst. Andere Variablen werden durch Wert erfasst. Eine Aufnahmeliste kann auch diese und Namen enthalten, gefolgt von ... als Elementen.

[&, capture-list] : erfasst implizit alle lokalen Variablen mit Namen, die nicht in der Liste aufgeführt sind. Die Aufnahmeliste kann dies enthalten. Aufgelistete Namen dürfen nicht vor & stehen. Variablen, die in der Erfassungsliste benannt sind, werden als Wert erfasst.

[=, capture-list] : erfasst implizit alle lokalen Variablen mit Namen, die nicht in der Liste aufgeführt sind. Die Aufnahmeliste darf das 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 durch Verweis erfasst wird und ein lokaler Name, der nicht von & stammt, immer vom Wert erfasst wird. Nur die Erfassung nach Referenz ermöglicht die Änderung von Variablen in der aufrufenden Umgebung.

Additional

Lambda expression

Zusätzliche Referenzen:


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 verschiedene Fälle tun. 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.


Antworten

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

A: Unter der Haube ist es Objekt der automatisch generierten Klasse mit dem Überladen von operator () const . Ein solches Objekt wird als Closure bezeichnet und vom Compiler erstellt. 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 Syntax Zucker wie Operatoren überladen statt Funktionen für benutzerdefinierte hinzufügen, subrtact Operationen ... Aber es speichern mehr Zeilen von nicht benötigtem Code, 1-3 Zeilen echte Logik zu einigen Klassen und etc. zu wickeln! 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 auf eine Variable mit statischer Speicherdauer in Lamdas verweisen. Sie alle sind gefangen.

1.2. Sie können lamda 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 ist eine vorhandene 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 ist eine vorhandene Notation, um alle nicht statischen Vars nach Wert oder nach Referenz zu erfassen und smth anzugeben. Mehr. Beispiele: Erfassen Sie alle nicht statischen Variablen nach Wert, aber durch Referenzerfassung 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 Elementvariablen des Objekts erfassen.

4. Konversionen

4.1. Lambda ist kein Funktionszeiger und keine anonyme Funktion , sondern kann implizit zum Funktionszeiger konvertiert werden.

ps

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




c++-faq