Ich versuche, den Codecode zu verstehen / zu klären, der generiert wird, wenn Captures an Lambdas übergeben werden, insbesondere in generalisierten Init-Captures, die in C ++ 14 hinzugefügt wurden.
Geben Sie die folgenden unten aufgeführten Codebeispiele an. Dies ist mein aktuelles Verständnis dessen, was der Compiler generieren wird.
Fall 1: Erfassung nach Wert / Standarderfassung nach Wert
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Würde gleichsetzen mit:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int x) : __x{x}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Es gibt also mehrere Kopien, eine zum Kopieren in den Konstruktorparameter und eine zum Kopieren in das Element, was für Typen wie Vektor usw. teuer wäre.
Fall 2: Erfassung nach Referenz / Standarderfassung nach Referenz
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Würde gleichsetzen mit:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int& x) : x_{x}{}
void operator()() const { std::cout << x << std::endl;}
private:
int& x_;
};
Der Parameter ist eine Referenz und das Mitglied ist eine Referenz, also keine Kopien. Schön für Typen wie Vektor etc.
Fall 3:
Generalisierte Init-Erfassung
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Mein Verständnis ist, dass dies Fall 1 in dem Sinne ähnlich ist, dass es in das Mitglied kopiert wird.
Ich vermute, dass der Compiler Code generiert, der ...
class __some_compiler_generated_name {
public:
__some_compiler_generated_name() : __x{33}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Auch wenn ich folgendes habe:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
Wie würde der Konstruktor aussehen? Verschiebt es es auch in das Mitglied?
Antworten:
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
: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
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Wenden wir dies auf Ihren Fall 1 an:
Der Schließungstyp dieses Lambdas hat ein unbenanntes nicht statisches Datenelement (nennen wir es
__x
) vom Typint
(dax
es weder eine Referenz noch eine Funktion ist), und Zugriffe aufx
innerhalb des Lambda-Körpers werden in Zugriffe auf transformiert__x
. Wenn wir den Lambda-Ausdruck auswerten (dh bei der Zuweisung zulambda
), initialisieren wir direkt__x
mitx
.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
:Es gibt einen weiteren Absatz über die Referenzerfassung von Referenzen, aber das machen wir nirgendwo.
Also für Fall 2:
Wir wissen nicht, ob ein Mitglied zum Schließungstyp hinzugefügt wurde.
x
im Lambda-Körper könnte sich nur direkt auf diex
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
:Schauen wir uns vor diesem Hintergrund Fall 3 an:
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:
Das Element vom Verschlusstyp wird initialisiert,
__p = std::move(unique_ptr_var)
wenn der Lambda-Ausdruck ausgewertet wird (dh wennl
es zugewiesen ist). Zugriffe aufp
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 :)
quelle
Fall 1
[x](){}
: Der generierte Konstruktor akzeptiert sein Argument durch einen möglicherweiseconst
qualifizierten Verweis, um unnötige Kopien zu vermeiden:Fall 2
[x&](){}
: Ihre Annahmen hier sind korrekt,x
werden übergeben und als Referenz gespeichert.Fall 3
[x = 33](){}
: Wieder richtig,x
wird durch Wert initialisiert.Fall 4
[p = std::move(unique_ptr_var)]
: Der Konstruktor sieht folgendermaßen aus:also ja, das
unique_ptr_var
wird in den verschluss "verschoben". Siehe auch Scott Meyers Punkt 32 in Effective Modern C ++ ("Verwenden Sie die Init-Erfassung, um Objekte in Abschlüsse zu verschieben").quelle
const
-qualifiziert" Warum?const
Kann hier zumindest nicht schaden, weil es nicht eindeutig ist / besser zusammenpasst, wenn nichtconst
usw. Wie auch immer, denkst du, ich sollte das entfernenconst
?Mit cppinsights.io muss weniger spekuliert werden .
Fall 1:
Code
Compiler generiert
Fall 2:
Code
Compiler generiert
Fall 3:
Code
Compiler generiert
Fall 4 (inoffiziell):
Code
Compiler generiert
Und ich glaube, dieser letzte Code beantwortet Ihre Frage. Eine Verschiebung erfolgt, jedoch nicht [technisch] im Konstruktor.
Captures selbst sind es nicht
const
, aber Sie können sehen, dass dieoperator()
Funktion ist. Wenn Sie die Erfassungen ändern müssen, markieren Sie das Lambda natürlich alsmutable
.quelle