Selon les sources que j'ai trouvées, une expression lambda est essentiellement implémentée par le compilateur créant une classe avec un opérateur d'appel de fonction surchargé et les variables référencées en tant que membres. Cela suggère que la taille des expressions lambda varie et que, compte tenu de suffisamment de variables de référence, la taille peut être arbitrairement grande .
Un std::function
devrait avoir une taille fixe , mais il doit être capable d'encapsuler n'importe quel type de callable, y compris n'importe quel lambdas du même type. Comment est-il mis en œuvre? Si std::function
utilise en interne un pointeur vers sa cible, que se passe-t-il lorsque l' std::function
instance est copiée ou déplacée? Y a-t-il des allocations de tas impliquées?
std::function
quelque temps. Il s'agit essentiellement d'une classe de descripteurs pour un objet polymorphe. Une classe dérivée de la classe de base interne est créée pour contenir les paramètres, alloués sur le tas - puis le pointeur vers celui-ci est conservé comme un sous-objet destd::function
. Je crois qu'il utilise le comptage de références commestd::shared_ptr
pour gérer la copie et le déplacement.Réponses:
L'implémentation de
std::function
peut différer d'une implémentation à l'autre, mais l'idée de base est qu'elle utilise l'effacement de type. Bien qu'il existe plusieurs façons de le faire, vous pouvez imaginer qu'une solution triviale (non optimale) pourrait être comme celle-ci (simplifiée pour le cas spécifique destd::function<int (double)>
par souci de simplicité):Dans cette approche simple, l'
function
objet stockerait uniquement ununique_ptr
type de base. Pour chaque foncteur différent utilisé avec lefunction
, un nouveau type dérivé de la base est créé et un objet de ce type instancié dynamiquement. L'std::function
objet est toujours de la même taille et allouera de l'espace selon les besoins pour les différents foncteurs du tas.Dans la vraie vie, il existe différentes optimisations qui offrent des avantages en termes de performances mais compliqueraient la réponse. Le type pourrait utiliser des optimisations de petits objets, la répartition dynamique peut être remplacée par un pointeur de fonction libre qui prend le foncteur comme argument pour éviter un niveau d'indirection ... mais l'idée est fondamentalement la même.
Concernant la question de savoir comment les copies
std::function
comportement des comportement, un test rapide indique que des copies de l'objet appelable interne sont effectuées, plutôt que de partager l'état.Le test indique qu'il
f2
obtient une copie de l'entité appelable, plutôt qu'une référence. Si l'entité appelable était partagée par les différentsstd::function<>
objets, la sortie du programme aurait été 5, 6, 7.la source
std::function
serait correcte si l'objet interne était copié, et je ne pense pas que ce soit le cas (pensez à un lambda qui capture une valeur et qui est mutable, stocké dans astd::function
, si l'état de la fonction a été copié, le nombre de copies de l'std::function
intérieur d'un algorithme standard pourrait entraîner des résultats différents, ce qui n'est pas souhaité.std::function
déclenchera une allocation.La réponse de @David Rodríguez - dribeas est bonne pour démontrer l'effacement de type mais pas assez car l'effacement de type inclut également la manière dont les types sont copiés (dans cette réponse, l'objet fonction ne sera pas constructible par copie). Ces comportements sont également stockés dans le
function
objet, en plus des données du foncteur.L'astuce, utilisée dans l'implémentation STL d'Ubuntu 14.04 gcc 4.8, consiste à écrire une fonction générique, à la spécialiser avec chaque type de foncteur possible et à les convertir en un type de pointeur de fonction universel. Par conséquent, les informations de type sont effacées .
J'ai bricolé une version simplifiée de cela. J'espère que ça aidera
Il y a aussi quelques optimisations dans la version STL
construct_f
etdestroy_f
sont mélangés en un pointeur de fonction (avec un paramètre supplémentaire qui dit quoi faire) comme pour sauver quelques octetsunion
, de sorte que lorsqu'unfunction
objet est construit à partir d'un pointeur de fonction, il sera stocké directement dans l'union
espace plutôt que dans l' espace du tasPeut-être que l'implémentation STL n'est pas la meilleure solution car j'ai entendu parler d'une implémentation plus rapide . Cependant, je pense que le mécanisme sous-jacent est le même.
la source
Pour certains types d'arguments ("si la cible de f est un objet appelable passé via
reference_wrapper
ou un pointeur de fonction"),std::function
le constructeur de n'autorise aucune exception, donc l'utilisation de la mémoire dynamique est hors de question. Dans ce cas, toutes les données doivent être stockées directement dans lestd::function
objet.Dans le cas général (y compris le cas lambda), l'utilisation de la mémoire dynamique (via l'allocateur standard ou un allocateur passé au
std::function
constructeur) est autorisée comme l'implémentation le juge opportun. Le standard recommande que les implémentations n'utilisent pas de mémoire dynamique si cela peut être évité, mais comme vous le dites à juste titre, si l'objet fonction (pas l'std::function
objet, mais l'objet qui est enveloppé à l'intérieur) est suffisamment grand, il n'y a aucun moyen de l'empêcher, puisquestd::function
a une taille fixe.Cette autorisation de lever des exceptions est accordée à la fois au constructeur normal et au constructeur de copie, ce qui autorise assez explicitement les allocations de mémoire dynamique pendant la copie. Pour les mouvements, il n'y a aucune raison pour laquelle la mémoire dynamique serait nécessaire. La norme ne semble pas l'interdire explicitement, et ne le peut probablement pas si le déplacement peut appeler le constructeur de déplacement du type de l'objet encapsulé, mais vous devriez pouvoir supposer que si l'implémentation et vos objets sont sensés, le déplacement ne provoquera pas toutes allocations.
la source
An
std::function
surcharge leoperator()
fait d'en faire un objet foncteur, les lambda fonctionnent de la même manière. Il crée essentiellement une structure avec des variables membres accessibles à l'intérieur de laoperator()
fonction. Donc, le concept de base à garder à l'esprit est qu'un lambda est un objet (appelé foncteur ou objet fonction) et non une fonction. La norme dit de ne pas utiliser de mémoire dynamique si cela peut être évité.la source
std::function
? Telle est la question clé ici.std::function
objet a la même taille et ne correspond pas à la taille des lambdas contenus.std::vector<T...>
objet a une taille fixe (copiletime) indépendante de l'instance d'allocateur réelle / du nombre d'éléments.std::function<void ()> f;
pas besoin d'allouer là,std::function<void ()> f = [&]() { /* captures tons of variables */ };
très probablement alloue.std::function<void()> f = &free_function;
n'attribue probablement pas non plus ...