Pourquoi la taille d’un pointeur sur une fonction est-elle différente de la taille d’un pointeur sur une fonction membre?

Un pointeur n’est-il pas juste une adresse? Ou je manque quelque chose?

J’ai testé avec plusieurs types de pointeurs:

  • les pointeurs sur les variables sont les mêmes (8B sur ma plate-forme)
  • les pointeurs vers les fonctions ont la même taille que les pointeurs vers les variables (8B à nouveau)
  • pointeurs vers des fonctions avec différents parameters – toujours les mêmes (8B)

MAIS les pointeurs vers les fonctions membres sont plus gros – 16B sur ma plate-forme.

Trois choses:

  1. Pourquoi les pointeurs sur les fonctions membres sont-ils plus grands? De quelle autre information ont-ils besoin?
  2. Autant que je sache, la norme ne dit rien sur la taille d’un pointeur, sauf que void* doit pouvoir “contenir” tout type de pointeur. En d’autres termes, tout pointeur doit pouvoir être converti en void* , n’est-ce pas? Si oui, alors pourquoi sizeof( void* ) vaut 8 alors que sizeof un pointeur sur une fonction membre vaut 16?
  3. Existe-t-il d’autres exemples de pointeurs ayant une taille différente (je veux dire, des plates formes standard et non des plates formes rares et spéciales)?

EDIT : Alors, j’ai remarqué que j’obtenais encore des votes à ce sujet quelques mois plus tard, même si ma réponse initiale était mauvaise et trompeuse (je ne me souviens même pas de ce que je pensais à ce moment-là, et cela n’a pas beaucoup de sens !) alors j’ai pensé essayer de clarifier la situation, car les gens doivent toujours arriver ici par la recherche.

Dans la situation la plus normale, vous pouvez à peu près penser à

 struct A { int i; int foo() { return i; } }; A a; a.foo(); 

comme

 struct A { int i; }; int A_foo( A* this ) { return this->i; }; A a; A_foo(&a); 

(Commençant à ressembler à C , n’est-ce pas?) On pourrait donc penser que le pointeur &A::foo est identique à un pointeur de fonction normal. Mais il y a quelques complications: l’inheritance multiple et les fonctions virtuelles.

Alors, imaginons que nous ayons:

 struct A {int a;}; struct B {int b;}; struct C : A, B {int c;}; 

Cela pourrait être présenté comme ceci:

Héritage multiple

Comme vous pouvez le constater, si vous souhaitez indiquer l’object avec un A* ou un C* , vous indiquez le début, mais si vous souhaitez l’indiquer avec un B* vous devez indiquer un point quelque part au milieu. Donc, si C hérite d’une fonction membre de B et que vous voulez y pointer, appelez la fonction sur un C* , il doit savoir comment mélanger le pointeur this . Cette information doit être stockée quelque part. Donc, il est regroupé avec le pointeur de la fonction.

Désormais, pour chaque classe comportant virtual fonctions virtual , le compilateur en crée une liste appelée table virtuelle . Il ajoute ensuite un pointeur supplémentaire à cette table dans la classe ( vptr ). Donc pour cette structure de classe:

 struct A { int a; virtual void foo(){}; }; struct B : A { int b; virtual void foo(){}; virtual void bar(){}; }; 

Le compilateur pourrait finir par le faire comme ceci: entrez la description de l'image ici

Ainsi, un pointeur de fonction membre sur une fonction virtuelle doit en réalité être un index dans la table virtuelle. Ainsi, un pointeur de fonction membre nécessite en fait 1) éventuellement un pointeur de fonction, 2) éventuellement un ajustement du pointeur this , et 3) éventuellement un index vtable. Pour être cohérent, chaque pointeur de fonction membre doit être capable de tout cela. Soit 8 octets pour le pointeur, 4 octets pour l’ajustement, 4 octets pour l’index, pour un total de 16 octets.

Je pense que c’est quelque chose qui varie beaucoup entre les compilateurs, et il y a beaucoup d’optimisations possibles. Probablement personne ne l’implémente réellement comme je l’ai décrit.

Voir ceci pour plus de détails (allez à “Implémentations des pointeurs de fonction de membre”).

Fondamentalement, ils doivent prendre en charge le comportement polymorphe. Voir un bel article de Raymond Chen.

Quelques explications peuvent être trouvées ici: La représentation sous-jacente des pointeurs de fonction membre

Bien que les pointeurs vers les membres se comportent comme des pointeurs ordinaires, leur représentation est tout à fait différente en coulisse. En fait, un pointeur sur membre consiste généralement en une structure contenant jusqu’à quatre champs dans certains cas. En effet, les pointeurs vers les membres doivent prendre en charge non seulement les fonctions de membre ordinaires, mais également les fonctions de membre virtuelles, les fonctions de membre d’objects ayant plusieurs classes de base et les fonctions de membre de classes de base virtuelles. Ainsi, la fonction membre la plus simple peut être représentée par un ensemble de deux pointeurs: un contenant l’adresse de mémoire physique de la fonction membre et un second pointeur contenant le pointeur this. Toutefois, dans les cas tels qu’une fonction membre virtuelle, un inheritance multiple et un inheritance virtuel, le pointeur sur membre doit stocker des informations supplémentaires. Par conséquent, vous ne pouvez pas diriger des pointeurs vers des membres sur des pointeurs ordinaires, ni sur des membres de types différents. Blockquote

Je suppose que cela a quelque chose à voir avec this pointeur … C’est-à-dire que chaque fonction membre doit également avoir le pointeur de la classe dans laquelle il se trouve. Le pointeur rend alors la fonction un peu plus grande.

Les principales raisons de représenter les pointeurs vers les fonctions membres sous la forme {this, T (*f)()} sont les suivantes:

  • L’implémentation dans le compilateur est plus simple que l’implémentation de pointeurs vers les fonctions membres en tant que T (*f)()

  • Cela n’implique pas la génération de code d’exécution ni la comptabilité supplémentaire

  • Il fonctionne raisonnablement bien par rapport à T (*f)()

  • Il n’ya pas assez de demande de la part des programmeurs C ++ pour que la taille des pointeurs vers les fonctions membres soit égale à sizeof(void*)

  • La génération de code d’exécution pendant l’exécution est de facto un tabou pour le code C ++ actuellement