Le lambda en question n'a en fait aucun état .
Examiner:
struct lambda {
auto operator()() const { return 17; }
};
Et si nous en avions lambda f;
, c'est une classe vide. Non seulement ce qui précède est lambda
fonctionnellement similaire à votre lambda, mais c'est (essentiellement) comment votre lambda est implémenté! (Il a également besoin d'un opérateur de pointeur de conversion implicite en fonction, et le nom lambda
va être remplacé par un pseudo-guid généré par le compilateur)
En C ++, les objets ne sont pas des pointeurs. Ce sont des choses réelles. Ils n'utilisent que l'espace nécessaire pour y stocker les données. Un pointeur vers un objet peut être plus grand qu'un objet.
Bien que vous puissiez considérer ce lambda comme un pointeur vers une fonction, ce n'est pas le cas. Vous ne pouvez pas réaffecter le auto f = [](){ return 17; };
à une autre fonction ou lambda!
auto f = [](){ return 17; };
f = [](){ return -42; };
ce qui précède est illégal . Il n'y a pas de place dans f
la boutique où la fonction va être appelée - ces informations sont stockées dans le genre de f
, pas la valeur f
!
Si vous avez fait ceci:
int(*f)() = [](){ return 17; };
ou ca:
std::function<int()> f = [](){ return 17; };
vous ne stockez plus directement le lambda. Dans ces deux cas, f = [](){ return -42; }
est légal - donc dans ces cas, nous stockons qui fonction nous invoquons la valeur de f
. Et sizeof(f)
n'est plus 1
, mais plutôt sizeof(int(*)())
ou plus grand (en gros, soyez de la taille du pointeur ou plus grand, comme vous vous en doutez. A std::function
une taille minimale impliquée par la norme (ils doivent pouvoir stocker des callables "en eux-mêmes" jusqu'à une certaine taille) qui est au moins aussi grand qu'un pointeur de fonction en pratique).
Dans ce int(*f)()
cas, vous stockez un pointeur de fonction vers une fonction qui se comporte comme si vous appeliez ce lambda. Cela ne fonctionne que pour les lambdas sans état (ceux avec une []
liste de capture vide ).
Dans ce std::function<int()> f
cas, vous créez une std::function<int()>
instance de classe d'effacement de type qui (dans ce cas) utilise le placement new pour stocker une copie du lambda de taille 1 dans un tampon interne (et, si un lambda plus grand a été passé (avec plus d'état ), utiliserait l'allocation de tas).
En guise de supposition, quelque chose comme celui-ci est probablement ce que vous pensez qu'il se passe. Qu'un lambda est un objet dont le type est décrit par sa signature. En C ++, il a été décidé de créer des abstractions lambdas à coût nul sur l'implémentation manuelle des objets de fonction. Cela vous permet de passer un lambda dans un std
algorithme (ou similaire) et de faire en sorte que son contenu soit entièrement visible par le compilateur lorsqu'il instancie le modèle d'algorithme. Si un lambda avait un type comme std::function<void(int)>
, son contenu ne serait pas entièrement visible et un objet fonction fabriqué à la main pourrait être plus rapide.
Le but de la standardisation C ++ est une programmation de haut niveau sans surcharge par rapport au code C fabriqué à la main.
Maintenant que vous comprenez que vous êtes f
en fait apatride, il devrait y avoir une autre question dans votre tête: le lambda n'a pas d'état. Pourquoi n'a-t-il pas de taille 0
?
Il y a la réponse courte.
Tous les objets en C ++ doivent avoir une taille minimale de 1 selon la norme, et deux objets du même type ne peuvent pas avoir la même adresse. Ceux-ci sont connectés, car un tableau de type T
aura les éléments sizeof(T)
séparés.
Maintenant, comme il n'a pas d'état, parfois il ne peut pas prendre de place. Cela ne peut pas arriver quand il est «seul», mais dans certains contextes, cela peut arriver. std::tuple
et un code de bibliothèque similaire exploite ce fait. Voici comment cela fonctionne:
Comme un lambda équivaut à une classe operator()
surchargée, les lambdas sans état (avec une []
liste de capture) sont toutes des classes vides. Ils ont sizeof
de 1
. En fait, si vous en héritez (ce qui est autorisé!), Ils ne prendront pas de place tant que cela ne provoquera pas de collision d'adresse de même type . (Ceci est connu comme l'optimisation de la base vide).
template<class T>
struct toy:T {
toy(toy const&)=default;
toy(toy &&)=default;
toy(T const&t):T(t) {}
toy(T &&t):T(std::move(t)) {}
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
le sizeof(make_toy( []{std::cout << "hello world!\n"; } ))
est sizeof(int)
(enfin, ce qui précède est illégal car vous ne pouvez pas créer un lambda dans un contexte non évalué: vous devez créer un named auto toy = make_toy(blah);
then do sizeof(blah)
, mais ce n'est que du bruit). sizeof([]{std::cout << "hello world!\n"; })
est toujours 1
(qualifications similaires).
Si nous créons un autre type de jouet:
template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):T(t), t2(t) {}
T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }
cela a deux copies du lambda. Comme ils ne peuvent pas partager la même adresse, sizeof(toy2(some_lambda))
c'est 2
!
struct
avec unoperator()
)