Génération de code Lambda C ++ avec des captures Init en C ++ 14

9

J'essaie de comprendre / clarifier le code qui est généré lorsque les captures sont passées à lambdas, en particulier dans les captures d'initialisation généralisées ajoutées en C ++ 14.

Donnez les exemples de code suivants répertoriés ci-dessous, c'est ma compréhension actuelle de ce que le compilateur va générer.

Cas 1: capture par valeur / capture par défaut par valeur

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

Équivaudrait à:

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;
};

Il y a donc plusieurs copies, une à copier dans le paramètre constructeur et une à copier dans le membre, ce qui coûterait cher pour des types comme le vecteur, etc.

Cas 2: capture par référence / capture par défaut par référence

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

Équivaudrait à:

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_;
};

Le paramètre est une référence et le membre est une référence donc pas de copie. Idéal pour les types comme le vecteur, etc.

Cas 3:

Capture d'initialisation généralisée

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

Je comprends que ceci est similaire au cas 1 dans le sens où il est copié sur le membre.

Je suppose que le compilateur génère du code similaire à ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Aussi si j'ai les éléments suivants:

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

À quoi ressemblerait le constructeur? Le déplace-t-il également dans le membre?

Blair Davidson
la source
1
@ rafix07 Dans ce cas, le code insight généré ne sera même pas compilé (il essaie de copier-initialiser le membre ptr unique de l'argument). cppinsights est utile pour obtenir un aperçu général, mais il est clairement impossible de répondre à cette question ici.
Max Langhof
Vous semblez supposer qu'il y a une traduction de lambda en foncteurs comme première étape de compilation, ou cherchez-vous simplement un code équivalent (c'est-à-dire le même comportement)? La façon dont un compilateur spécifique génère du code (et quel code il génère) dépendra du compilateur, de la version, de l'architecture, des indicateurs, etc. Alors, demandez-vous une plate-forme spécifique? Sinon, votre question ne répond pas vraiment. Autre que le code généré, il sera probablement plus efficace que les foncteurs que vous listez (par exemple, les constructeurs intégrés, en évitant les copies inutiles, etc.).
Sander De Dycker
2
Si vous êtes intéressé par ce que la norme C ++ a à dire à ce sujet, reportez-vous à [expr.prim.lambda] . C'est trop pour résumer ici comme réponse.
Sander De Dycker

Réponses:

2

Cette question ne peut pas être entièrement répondu dans le code. Vous pourrez peut-être écrire du code quelque peu "équivalent", mais la norme n'est pas spécifiée de cette façon.

Avec cela à l'écart, plongons-nous [expr.prim.lambda]. La première chose à noter est que les constructeurs ne sont mentionnés que dans [expr.prim.lambda.closure]/13:

Le type de fermeture associé à une expression lambda n'a pas de constructeur par défaut si l' expression lambda a une capture lambda et un constructeur par défaut par défaut sinon. Il a un constructeur de copie par défaut et un constructeur de déplacement par défaut ([class.copy.ctor]). Il a un opérateur d'affectation de copie supprimé si l' expression lambda a une capture lambda et des opérateurs d'affectation de copie et de déplacement par défaut dans le cas contraire ([class.copy.assign]). [ Remarque: Ces fonctions membres spéciales sont implicitement définies comme d'habitude et peuvent donc être définies comme supprimées. - note de fin ]

Donc, dès le départ, il devrait être clair que les constructeurs ne sont pas formellement comment la capture d'objets est définie. Vous pouvez être assez proche (voir la réponse cppinsights.io), mais les détails diffèrent (notez que le code de cette réponse pour le cas 4 ne se compile pas).


Ce sont les principales clauses standard nécessaires pour discuter du cas 1:

[expr.prim.lambda.capture]/10

[...]
Pour chaque entité capturée par copie, un membre de données non statique non nommé est déclaré dans le type de fermeture. L'ordre de déclaration de ces membres n'est pas précisé. Le type d'un tel membre de données est le type référencé si l'entité est une référence à un objet, une référence lvalue au type de fonction référencé si l'entité est une référence à une fonction, ou le type de l'entité capturée correspondante dans le cas contraire. Un membre d'un syndicat anonyme ne doit pas être capturé par copie.

[expr.prim.lambda.capture]/11

Chaque expression id dans l'instruction composée d'une expression lambda qui est une utilisation odr d'une entité capturée par copie est transformée en un accès au membre de données sans nom correspondant du type de fermeture. [...]

[expr.prim.lambda.capture]/15

Lorsque l'expression lambda est évaluée, les entités capturées par copie sont utilisées pour initialiser directement chaque membre de données non statique correspondant de l'objet de fermeture résultant, et les membres de données non statiques correspondant aux captures init sont initialisés comme indiqué par l'initialiseur correspondant (qui peut être une copie ou une initialisation directe). [...]

Appliquons ceci à votre cas 1:

Cas 1: capture par valeur / capture par défaut par valeur

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

Le type de fermeture de ce lambda aura un membre de données non statique non nommé (appelons-le __x) de type int(car il xne s'agit ni d'une référence ni d'une fonction), et les accès à l' xintérieur du corps lambda sont transformés en accès à __x. Lorsque nous évaluons l'expression lambda (c'est-à-dire lors de l'affectation à lambda), nous initialisons directement __x avec x.

Bref, une seule copie a lieu . Le constructeur du type de fermeture n'est pas impliqué et il n'est pas possible de l'exprimer en C ++ "normal" (notez que le type de fermeture n'est pas non plus un type agrégé ).


La capture de référence implique [expr.prim.lambda.capture]/12:

Une entité est capturée par référence si elle est implicitement ou explicitement capturée mais pas capturée par copie. Il n'est pas spécifié si des membres de données non statiques supplémentaires non nommés sont déclarés dans le type de fermeture pour les entités capturées par référence. [...]

Il y a un autre paragraphe sur la capture de références, mais nous ne faisons cela nulle part.

Donc, pour le cas 2:

Cas 2: capture par référence / capture par défaut par référence

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

Nous ne savons pas si un membre est ajouté au type de fermeture. xdans le corps lambda pourrait se référer directement à l' xextérieur. C'est au compilateur de comprendre, et il le fera dans une certaine forme de langage intermédiaire (qui diffère d'un compilateur à un autre), pas une transformation source du code C ++.


Les captures init sont détaillées dans [expr.prim.lambda.capture]/6:

Une capture init se comporte comme si elle déclarait et capturait explicitement une variable de la forme auto init-capture ;dont la région déclarative est l'instruction composée de l'expression lambda, sauf que:

  • (6.1) si la capture est par copie (voir ci-dessous), le membre de données non statique déclaré pour la capture et la variable sont traités comme deux façons différentes de se référer au même objet, qui a la durée de vie des données non statiques membre, et aucune copie ni destruction supplémentaire n'est effectuée, et
  • (6.2) si la capture est par référence, la durée de vie de la variable se termine lorsque la durée de vie de l'objet de fermeture se termine.

Compte tenu de cela, regardons le cas 3:

Cas 3: capture d'initialisation généralisée

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

Comme indiqué, imaginez cela comme une variable créée par auto x = 33;et explicitement capturée par copie. Cette variable n'est "visible" que dans le corps lambda. Comme indiqué [expr.prim.lambda.capture]/15précédemment, l'initialisation du membre correspondant du type de fermeture ( __xpour la postérité) se fait par l'initialiseur donné lors de l'évaluation de l'expression lambda.

Pour éviter tout doute: cela ne signifie pas que les choses sont initialisées deux fois ici. Le auto x = 33;est un "comme si" pour hériter de la sémantique des captures simples, et l'initialisation décrite est une modification de cette sémantique. Une seule initialisation se produit.

Cela couvre également le cas 4:

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

Le membre de type de fermeture est initialisé par __p = std::move(unique_ptr_var)lorsque l'expression lambda est évaluée (c'est-à-dire lorsqu'elle lest affectée à). Les accès à pdans le corps lambda sont transformés en accès à __p.


TL; DR: Seul le nombre minimal de copies / initialisations / déplacements est effectué (comme on pourrait l'espérer / s'y attendre). Je suppose que les lambdas ne sont pas spécifiés en termes de transformation source (contrairement à d'autres sucres syntaxiques) exactement parce que l' expression des choses en termes de constructeurs nécessiterait des opérations superflues.

J'espère que cela apaise les craintes exprimées dans la question :)

Max Langhof
la source
9

Cas 1 [x](){} : Le constructeur généré acceptera son argument par constune référence éventuellement qualifiée pour éviter les copies inutiles:

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

Cas 2 [x&](){} : Vos hypothèses ici sont correctes, xsont passées et stockées par référence.


Cas 3 [x = 33](){} : Encore une fois correct, xest initialisé par valeur.


Cas 4 [p = std::move(unique_ptr_var)] : Le constructeur ressemblera à ceci:

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

alors oui, le unique_ptr_var"s'installe" dans la fermeture. Voir également l'article 32 de Scott Meyer dans Effective Modern C ++ ("Utiliser la capture init pour déplacer des objets dans des fermetures").

lubgr
la source
" constqualifié" Pourquoi?
cpplearner
@cpplearner Mh, bonne question. Je suppose que j'ai inséré cela parce qu'un de ces automatismes mentaux s'est déclenché ^^ Au moins, constça ne peut pas faire de mal ici en raison d'une ambiguïté / meilleure correspondance lorsque non constetc. Quoi qu'il en soit, pensez-vous que je devrais supprimer le const?
lubgr
Je pense que const devrait rester, et si l'argument passé à const est réellement const?
Aconcagua
Vous dites donc que deux constructions de déplacement (ou de copie) se produisent ici?
Max Langhof
Désolé, je veux dire dans le cas 4 (pour les mouvements) et le cas 1 (pour les copies). La partie copie de ma question n'a aucun sens sur la base de vos déclarations (mais je remets en question ces déclarations).
Max Langhof
5

Il est moins nécessaire de spéculer, en utilisant cppinsights.io .

Cas 1:
Code

#include <memory>

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

Le compilateur génère

#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});
}

Cas 2:
Code

#include <iostream>
#include <memory>

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

Le compilateur génère

#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});
}

Cas 3:
Code

#include <iostream>

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

Le compilateur génère

#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});
}

Cas 4 (officieusement):
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; };
}

Le compilateur génère

// 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))});
}

Et je crois que ce dernier morceau de code répond à votre question. Un mouvement se produit, mais pas [techniquement] dans le constructeur.

Les captures elles-mêmes ne le sont pas const, mais vous pouvez voir que la operator()fonction l'est. Naturellement, si vous devez modifier les captures, vous marquez le lambda comme mutable.

sweenish
la source
Le code que vous montrez pour le dernier cas ne se compile même pas. La conclusion "un mouvement se produit, mais pas [techniquement] dans le constructeur" ne peut pas être prise en charge par ce code.
Max Langhof
Le code de cas 4 se compile très certainement sur mon Mac. Je suis surpris que le code développé généré à partir de cppinsights ne se compile pas. Le site a été jusqu'à présent assez fiable pour moi. Je vais soulever un problème avec eux. EDIT: J'ai confirmé que le code généré ne compile pas; ce n'était pas clair sans cette modification.
sweenish
1
Lien vers le problème en cas d'intérêt: github.com/andreasfertig/cppinsights/issues/258 Je recommande toujours le site pour des choses comme tester SFINAE et si des conversions implicites se produiront ou non.
sweenish