C ++ Lambda-Codegenerierung mit Init Captures in C ++ 14


9

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?


1
@ rafix07 In diesem Fall wird der generierte Insight-Code nicht einmal kompiliert (es wird versucht, das eindeutige ptr-Mitglied aus dem Argument zu kopieren und zu initialisieren). cppinsights ist nützlich, um einen allgemeinen Überblick zu erhalten, kann diese Frage hier jedoch eindeutig nicht beantworten.
Max Langhof

Sie scheinen anzunehmen, dass es als ersten Schritt der Kompilierung eine Übersetzung von Lambdas in Funktoren gibt, oder suchen Sie nur nach gleichwertigem Code (dh gleichem Verhalten)? Die Art und Weise, wie ein bestimmter Compiler Code generiert (und welchen Code er generiert), hängt vom Compiler, der Version, der Architektur, den Flags usw. ab. Fragen Sie also nach einer bestimmten Plattform? Wenn nicht, ist Ihre Frage nicht wirklich beantwortbar. Anders als der tatsächlich generierte Code ist er wahrscheinlich effizienter als die von Ihnen aufgelisteten Funktoren (z. B. Inline-Konstruktoren, Vermeidung unnötiger Kopien usw.).
Sander De Dycker

2
Wenn Sie daran interessiert sind, was der C ++ - Standard dazu zu sagen hat, lesen Sie [expr.prim.lambda] . Es ist zu viel, um es hier als Antwort zusammenzufassen.
Sander De Dycker

Antworten:


2

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 xes weder eine Referenz noch eine Funktion ist), und Zugriffe auf xinnerhalb 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. xim Lambda-Körper könnte sich nur direkt auf die xAuß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]/15erwähnt, erfolgt die Initialisierung des entsprechenden Mitglieds des Verschlusstyps ( __xfü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 les zugewiesen ist). Zugriffe auf pim 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 :)


9

Fall 1 [x](){} : Der generierte Konstruktor akzeptiert sein Argument durch einen möglicherweise constqualifizierten Verweis, um unnötige Kopien zu vermeiden:

__some_compiler_generated_name(const int& x) : x_{x}{}

Fall 2 [x&](){} : Ihre Annahmen hier sind korrekt, xwerden übergeben und als Referenz gespeichert.


Fall 3 [x = 33](){} : Wieder richtig, xwird durch Wert initialisiert.


Fall 4 [p = std::move(unique_ptr_var)] : Der Konstruktor sieht folgendermaßen aus:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

also ja, das unique_ptr_varwird 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").


" const-qualifiziert" Warum?
cpplearner

@cpplearner Mh, gute Frage. Ich schätze, ich habe das eingefügt, weil einer dieser mentalen Automatismen eingetreten ist ^^ constKann hier zumindest nicht schaden, weil es nicht eindeutig ist / besser zusammenpasst, wenn nicht constusw. Wie auch immer, denkst du, ich sollte das entfernen const?
lubgr

Ich denke, const sollte bleiben, was ist, wenn das Argument, an das übergeben wird, tatsächlich const ist?
Aconcagua

Sie sagen also, dass hier zwei Verschiebungs- (oder Kopier-) Konstruktionen stattfinden?
Max Langhof

Entschuldigung, ich meine in Fall 4 (für die Züge) und Fall 1 (für die Kopien). Der Kopie-Teil meiner Frage macht aufgrund Ihrer Aussagen keinen Sinn (aber ich stelle diese Aussagen in Frage).
Max Langhof

5

Mit cppinsights.io muss weniger spekuliert werden .

Fall 1:
Code

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Compiler generiert

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Fall 2:
Code

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Compiler generiert

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Fall 3:
Code

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Compiler generiert

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Fall 4 (inoffiziell):
Code

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Compiler generiert

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

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 die operator()Funktion ist. Wenn Sie die Erfassungen ändern müssen, markieren Sie das Lambda natürlich als mutable.


Der Code, den Sie für den letzten Fall anzeigen, wird nicht einmal kompiliert. Die Schlussfolgerung "Eine Verschiebung erfolgt, aber nicht [technisch] im Konstruktor" kann von diesem Code nicht unterstützt werden.
Max Langhof

Der Code von Fall 4 wird mit Sicherheit auf meinem Mac kompiliert. Ich bin überrascht, dass der von cppinsights generierte erweiterte Code nicht kompiliert werden kann. Die Seite war bis jetzt für mich ziemlich zuverlässig. Ich werde ein Problem mit ihnen ansprechen. EDIT: Ich habe bestätigt, dass der generierte Code nicht kompiliert wird. das war ohne diese bearbeitung nicht klar.
Zwischen

1
Link zum Problem bei Interesse: github.com/andreasfertig/cppinsights/issues/258 Ich empfehle die Website weiterhin zum Testen von SFINAE und ob implizite Casts auftreten oder nicht.
Zwischen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.