C++-Lambda-Code-Generierung mit Init-Captures in C++ 14




c++14 move (2)

Ich versuche, den Code-Code zu verstehen / zu klären, der generiert wird, wenn Captures an Lambdas übergeben werden, insbesondere in verallgemeinerten Init-Captures, die in C ++ 14 hinzugefügt wurden.

Geben Sie die folgenden unten aufgelisteten Codebeispiele an, um zu verstehen, 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 Element ist eine Referenz, also keine Kopien. Nizza für Typen wie Vektor usw.

Fall 3:

Generalisierte Init-Erfassung

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

Mein Verständnis ist, dass dies dem Fall 1 in dem Sinne ähnlich ist, dass es in das Mitglied kopiert wird.

Ich vermute, der Compiler generiert Code ähnlich wie ...

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?


Diese Frage kann nicht vollständig im Code beantwortet werden. Möglicherweise können Sie etwas "äquivalenten" Code schreiben, aber der Standard ist nicht so spezifiziert.

Lassen Sie uns aus dem Weg gehen und in [expr.prim.lambda] . Das erste, was zu beachten ist, ist, dass Konstruktoren nur in [expr.prim.lambda.closure]/13 :

Der einem Lambda-Ausdruck zugeordnete Schließungstyp hat keinen Standardkonstruktor, wenn der Lambda-Ausdruck eine Lambda-Erfassung hat, und ansonsten einen Standardkonstruktor. Es hat einen standardmäßigen Kopierkonstruktor und einen standardmäßigen Verschiebungskonstruktor ([class.copy.ctor]). Es hat einen Zuweisungsoperator für gelöschte Kopien, wenn der Lambda-Ausdruck über einen Lambda-Erfassungsoperator verfügt und ansonsten die Zuweisungsoperatoren für Kopieren und Verschieben standardmäßig verwendet werden ([class.copy.assign]). [ Hinweis: Diese speziellen Elementfunktionen sind 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 Objekte erfasst werden. Sie können ziemlich nahe kommen (siehe die cppinsights.io-Antwort), 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 Entität, die per Kopie erfasst wird, wird ein unbenanntes nicht statisches Datenelement im Abschlusstyp deklariert. Die Deklarationsreihenfolge dieser Mitglieder ist nicht festgelegt. Der Typ eines solchen Datenelements ist der referenzierte Typ, wenn die Entität ein Verweis auf ein Objekt ist, ein Wertverweis auf den referenzierten Funktionstyp, wenn die Entität ein Verweis auf eine Funktion ist, oder der Typ der entsprechenden erfassten Entität. Ein Mitglied einer anonymen Gewerkschaft wird nicht durch Kopie erfasst.

[expr.prim.lambda.capture]/11

Jeder ID-Ausdruck in der zusammengesetzten Anweisung eines Lambda-Ausdrucks , der eine nicht ordnungsgemäße Verwendung einer durch Kopie erfassten Entität darstellt, wird in einen Zugriff auf das entsprechende unbenannte Datenelement des Abschlusstyps 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 wird vom entsprechenden Initialisierer angegeben (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 Closure-Typ dieses Lambdas hat ein unbenanntes nicht statisches Datenelement (nennen wir es __x ) vom Typ int (da x weder eine Referenz noch eine Funktion ist), und Zugriffe auf x innerhalb des Lambda-Körpers werden in Zugriffe auf __x . Wenn wir den Lambda-Ausdruck auswerten (dh lambda zuweisen), direct-initialize wir __x mit x .

Kurz gesagt, es findet nur eine Kopie statt . Der Konstruktor des Abschlusstyps ist nicht beteiligt, und es ist nicht möglich, dies in "normalem" C ++ auszudrücken (beachten Sie, dass der Abschlusstyp 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, jedoch nicht als Kopie erfasst wird. Es ist nicht festgelegt, ob zusätzliche unbenannte nicht statische Datenelemente im Abschlusstyp für durch Verweis erfasste Entitäten deklariert werden. [...]

Es gibt einen weiteren Absatz über das Erfassen 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 dem Schließungstyp ein Mitglied hinzugefügt wurde. x im Lambda-Körper könnte sich direkt auf das x außerhalb beziehen. Dies muss der Compiler selbst herausfinden, und zwar in einer Form von Zwischensprache (die sich von Compiler zu Compiler unterscheidet), nicht in einer Quelltransformation des C ++ - Codes.

Init-Captures werden in [expr.prim.lambda.capture]/6 detailliert beschrieben:

Ein Init-Capture verhält sich so, als würde es eine Variable der Form auto init-capture ; deklarieren und explizit auto init-capture ; deren deklarative Region die zusammengesetzte Aussage des Lambda-Ausdrucks ist, mit der Ausnahme, dass:

  • (6.1) Wenn es sich bei der Erfassung um eine Kopie handelt (siehe unten), werden das für die Erfassung deklarierte nicht statische Datenelement und die Variable als zwei verschiedene Arten des Verweises auf dasselbe Objekt behandelt, das die Lebensdauer der nicht statischen Daten hat Mitglied, und keine zusätzliche Kopie und Zerstörung durchgeführt wird, und
  • (6.2) Wenn es sich bei der Erfassung um eine Referenz handelt, endet die Lebensdauer der Variablen, wenn die Lebensdauer des Abschlussobjekts endet.

Schauen wir uns daher Fall 3 an:

Fall 3: Generalisierte Init-Erfassung

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

Stellen Sie sich dies wie angegeben als eine Variable vor, die mit auto x = 33; und ausdrücklich durch Kopie erfasst. Diese Variable ist nur im Lambda-Körper "sichtbar". Wie bereits in [expr.prim.lambda.capture]/15 erwähnt, erfolgt die Initialisierung des entsprechenden __x des Verschlusstyps ( __x für die Nachwelt) nach Auswertung des Lambda-Ausdrucks durch den angegebenen Initialisierer.

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 findet nur eine Initialisierung statt.

Dies gilt auch für Fall 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

Das __p = std::move(unique_ptr_var) Verschlusstyp wird durch __p = std::move(unique_ptr_var) initialisiert, wenn der Lambda-Ausdruck ausgewertet wird (dh wenn l zugewiesen wird). Zugriffe auf p im Lambda-Körper werden in Zugriffe auf __p .

TL; DR: Nur die minimale Anzahl von Kopien / Initialisierungen / Zügen wird ausgeführt (wie man es erhoffen würde). Ich würde davon ausgehen, dass Lambdas im Gegensatz zu anderen syntaktischen Zuckern nicht im Sinne einer Quelltransformation spezifiziert werden, da das Ausdrücken von Dingen in Form von Konstruktoren überflüssige Operationen erfordern würde.

Ich hoffe, dies beseitigt die in der Frage geäußerten Befürchtungen :)


Mit cppinsights.io müssen Sie weniger spekulieren.

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 findet statt, jedoch nicht [technisch] im Konstruktor.

Captures selbst sind keine 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 .





move