TL;DR : Le code dans la question n'est pas garanti par la norme, et il y a des implémentations raisonnables de lambdas qui le font casser. Supposons qu'il ne soit pas portable et utilisons plutôt
std::function<void()> make_function(int& x)
{
const auto px = &x;
return [/* = */ px]{ std::cout << *px << std::endl; };
}
À partir de C++14, vous pouvez vous passer de l'utilisation explicite d'un pointeur en utilisant une capture initialisée, qui force la création d'une nouvelle variable de référence pour la lambda, au lieu de réutiliser celle de la portée englobante :
std::function<void()> make_function(int& x)
{
return [&x = x]{ std::cout << x << std::endl; };
}
A première vue, il semble que devrait être sûr, mais la formulation de la norme pose un petit problème :
Une expression lambda dont la plus petite portée englobante est une portée de bloc (3.3.3) est une expression lambda locale ; toute autre expression lambda ne doit pas avoir de capture-default ou de simple-capture dans son introducteur lambda. Le site portée de l'action d'une expression lambda locale est l'ensemble des portées englobantes jusqu'à et y compris l'élément fonction englobante la plus intérieure et ses paramètres.
...
Toutes ces entités capturées implicitement doivent être déclarées dans la portée de l'expression lambda.
...
[Note : Si une entité est implicitement ou explicitement capturée par référence, invoquer l'opérateur d'appel de fonction de l'expression lambda correspondante après la fin de la durée de vie de l'entité est susceptible d'entraîner un comportement non défini. - fin de la note ]
Ce que nous attendons, c'est que x
utilisé à l'intérieur make_function
Il s'agit de i
en main()
(puisque c'est ce que font les références), et l'entité i
est capturé par référence. Puisque cette entité vit toujours au moment de l'appel lambda, tout est bon.
Mais ! les "entités capturées implicitement" doivent être "dans le champ d'application de l'expression lambda", et i
en main()
n'est pas dans le périmètre d'atteinte :( A moins que le paramètre x
compte comme "déclarée dans le champ d'application" même si l'entité i
elle-même est en dehors du champ d'application.
Ce à quoi ça ressemble, c'est que, contrairement à tout autre endroit en C++, une référence à une référence est créée et la durée de vie d'une référence a un sens.
C'est définitivement quelque chose que j'aimerais voir la norme clarifier.
En attendant, la variante présentée dans la section TL;DR est définitivement sûre car le pointeur est capturé par valeur (stocké à l'intérieur de l'objet lambda lui-même), et c'est un pointeur valide vers un objet qui dure jusqu'à l'appel de la lambda. Je m'attends également à ce que la capture par référence finisse par stocker un pointeur de toute façon, donc il ne devrait pas y avoir de pénalité d'exécution pour faire cela.
En y regardant de plus près, on imagine aussi qu'il pourrait se casser. Rappelez-vous que sur x86, dans le code machine final, les variables locales et les paramètres de fonction sont accessibles en utilisant l'adressage relatif EBP. Les paramètres ont un décalage positif, tandis que les variables locales sont négatives. (D'autres architectures ont des noms de registres différents mais beaucoup fonctionnent de la même manière). Quoi qu'il en soit, cela signifie que la capture par référence peut être implémentée en capturant uniquement la valeur de EBP. Ensuite, les locales et les paramètres peuvent à nouveau être trouvés via l'adressage relatif. En fait, je crois avoir entendu parler d'implémentations de lambdas (dans des langages qui avaient des lambdas bien avant C++) faisant exactement cela : capturer le "stack frame" où le lambda a été défini.
Ce que cela implique, c'est que lorsque make_function
retourne et son cadre de pile disparaît, ainsi que toute capacité d'accès aux locaux ET aux paramètres, même ceux qui sont des références.
Et la norme contient la règle suivante, sans doute spécifiquement pour permettre cette approche :
Il n'est pas précisé 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.
Conclusion : Le code dans la question n'est pas garanti par la norme, et il y a des implémentations raisonnables de lambdas qui provoquent sa rupture. Supposons qu'il ne soit pas portable.
2 votes
Notez qu'une autre solution sûre pour capturer l'emplacement des
x
est :std::function<void()> make_function(int& x) { auto px = &x; return [=](){ std::cout << *px << std::endl; }; }
0 votes
Oui, cela vaut la peine d'être mentionné. Merci.
0 votes
Je viens de mettre à jour le commentaire ci-dessus pour montrer que le paramètre peut rester une référence. L'important est de fermer sur un pointeur, par valeur.
0 votes
Existe-t-il vraiment une "référence à une référence" ? Ma compréhension a toujours été que toute référence est simplement à l'instance originale et non à la ou aux références à partir desquelles elle a été créée. C'est-à-dire que même s'il y a une chaîne de références faites à partir d'autres références, les références intermédiaires peuvent sortir du champ d'application sans affecter les références créées à partir d'elles, tant que l'élément original est toujours actuel.