Suche…
Syntax
- [ default-capture , capture-list ] ( Argumentliste ) veränderliche Attribute für Throw-Spezifikation -> Rückgabetyp { lambda-body } // Reihenfolge der Lambda-Bezeichner und Attribute
- [ Erfassungsliste ] ( Argumentliste ) { Lambda-Body } // Allgemeine Lambda-Definition.
- [=] ( argument-list ) { lambda-body } // Erfasst alle benötigten lokalen Variablen nach Wert.
- [&] ( argument-list ) { lambda-body } // Erfasst alle benötigten lokalen Variablen per Verweis.
- [ capture-list ] { lambda-body } // Argumentliste und Angaben können weggelassen werden.
Parameter
Parameter | Einzelheiten |
---|---|
Standard-Capture | Gibt an, wie alle nicht aufgelisteten Variablen erfasst werden. Kann = (Erfassung nach Wert) oder & (Aufnahme nach Referenz) sein. Ohne Angabe sind nicht aufgeführte Variablen im Lambda-Body nicht erreichbar . Das Standard-Capture muss vor der Capture-Liste stehen . |
Capture-Liste | Gibt an, wie lokale Variablen im Lambda-Body verfügbar gemacht werden . Variablen ohne Präfix werden nach Wert erfasst. Mit & Variablen werden durch Verweis erfasst. Innerhalb einer Klassenmethode, this kann verwendet werden , um alle zugänglich ihre Mitglieder durch Bezugnahme zu machen. Auf nicht aufgelistete Variablen kann nicht zugegriffen werden, es sei denn, der Liste ist eine Standarderfassung vorangestellt. |
Argumentliste | Gibt die Argumente der Lambda-Funktion an. |
veränderlich | (optional) Normalerweise werden durch Wert erfasste Variablen const . Durch die Angabe der mutable Eigenschaft werden sie nicht konstant. Änderungen an diesen Variablen werden zwischen Aufrufen beibehalten. |
Wurfspezifikation | (optional) Gibt das Auslöseverhalten der Lambda-Funktion an. Zum Beispiel: noexcept oder throw(std::exception) . |
Attribute | (optional) Beliebige Attribute für die Lambda-Funktion. Wenn zum Beispiel der Lambda-Body immer eine Ausnahme [[noreturn]] kann [[noreturn]] verwendet werden. |
-> Rückgabetyp | (optional) Gibt den Rückgabetyp der Lambda-Funktion an. Erforderlich, wenn der Rückgabetyp nicht vom Compiler bestimmt werden kann. |
Lambda-Körper | Ein Codeblock, der die Implementierung der Lambda-Funktion enthält. |
Bemerkungen
C ++ 17 (der aktuelle Entwurf) führt constexpr
Lambdas ein, im Wesentlichen Lambdas, die zur Kompilierzeit ausgewertet werden können. Ein Lambda ist automatisch constexpr
wenn es die Anforderungen von constexpr
erfüllt. Sie können es jedoch auch mit dem Schlüsselwort constexpr
angeben:
//Explicitly define this lambdas as constexpr
[]() constexpr {
//Do stuff
}
Was ist ein Lambda-Ausdruck?
Mit einem Lambda-Ausdruck können Sie einfache Funktionsobjekte auf einfache Weise erstellen. Ein Lambda-Ausdruck ist ein Prvalue, dessen Ergebnisobjekt Schließobjekt genannt wird , das sich wie ein Funktionsobjekt verhält.
Der Name "Lambda-Ausdruck" stammt von Lambda-Kalkül , einem mathematischen Formalismus, der in den 1930er Jahren von Alonzo Church zur Untersuchung von Fragen zu Logik und Berechenbarkeit erfunden wurde. Lambda-Kalkül bildete die Grundlage von LISP , einer funktionalen Programmiersprache. Im Vergleich zu Lambda-Kalkül und LISP weisen C ++ - Lambda-Ausdrücke die Eigenschaften auf, unbenannt zu sein und Variablen aus dem umgebenden Kontext zu erfassen, ihnen fehlt jedoch die Fähigkeit, Funktionen auszuführen und zurückzugeben.
Ein Lambda-Ausdruck wird häufig als Argument für Funktionen verwendet, die ein aufrufbares Objekt enthalten. Dies kann einfacher sein als das Erstellen einer benannten Funktion, die nur verwendet wird, wenn sie als Argument übergeben wird. In solchen Fällen werden im Allgemeinen Lambda-Ausdrücke bevorzugt, da sie es ermöglichen, die Funktionsobjekte inline zu definieren.
Ein Lambda besteht normalerweise aus drei Teilen: einer Erfassungsliste []
, einer optionalen Parameterliste ()
und einem Hauptteil {}
, die alle leer sein können:
[](){} // An empty lambda, which does and returns nothing
Aufnahmeliste
[]
ist die Erfassungsliste . Auf Variablen des umschließenden Bereichs kann standardmäßig kein Lambda zugreifen. Durch das Erfassen einer Variablen ist sie innerhalb des Lambda als Kopie oder als Referenz verfügbar . Die erfassten Variablen werden Teil des Lambda. Im Gegensatz zu Funktionsargumenten müssen sie beim Aufruf des Lambda nicht übergeben werden.
int a = 0; // Define an integer variable
auto f = []() { return a*9; }; // Error: 'a' cannot be accessed
auto f = [a]() { return a*9; }; // OK, 'a' is "captured" by value
auto f = [&a]() { return a++; }; // OK, 'a' is "captured" by reference
// Note: It is the responsibility of the programmer
// to ensure that a is not destroyed before the
// lambda is called.
auto b = f(); // Call the lambda function. a is taken from the capture list and not passed here.
Parameterliste
()
ist die Parameterliste , die fast die gleiche ist wie bei regulären Funktionen. Wenn das Lambda keine Argumente akzeptiert, können diese Klammern weggelassen werden (es sei denn, Sie müssen das Lambda als mutable
deklarieren). Diese beiden Lambdas sind gleichwertig:
auto call_foo = [x](){ x.foo(); };
auto call_foo2 = [x]{ x.foo(); };
Die Parameterliste kann anstelle von tatsächlichen Typen den Platzhaltertyp auto
verwenden. Dadurch verhält sich dieses Argument wie ein Vorlagenparameter einer Funktionsvorlage. Die folgenden Lambdas sind gleichwertig, wenn Sie einen Vektor in generischem Code sortieren möchten:
auto sort_cpp11 = [](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs) { return lhs < rhs; };
auto sort_cpp14 = [](const auto &lhs, const auto &rhs) { return lhs < rhs; };
Funktionskörper
{}
ist der Körper , der dem von regulären Funktionen entspricht.
Ein Lambda anrufen
Das Ergebnisobjekt eines Lambda-Ausdrucks ist eine Schließung , die mit dem operator()
aufgerufen werden kann (wie bei anderen Funktionsobjekten):
int multiplier = 5;
auto timesFive = [multiplier](int a) { return a * multiplier; };
std::out << timesFive(2); // Prints 10
multiplier = 15;
std::out << timesFive(2); // Still prints 2*5 == 10
Rückgabetyp
Standardmäßig wird der Rückgabetyp eines Lambda-Ausdrucks abgeleitet.
[](){ return true; };
In diesem Fall ist der Rückgabetyp bool
.
Sie können den Rückgabetyp auch manuell mit der folgenden Syntax angeben:
[]() -> bool { return true; };
Mutable Lambda
Objekte, die im Lambda-Wert erfasst werden, sind standardmäßig unveränderlich. Dies liegt daran, dass der operator()
des generierten Abschlussobjekts standardmäßig const
ist.
auto func = [c = 0](){++c; std::cout << c;}; // fails to compile because ++c
// tries to mutate the state of
// the lambda.
Das Ändern kann mit dem Schlüsselwort mutable
erlaubt werden, wodurch der operator()
des näheren Objekts nicht- const
:
auto func = [c = 0]() mutable {++c; std::cout << c;};
Bei Verwendung zusammen mit dem Rückgabetyp wird das mutable
davor mutable
.
auto func = [c = 0]() mutable -> int {++c; std::cout << c; return c;};
Ein Beispiel zur Veranschaulichung der Nützlichkeit von Lambdas
Vor C ++ 11:
// Generic functor used for comparison
struct islessthan
{
islessthan(int threshold) : _threshold(threshold) {}
bool operator()(int value) const
{
return value < _threshold;
}
private:
int _threshold;
};
// Declare a vector
const int arr[] = { 1, 2, 3, 4, 5 };
std::vector<int> vec(arr, arr+5);
// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold));
Seit C ++ 11:
// Declare a vector
std::vector<int> vec{ 1, 2, 3, 4, 5 };
// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value) { return value < threshold; });
Angabe des Rückgabetyps
Bei Lambdas mit einer einzelnen return-Anweisung oder mehreren return-Anweisungen, deren Ausdrücke denselben Typ haben, kann der Compiler den Rückgabetyp ableiten:
// Returns bool, because "value > 10" is a comparison which yields a Boolean result
auto l = [](int value) {
return value > 10;
}
Bei Lambdas mit mehreren return-Anweisungen verschiedener Typen kann der Compiler den Rückgabetyp nicht ableiten:
// error: return types must match if lambda has unspecified return type
auto l = [](int value) {
if (value < 10) {
return 1;
} else {
return 1.5;
}
};
In diesem Fall müssen Sie den Rückgabetyp explizit angeben:
// The return type is specified explicitly as 'double'
auto l = [](int value) -> double {
if (value < 10) {
return 1;
} else {
return 1.5;
}
};
Die Regeln hierfür entsprechen den Regeln für die auto
Typabzug. Lambdas ohne explizit angegebene Rückgabetypen geben niemals Referenzen zurück. Wenn ein Referenztyp gewünscht wird, muss er auch explizit angegeben werden:
auto copy = [](X& x) { return x; }; // 'copy' returns an X, so copies its input
auto ref = [](X& x) -> X& { return x; }; // 'ref' returns an X&, no copy
Erfassung nach Wert
Wenn Sie den Namen der Variablen in der Erfassungsliste angeben, erfasst der Lambda ihn nach Wert. Dies bedeutet, dass der generierte Abschlusstyp für das Lambda eine Kopie der Variablen speichert. Dies erfordert auch , dass die Art Variable sein copy-konstruierbar:
int a = 0;
[a]() {
return a; // Ok, 'a' is captured by value
};
auto p = std::unique_ptr<T>(...);
[p]() { // Compile error; `unique_ptr` is not copy-constructible
return p->createWidget();
};
Ab C ++ 14 können Variablen vor Ort initialisiert werden. Auf diese Weise können nur Arten von Bewegungen im Lambda erfasst werden.
auto p = std::make_unique<T>(...);
[p = std::move(p)]() {
return p->createWidget();
};
Obwohl ein Lambda Variablen nach Wert erfasst, wenn sie durch ihren Namen angegeben werden, können diese Variablen nicht standardmäßig innerhalb des Lambda-Körpers geändert werden. Dies liegt daran, dass der Schließungstyp den Lambda-Body in eine Deklaration von operator() const
.
Die const
gilt für Zugriffe auf Membervariablen des Abschlusstyps und erfasste Variablen, die Mitglieder des Closures sind (alle gegenteiligen Anschein)
int a = 0;
[a]() {
a = 2; // Illegal, 'a' is accessed via `const`
decltype(a) a1 = 1;
a1 = 2; // valid: variable 'a1' is not const
};
Um das const
zu entfernen, müssen Sie das Schlüsselwort mutable
im Lambda angeben:
int a = 0;
[a]() mutable {
a = 2; // OK, 'a' can be modified
return a;
};
Da a
durch Wert erfasst wurde, wirken sich Änderungen durch Aufruf des Lambda nicht auf a
. Der Wert von a
wurde in den Lambda kopiert , wenn es gebaut wurde, so die Kopie des Lambda a
ist unabhängig von der externen a
Variable.
int a = 5 ;
auto plus5Val = [a] (void) { return a + 5 ; } ;
auto plus5Ref = [&a] (void) {return a + 5 ; } ;
a = 7 ;
std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ;
// The result will be "7, value 10, reference 12"
Generalisierte Erfassung
Lambdas können Ausdrücke und nicht nur Variablen erfassen. Auf diese Weise können Lambdas Nur-Bewegung-Typen speichern:
auto p = std::make_unique<T>(...);
auto lamb = [p = std::move(p)]() //Overrides capture-by-value of `p`.
{
p->SomeFunc();
};
Dadurch wird die äußere p
Variable in die Lambda-Capture-Variable, auch als p
bezeichnet, p
. lamb
besitzt nun den von make_unique
zugewiesenen make_unique
. Da der Abschluss einen Typ enthält, der nicht kopierbar ist, bedeutet dies, dass lamb
selbst nicht kopierbar ist. Es kann aber verschoben werden:
auto lamb_copy = lamb; //Illegal
auto lamb_move = std::move(lamb); //legal.
Jetzt besitzt lamb_move
die Erinnerung.
Beachten Sie, dass für std::function<>
die gespeicherten Werte kopierbar sein müssen. Sie können Ihre eigene move-only-erfordernde std::function
schreiben oder das Lambda einfach in einen shared_ptr
Wrapper shared_ptr
:
auto shared_lambda = [](auto&& f){
return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))]
(auto&&...args)->decltype(auto) {
return (*spf)(decltype(args)(args)...);
};
};
auto lamb_shared = shared_lambda(std::move(lamb_move));
nimmt unseren Einzug nur Lambda und stopft seinen Zustand in einen gemeinsamen Zeiger gibt dann eine Lambda, die kopiert werden und dann in einer gespeicherten std::function
oder ähnlichem.
Die generalisierte Erfassung verwendet den auto
Typabzug für den Variablentyp. Diese Captures werden standardmäßig als Werte deklariert, sie können jedoch auch Referenzen sein:
int a = 0;
auto lamb = [&v = a](int add) //Note that `a` and `v` have different names
{
v += add; //Modifies `a`
};
lamb(20); //`a` becomes 20.
Die Generalize-Erfassung muss überhaupt keine externe Variable erfassen. Es kann einen beliebigen Ausdruck erfassen:
auto lamb = [p = std::make_unique<T>(...)]()
{
p->SomeFunc();
}
Dies ist nützlich, um Lambdas beliebige Werte zu geben, die sie speichern und möglicherweise modifizieren können, ohne sie extern zum Lambda deklarieren zu müssen. Das ist natürlich nur sinnvoll, wenn Sie nicht auf diese Variablen zugreifen möchten, nachdem das Lambda seine Arbeit beendet hat.
Nach Referenz erfassen
Wenn Sie dem Namen einer lokalen Variable ein &
voranstellen, wird die Variable als Referenz erfasst. Konzeptionell bedeutet dies, dass der Schließungstyp des Lambda eine Referenzvariable hat, die als Referenz auf die entsprechende Variable von außerhalb des Gültigkeitsbereichs des Lambda initialisiert wird. Jede Verwendung der Variablen im Lambda-Body bezieht sich auf die ursprüngliche Variable:
// Declare variable 'a'
int a = 0;
// Declare a lambda which captures 'a' by reference
auto set = [&a]() {
a = 1;
};
set();
assert(a == 1);
Das Schlüsselwort mutable
wird nicht benötigt, da a
selbst nicht const
.
Erfassen durch Referenz bedeutet natürlich, dass das Lambda nicht dem Umfang der erfassten Variablen entgehen darf . Sie können also Funktionen aufrufen, die eine Funktion übernehmen, aber Sie dürfen keine Funktion aufrufen, die das Lambda über den Bereich Ihrer Referenzen hinaus speichert . Und du darfst das Lambda nicht zurückgeben.
Standarderfassung
Auf lokale Variablen, die nicht explizit in der Erfassungsliste angegeben sind, kann standardmäßig nicht vom Lambda-Body aus zugegriffen werden. Es ist jedoch möglich, durch den Lambda-Body benannte Variablen implizit zu erfassen:
int a = 1;
int b = 2;
// Default capture by value
[=]() { return a + b; }; // OK; a and b are captured by value
// Default capture by reference
[&]() { return a + b; }; // OK; a and b are captured by reference
Die explizite Erfassung kann immer noch neben der impliziten Standarderfassung erfolgen. Die explizite Erfassungsdefinition überschreibt die Standarderfassung:
int a = 0;
int b = 1;
[=, &b]() {
a = 2; // Illegal; 'a' is capture by value, and lambda is not 'mutable'
b = 2; // OK; 'b' is captured by reference
};
Generische Lambdas
Lambda-Funktionen können Argumente beliebigen Typs annehmen. Dadurch kann ein Lambda generischer sein:
auto twice = [](auto x){ return x+x; };
int i = twice(2); // i == 4
std::string s = twice("hello"); // s == "hellohello"
Dies wird in C ++ implementiert, indem der operator()
des Abschlusstyps eine Vorlagenfunktion wird. Der folgende Typ hat das gleiche Verhalten wie der obige Lambda-Verschluss:
struct _unique_lambda_type
{
template<typename T>
auto operator() (T x) const {return x + x;}
};
Nicht alle Parameter in einem generischen Lambda müssen generisch sein:
[](auto x, int y) {return x + y;}
Hier wird x
basierend auf dem ersten Funktionsargument abgeleitet, während y
immer int
.
Generische Lambdas können auch Argumente als Referenz verwenden, wobei die üblichen Regeln für auto
und &
. Wenn ein generischer Parameter als auto&&
, ist dies eine Weiterleitungsreferenz auf das übergebene Argument und keine rvalue- Referenz :
auto lamb1 = [](int &&x) {return x + 5;};
auto lamb2 = [](auto &&x) {return x + 5;};
int x = 10;
lamb1(x); // Illegal; must use `std::move(x)` for `int&&` parameters.
lamb2(x); // Legal; the type of `x` is deduced as `int&`.
Lambda-Funktionen können variadisch sein und ihre Argumente perfekt weiterleiten:
auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);};
oder:
auto lam = [](auto&&... args){return f(decltype(args)(args)...);};
was nur "richtig" mit Variablen vom Typ auto&&
funktioniert.
Ein guter Grund, generische Lambdas zu verwenden, ist der Besuch der Syntax.
boost::variant<int, double> value;
apply_visitor(value, [&](auto&& e){
std::cout << e;
});
Hier besuchen wir polymorph. In anderen Zusammenhängen sind die Namen des Typs, den wir übergeben, nicht interessant:
mutex_wrapped<std::ostream&> os = std::cout;
os.write([&](auto&& os){
os << "hello world\n";
});
Das Wiederholen des Typs von std::ostream&
ist hier Rauschen. Es wäre so, als müssten Sie bei jeder Verwendung den Typ einer Variablen erwähnen. Hier schaffen wir einen Besucher, aber keinen polymorphen Besucher; auto
wird aus dem gleichen Grund verwendet, in dem Sie auto
in einer for(:)
Schleife verwenden können.
Konvertierung in Funktionszeiger
Wenn die Capture-Liste eines Lambdas leer ist, wird das Lambda implizit in einen Funktionszeiger konvertiert, der dieselben Argumente verwendet und denselben Rückgabetyp zurückgibt:
auto sorter = [](int lhs, int rhs) -> bool {return lhs < rhs;};
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter; // implicit conversion
Eine solche Konvertierung kann auch mit einem unären Plus-Operator durchgeführt werden:
func_ptr sorter_func2 = +sorter; // enforce implicit conversion
Das Aufrufen dieses Funktionszeigers verhält sich genauso wie das Aufrufen von operator()
auf dem Lambda. Dieser Funktionszeiger ist in keiner Weise von der Existenz des Quell-Lambda-Verschlusses abhängig. Sie kann daher den Lambda-Verschluss überleben.
Diese Funktion ist vor allem für die Verwendung von Lambdas mit APIs hilfreich, die Funktionszeiger anstelle von C ++ - Funktionsobjekten verwenden.
Die Konvertierung in einen Funktionszeiger ist auch für generische Lambdas mit leerer Erfassungsliste möglich. Bei Bedarf wird ein Vorlagenargumentenabzug verwendet, um die richtige Spezialisierung auszuwählen.
auto sorter = [](auto lhs, auto rhs) { return lhs < rhs; };
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter; // deduces int, int
// note however that the following is ambiguous
// func_ptr sorter_func2 = +sorter;
Klasse Lambdas und Capture davon
Ein in einer Member-Funktion einer Klasse ausgewerteter Lambda-Ausdruck ist implizit ein Freund dieser Klasse:
class Foo
{
private:
int i;
public:
Foo(int val) : i(val) {}
// definition of a member function
void Test()
{
auto lamb = [](Foo &foo, int val)
{
// modification of a private member variable
foo.i = val;
};
// lamb is allowed to access a private member, because it is a friend of Foo
lamb(*this, 30);
}
};
Ein solches Lambda ist nicht nur ein Freund dieser Klasse, es hat den gleichen Zugriff wie die Klasse, in der es deklariert ist.
Lambdas können this
Zeiger erfassen, der die Objektinstanz darstellt, für die die äußere Funktion aufgerufen wurde. Dazu fügen Sie this
der Capture-Liste hinzu:
class Foo
{
private:
int i;
public:
Foo(int val) : i(val) {}
void Test()
{
// capture the this pointer by value
auto lamb = [this](int val)
{
i = val;
};
lamb(30);
}
};
Wenn this
erfasst wird, kann das Lambda die Namen der enthaltenen Klasse so verwenden, als wäre es in der enthaltenen Klasse. Ein implizites this->
wird also auf solche Mitglieder angewendet.
Beachten Sie, dass this
vom Wert erfasst wird, nicht jedoch vom Wert des Typs. Es wird durch den Wert von this
erfasst, der ein Zeiger ist . Als solches ist das Lambda nicht besitzt this
. Wenn das Lambda-Out die Lebensdauer des Objekts, das es erstellt hat, lebt, kann das Lambda ungültig werden.
Das bedeutet auch , dass das Lambda ändern kann this
ohne erklärt zu werden mutable
. Es ist der Zeiger, der const
, nicht das Objekt, auf das gezeigt wird. Das heißt, es sei denn, die äußere Elementfunktion war selbst eine const
Funktion.
Beachten Sie außerdem, dass die Standard - Capture - Klauseln, die beide [=]
und [&]
, wird auch erfassen this
implizit. Und beide erfassen es durch den Wert des Zeigers. Es ist in der Tat ein Fehler, this
in der Capture-Liste anzugeben, wenn ein Standardwert angegeben wird.
Lambda-Dateien können eine Kopie this
Objekts erfassen, das zum Zeitpunkt der Lambda-Erstellung erstellt wurde. Dies geschieht durch Hinzufügen von *this
zur Aufnahmeliste:
class Foo
{
private:
int i;
public:
Foo(int val) : i(val) {}
void Test()
{
// capture a copy of the object given by the this pointer
auto lamb = [*this](int val) mutable
{
i = val;
};
lamb(30); // does not change this->i
}
};
Übertragen von Lambda-Funktionen nach C ++ 03 mithilfe von Funktoren
Lambda-Funktionen in C ++ sind syntaktischer Zucker, der eine sehr kurze Syntax für das Schreiben von Funktoren bietet. Daher kann eine äquivalente Funktionalität in C ++ 03 erhalten werden (wenn auch viel ausführlicher), indem die Lambda-Funktion in einen Funktor konvertiert wird:
// Some dummy types:
struct T1 {int dummy;};
struct T2 {int dummy;};
struct R {int dummy;};
// Code using a lambda function (requires C++11)
R use_lambda(T1 val, T2 ref) {
// Use auto because the type of the lambda is unknown.
auto lambda = [val, &ref](int arg1, int arg2) -> R {
/* lambda-body */
return R();
};
return lambda(12, 27);
}
// The functor class (valid C++03)
// Similar to what the compiler generates for the lambda function.
class Functor {
// Capture list.
T1 val;
T2& ref;
public:
// Constructor
inline Functor(T1 val, T2& ref) : val(val), ref(ref) {}
// Functor body
R operator()(int arg1, int arg2) const {
/* lambda-body */
return R();
}
};
// Equivalent to use_lambda, but uses a functor (valid C++03).
R use_functor(T1 val, T2 ref) {
Functor functor(val, ref);
return functor(12, 27);
}
// Make this a self-contained example.
int main() {
T1 t1;
T2 t2;
use_functor(t1,t2);
use_lambda(t1,t2);
return 0;
}
Wenn die Lambda-Funktion mutable
ist, machen Sie den Aufrufoperator des Funkers zu non-const, dh:
R operator()(int arg1, int arg2) /*non-const*/ {
/* lambda-body */
return R();
}
Rekursive Lambdas
Nehmen wir an, wir möchten Euclids gcd()
als Lambda schreiben. Als Funktion ist es:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}
Ein Lambda kann jedoch nicht rekursiv sein, es gibt keine Möglichkeit, sich selbst anzurufen. Ein Lambda hat keinen Namen und die Verwendung this
in den Körper eines Lambda bezieht sich auf ein erfasstes this
(die Lambda unter der Annahme , wird im Körper einer Elementfunktion geschaffen, sonst ist es ein Fehler). Wie lösen wir dieses Problem?
Verwenden Sie die std::function
Wir können eine Lambda-Capture-Referenz zu einer noch nicht erstellten std::function
:
std::function<int(int, int)> gcd = [&](int a, int b){
return b == 0 ? a : gcd(b, a%b);
};
Das funktioniert, sollte aber sparsam eingesetzt werden. Es ist langsam (wir verwenden jetzt Typenlöschung anstelle eines direkten Funktionsaufrufs), es ist fragil (das Kopieren von gcd
oder das Zurückgeben von gcd
wird gcd
, da das Lambda auf das ursprüngliche Objekt verweist), und es funktioniert nicht mit generischen Lambdas.
Mit zwei intelligenten Zeigern:
auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
*gcd_self = std::make_unique<std::function<int(int, int)>>(
[gcd_self](int a, int b){
return b == 0 ? a : (**gcd_self)(b, a%b);
};
};
Dies führt zu einer erheblichen Anzahl von Umleitungen (was Overhead ist), aber es kann kopiert / zurückgegeben werden, und alle Kopien teilen sich den Status. Es erlaubt Ihnen, das Lambda zurückzugeben, und ist sonst weniger zerbrechlich als die obige Lösung.
Verwenden Sie einen Y-Kombinator
Mit Hilfe einer kurzen Utility-Struktur können wir all diese Probleme lösen:
template <class F>
struct y_combinator {
F f; // the lambda will be stored here
// a forwarding operator():
template <class... Args>
decltype(auto) operator()(Args&&... args) const {
// we pass ourselves to f, then the arguments.
// the lambda should take the first argument as `auto&& recurse` or similar.
return f(*this, std::forward<Args>(args)...);
}
};
// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
return {std::forward<F>(f)};
}
// (Be aware that in C++17 we can do better than a `make_` function)
wir können unsere gcd
wie gcd
implementieren:
auto gcd = make_y_combinator(
[](auto&& gcd, int a, int b){
return b == 0 ? a : gcd(b, a%b);
}
);
Der y_combinator
ist ein Konzept aus dem Lambda-Kalkül, mit dem Sie eine Rekursion haben können, ohne sich selbst benennen zu können, bis Sie definiert sind. Das ist genau das Problem, das Lambdas haben.
Sie erstellen ein Lambda, das als erstes Argument "recurse" verwendet. Wenn Sie rekursieren möchten, übergeben Sie die Argumente, um erneut aufzurufen.
Der y_combinator
dann ein Funktionsobjekt zurück, das diese Funktion mit seinen Argumenten aufruft, jedoch mit einem geeigneten "recurse" -Objekt (nämlich dem y_combinator
selbst) als erstes Argument. Die übrigen Argumente, mit denen Sie den y_combinator
aufrufen, werden y_combinator
an das Lambda weitergeleitet.
Zusamenfassend:
auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
// write body that processes some arguments
// when you want to recurse, call recurse(some other arguments)
});
und Sie haben eine Rekursion in einem Lambda ohne gravierende Einschränkungen oder erheblichen Mehraufwand.
Verwenden von Lambdas zum Auspacken des Inline-Parameterpakets
Das Auspacken von Parametern erfordert normalerweise das Schreiben einer Hilfsfunktion für jedes Mal, wenn Sie dies tun möchten.
In diesem Spielzeugbeispiel:
template<std::size_t...Is>
void print_indexes( std::index_sequence<Is...> ) {
using discard=int[];
(void)discard{0,((void)(
std::cout << Is << '\n' // here Is is a compile-time constant.
),0)...};
}
template<std::size_t I>
void print_indexes_upto() {
return print_indexes( std::make_index_sequence<I>{} );
}
Der print_indexes_upto
möchte ein Parameterpaket mit Indizes erstellen und auspacken. Dazu muss eine Hilfsfunktion aufgerufen werden. Jedes Mal, wenn Sie ein von Ihnen erstelltes Parameterpaket auspacken möchten, müssen Sie dazu eine benutzerdefinierte Hilfsfunktion erstellen.
Dies kann mit Lambdas vermieden werden.
Sie können Parameterpakete wie folgt in eine Reihe von Aufrufen eines Lambda entpacken:
template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index{};
template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
return [](auto&& f){
using discard=int[];
(void)discard{0,(void(
f( index<Is> )
),0)...};
};
}
template<std::size_t N>
auto index_over(index_t<N> = {}) {
return index_over( std::make_index_sequence<N>{} );
}
Mit fold-Ausdrücken kann index_over()
folgendermaßen vereinfacht werden:
template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
return [](auto&& f){
((void)(f(index<Is>)), ...);
};
}
Sobald Sie dies getan haben, können Sie dies verwenden, um das manuelle Auspacken von Parameterpaketen durch eine zweite Überladung in anderem Code zu ersetzen, sodass Sie die Parameterpakete "inline" entpacken können:
template<class Tup, class F>
void for_each_tuple_element(Tup&& tup, F&& f) {
using T = std::remove_reference_t<Tup>;
using std::tuple_size;
auto from_zero_to_N = index_over< tuple_size<T>{} >();
from_zero_to_N(
[&](auto i){
using std::get;
f( get<i>( std::forward<Tup>(tup) ) );
}
);
}
Das auto i
durch das index_over
an das Lambda index_over
ist std::integral_constant<std::size_t, ???>
. Dies hat eine constexpr
Konvertierung in std::size_t
, die nicht vom Zustand this
constexpr
abhängt. constexpr
können wir es als Kompilierungszeitkonstante verwenden, z. B. wenn wir es an std::get<i>
.
Um zum Spielzeugbeispiel oben zurückzukehren, schreiben Sie es wie folgt um:
template<std::size_t I>
void print_indexes_upto() {
index_over(index<I>)([](auto i){
std::cout << i << '\n'; // here i is a compile-time constant
});
}
Das ist viel kürzer und behält die Logik in dem Code, der es verwendet.
Live-Beispiel zum Spielen.