105 votes

Comment std :: function est-il implémenté?

Selon les sources que j'ai trouvé, une expression lambda est essentiellement mis en œuvre par le compilateur de la création d'une classe surchargée appel de fonction de l'opérateur et les variables référencées en tant que membres. Ceci suggère que la taille des expressions lambda varie, et donné assez de références de variables que la taille peut être arbitrairement grand.

Un std::function devrait avoir une taille fixe, mais il doit être capable d'encapsuler n'importe quel type de callables, y compris toute lambdas de même nature. Comment est-il mis en œuvre? Si std::function utilise en interne un pointeur vers sa cible, puis ce qui se passe, lorsque l' std::function instance est copié ou déplacé? Existe-il des allocations de tas impliqués?

84voto

La mise en œuvre de l' std::function peuvent varier d'une implémentation à une autre, mais l'idée de base est qu'il utilise le type-erasure. Bien qu'il existe de multiples façons de le faire, vous pouvez imaginer un triviale (non optimale) solution pouvait être comme ça (simplifié pour le cas spécifique de l' std::function<int (double)> , par souci de simplicité):

struct callable_base {
   virtual int operator()(double d) = 0;
   virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
   F functor;
   callable(F functor) : functor(functor) {}
   virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
   std::unique_ptr<callable_base> c;
public:
   template <typename F>
   function(F f) {
      c.reset(new callable(f));
   }
   int operator()(double d) { return c(d); }
// ...
};

Dans cette approche simple de l' function objet serait de stocker juste un unique_ptr d'un type de base. Pour chaque foncteur utilisé avec l' function, un nouveau type dérivé de la base est créée et un objet de ce type instanciés dynamiquement. L' std::function objet est toujours de la même taille et va allouer de l'espace nécessaire pour les différents foncteurs dans le tas.

Dans la vraie vie, il y a différentes optimisations que de fournir des avantages de performance, mais ne ferait que compliquer la réponse. Le type peut utiliser de petits objets, des optimisations, la répartition dynamique peut être remplacé par un libre-pointeur de fonction 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 comment les copies de l' std::function se comporter, un test rapide indique que des copies de l'intérieur de l'objet appelable sont fait, plutôt que de partager l'état.

// g++4.8
int main() {
   int value = 5;
   typedef std::function<void()> fun;
   fun f1 = [=]() mutable { std::cout << value++ << '\n' };
   fun f2 = f1;
   f1();                    // prints 5
   fun f3 = f1;
   f2();                    // prints 5
   f3();                    // prints 6 (copy after first increment)
}

Le test indique qu' f2 obtient une copie de la appelable entité, plutôt qu'une référence. Si le callable entité a été partagée par les différents std::function<> objets, la sortie du programme aurait été de 5, 6, 7.

20voto

hvd Points 42125

Pour certains types d'arguments ("si f est la cible est un objet appelable transmises par le biais d' reference_wrapper ou un pointeur de fonction"), std::functions'constructeur n'autorise pas de tout exceptions, donc utilisation de la mémoire dynamique est hors de question. Pour ce cas, toutes les données doivent être stockées directement à l'intérieur de l' std::function objet.

Dans le cas général, (y compris le lambda cas), l'utilisation de la mémoire dynamique (via l'allocateur standard, ou un allocateur passé à l' std::function constructeur) est admis que la mise en œuvre, le juge approprié. La norme recommande d'implémentations ne pas utiliser de la mémoire dynamique si elle peut être évitée, mais comme vous le dites à juste titre, si la fonction de l'objet (pas l' std::function de l'objet, mais l'objet enveloppé à l'intérieur) est assez grand, il n'y a aucun moyen de l'empêcher, depuis std::function a une taille fixe.

Cette autorisation de lever des exceptions est accordée à la fois le constructeur normal et le constructeur de copie, ce qui assez autorise explicitement la dynamique des allocations de mémoire lors de la copie. Pour se déplace, il n'ya aucune raison pourquoi la mémoire dynamique serait nécessaire. La norme ne semble pas interdire explicitement, et ne peut probablement pas si le mouvement pourrait appeler le constructeur de déplacement de l'objet enveloppé de son type, mais vous devriez être en mesure de supposer que si la mise en œuvre et de vos objets sont sensibles, le déplacement ne causera pas d'allocations.

-5voto

aaronman Points 8034

Un std::function surcharge le operator() faisant un objet foncteur, le travail de lambda fonctionne de la même manière. Fondamentalement, il crée une structure avec des variables membres accessibles via la fonction operator() . Donc, le concept de base à garder à l'esprit est qu'un lambda est un objet (appelé un foncteur ou un objet de fonction) et non une fonction. La norme dit de ne pas utiliser de mémoire dynamique si cela peut être évité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X