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?
Réponses:
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
: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
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Appliquons ceci à votre cas 1:
Le type de fermeture de ce lambda aura un membre de données non statique non nommé (appelons-le
__x
) de typeint
(car ilx
ne s'agit ni d'une référence ni d'une fonction), et les accès à l'x
inté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
avecx
.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
: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:
Nous ne savons pas si un membre est ajouté au type de fermeture.
x
dans le corps lambda pourrait se référer directement à l'x
exté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
:Compte tenu de cela, regardons le cas 3:
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]/15
précédemment, l'initialisation du membre correspondant du type de fermeture (__x
pour 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:
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'ellel
est affectée à). Les accès àp
dans 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 :)
la source
Cas 1
[x](){}
: Le constructeur généré acceptera son argument parconst
une référence éventuellement qualifiée pour éviter les copies inutiles:Cas 2
[x&](){}
: Vos hypothèses ici sont correctes,x
sont passées et stockées par référence.Cas 3
[x = 33](){}
: Encore une fois correct,x
est initialisé par valeur.Cas 4
[p = std::move(unique_ptr_var)]
: Le constructeur ressemblera à ceci: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").la source
const
qualifié" Pourquoi?const
ça ne peut pas faire de mal ici en raison d'une ambiguïté / meilleure correspondance lorsque nonconst
etc. Quoi qu'il en soit, pensez-vous que je devrais supprimer leconst
?Il est moins nécessaire de spéculer, en utilisant cppinsights.io .
Cas 1:
Code
Le compilateur génère
Cas 2:
Code
Le compilateur génère
Cas 3:
Code
Le compilateur génère
Cas 4 (officieusement):
Code
Le compilateur génère
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 laoperator()
fonction l'est. Naturellement, si vous devez modifier les captures, vous marquez le lambda commemutable
.la source