Diese Frage kann im Code nicht vollständig beantwortet werden. Möglicherweise können Sie etwas "äquivalenten" Code schreiben, aber der Standard wird auf diese Weise nicht angegeben.
Lassen Sie uns mit dem aus dem Weg gehen [expr.prim.lambda]
. Das erste, was zu beachten ist, ist, dass Konstruktoren nur erwähnt werden in [expr.prim.lambda.closure]/13
:
Der einem Lambda-Ausdruck zugeordnete Schließungstyp hat keinen Standardkonstruktor, wenn der Lambda-Ausdruck eine Lambda-Erfassung und ansonsten einen Standardstandardkonstruktor hat. Es verfügt über einen standardmäßigen Kopierkonstruktor und einen standardmäßigen Verschiebungskonstruktor ([class.copy.ctor]). Es hat einen gelöschten Kopierzuweisungsoperator, wenn der Lambda-Ausdruck einen Lambda-Capture- und einen standardmäßigen Kopier- und Verschiebungszuweisungsoperator hat ([class.copy.assign]). [ Hinweis: Diese speziellen Elementfunktionen werden implizit wie gewohnt definiert und können daher als gelöscht definiert werden. - Endnote ]
Auf Anhieb sollte klar sein, dass Konstruktoren nicht formal definieren, wie das Erfassen von Objekten definiert wird. Sie können ziemlich nah dran sein (siehe die Antwort cppinsights.io), aber die Details unterscheiden sich (beachten Sie, dass der Code in dieser Antwort für Fall 4 nicht kompiliert wird).
Dies sind die wichtigsten Standardklauseln, die zur Erörterung von Fall 1 erforderlich sind:
[expr.prim.lambda.capture]/10
[...]
Für jede durch Kopie erfasste Entität wird ein unbenanntes nicht statisches Datenelement im Schließungstyp deklariert. Die Deklarationsreihenfolge dieser Mitglieder ist nicht festgelegt. Der Typ eines solchen Datenelements ist der referenzierte Typ, wenn die Entität eine Referenz auf ein Objekt ist, eine Wertreferenz auf den referenzierten Funktionstyp, wenn die Entität eine Referenz auf eine Funktion ist, oder der Typ der entsprechenden erfassten Entität auf andere Weise. Ein Mitglied einer anonymen Gewerkschaft darf nicht kopiert werden.
[expr.prim.lambda.capture]/11
Jeder ID-Ausdruck in der zusammengesetzten Anweisung eines Lambda-Ausdrucks , der eine odr-Verwendung einer durch Kopie erfassten Entität darstellt, wird in einen Zugriff auf das entsprechende unbenannte Datenelement des Schließungstyps umgewandelt. [...]
[expr.prim.lambda.capture]/15
Wenn der Lambda-Ausdruck ausgewertet wird, werden die Entitäten, die durch Kopieren erfasst werden, verwendet, um jedes entsprechende nicht statische Datenelement des resultierenden Abschlussobjekts direkt zu initialisieren, und die nicht statischen Datenelemente, die den Init-Erfassungen entsprechen, werden als initialisiert Dies wird durch den entsprechenden Initialisierer angezeigt (dies kann eine Kopier- oder Direktinitialisierung sein). [...]
Wenden wir dies auf Ihren Fall 1 an:
Fall 1: Erfassung nach Wert / Standarderfassung nach Wert
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Der Schließungstyp dieses Lambdas hat ein unbenanntes nicht statisches Datenelement (nennen wir es __x
) vom Typ int
(da x
es weder eine Referenz noch eine Funktion ist), und Zugriffe auf x
innerhalb des Lambda-Körpers werden in Zugriffe auf transformiert __x
. Wenn wir den Lambda-Ausdruck auswerten (dh bei der Zuweisung zu lambda
), initialisieren wir direkt__x
mit x
.
Kurz gesagt, es findet nur eine Kopie statt . Der Konstruktor des Verschlusstyps ist nicht beteiligt, und es ist nicht möglich, dies in "normalem" C ++ auszudrücken (beachten Sie, dass der Verschlusstyp auch kein Aggregattyp ist ).
Die Referenzerfassung umfasst [expr.prim.lambda.capture]/12
:
Eine Entität wird als Referenz erfasst, wenn sie implizit oder explizit erfasst, aber nicht durch Kopie erfasst wird. Es ist nicht angegeben, ob zusätzliche unbenannte nicht statische Datenelemente im Schließungstyp für durch Referenz erfasste Entitäten deklariert sind. [...]
Es gibt einen weiteren Absatz über die Referenzerfassung von Referenzen, aber das machen wir nirgendwo.
Also für Fall 2:
Fall 2: Erfassung nach Referenz / Standarderfassung nach Referenz
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Wir wissen nicht, ob ein Mitglied zum Schließungstyp hinzugefügt wurde. x
im Lambda-Körper könnte sich nur direkt auf die x
Außenseite beziehen . Dies muss vom Compiler herausgefunden werden. Dies geschieht in einer Zwischensprache (die sich von Compiler zu Compiler unterscheidet), nicht in einer Quelltransformation des C ++ - Codes.
Erste Aufnahmen sind detailliert in [expr.prim.lambda.capture]/6
:
Ein Init-Capture verhält sich so, als würde er eine Variable der Form deklarieren und explizit erfassen, auto init-capture ;
deren deklarativer Bereich die zusammengesetzte Anweisung des Lambda-Ausdrucks ist, mit der Ausnahme, dass:
- (6.1) Wenn die Erfassung durch Kopie erfolgt (siehe unten), werden das für die Erfassung deklarierte nicht statische Datenelement und die Variable als zwei verschiedene Arten der Bezugnahme auf dasselbe Objekt behandelt, das die Lebensdauer der nicht statischen Daten hat Mitglied, und es wird keine zusätzliche Kopie und Zerstörung durchgeführt, und
- (6.2) Wenn die Erfassung durch Bezugnahme erfolgt, endet die Lebensdauer der Variablen, wenn die Lebensdauer des Abschlussobjekts endet.
Schauen wir uns vor diesem Hintergrund Fall 3 an:
Fall 3: Generalisierte Init-Erfassung
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Stellen Sie sich dies wie gesagt als eine Variable vor, die auto x = 33;
von einer Kopie erstellt und explizit erfasst wird. Diese Variable ist nur im Lambda-Körper "sichtbar". Wie bereits [expr.prim.lambda.capture]/15
erwähnt, erfolgt die Initialisierung des entsprechenden Mitglieds des Verschlusstyps ( __x
für die Nachwelt) durch den angegebenen Initialisierer bei Auswertung des Lambda-Ausdrucks.
Um Zweifel zu vermeiden: Dies bedeutet nicht, dass die Dinge hier zweimal initialisiert werden. Das auto x = 33;
ist ein "als ob", um die Semantik einfacher Captures zu erben, und die beschriebene Initialisierung ist eine Modifikation dieser Semantik. Es erfolgt nur eine Initialisierung.
Dies gilt auch für Fall 4:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Das Element vom Verschlusstyp wird initialisiert, __p = std::move(unique_ptr_var)
wenn der Lambda-Ausdruck ausgewertet wird (dh wenn l
es zugewiesen ist). Zugriffe auf p
im Lambda-Körper werden in Zugriffe auf umgewandelt __p
.
TL; DR: Es wird nur die minimale Anzahl von Kopien / Initialisierungen / Verschiebungen ausgeführt (wie man hoffen / erwarten würde). Ich würde annehmen, dass Lambdas nicht im Sinne einer Quellentransformation spezifiziert werden (im Gegensatz zu anderen syntaktischen Zuckern), genau weil das Ausdrücken von Dingen in Konstruktoren überflüssige Operationen erfordern würde.
Ich hoffe das beseitigt die in der Frage geäußerten Ängste :)