Implémentations alternatives du mécanisme virtuel?

C ++ prend en charge la liaison dynamic via un mécanisme virtuel. Mais si je comprends bien, le mécanisme virtuel est un détail d’implémentation du compilateur et la norme spécifie simplement les comportements de ce qui devrait se produire dans des scénarios spécifiques. La plupart des compilateurs implémentent le mécanisme virtuel via la table virtuelle et le pointeur virtuel. Et oui, je suis conscient de la façon dont cela fonctionne, alors ma question ne concerne pas les détails d’implémentation des pointeurs virtuels et de la table. Mes questions sont:

  1. Existe-t-il des compilateurs qui implémentent le mécanisme virtuel d’une autre manière que le pointeur virtuel et le mécanisme de table virtuelle? Pour autant que je sache le plus (lisez g ++, Microsoft visual studio), implémentez-le via une table virtuelle, un mécanisme de pointeur. Existe-t-il pratiquement d’autres implémentations de compilateurs?
  2. La sizeof de n’importe quelle classe avec juste une fonction virtuelle sera la taille d’un pointeur (vptr dedans) sur ce compilateur . Donc, étant donné que le mécanisme virtuel ptr et tbl est implémenté, est-ce que cette déclaration sera toujours vraie?

Il n’est pas vrai que les pointeurs vtable dans les objects soient toujours les plus efficaces. Mon compilateur pour un autre langage utilisé pour utiliser des pointeurs dans l’object pour des raisons similaires mais ne le fait plus: au lieu de cela, il utilise une structure de données distincte qui mappe l’adresse de l’object aux métadonnées requirejses: par le ramasse-miettes.

Cette implémentation coûte un peu plus d’espace de stockage pour un seul object simple, est plus efficace pour les objects complexes comportant de nombreuses bases, et est beaucoup plus efficace pour les tableaux, car une seule entrée est requirejse dans la table de mappage. Mon implémentation particulière peut également trouver les métadonnées contenant un pointeur vers n’importe quel point à l’intérieur de l’object.

La recherche est extrêmement rapide et les besoins de stockage très modestes, car j’utilise la meilleure structure de données de la planète: les baies de Judy.

Je ne connais pas non plus de compilateur C ++ utilisant autre chose que des pointeurs vtable, mais ce n’est pas la seule solution. En fait, la sémantique d’initialisation des classes avec des bases complique toute implémentation. Cela est dû au fait que le type complet doit être visible lorsque l’object est construit. En conséquence de cette sémantique, les objects mixin complexes conduisent à la génération d’ensembles vtables massifs, d’objects volumineux et à une initialisation lente des objects. Ce n’est probablement pas une conséquence de la technique vtable autant que le besoin de suivre servilement l’exigence que le type d’exécution d’un sous-object soit correct à tout moment. En fait, il n’y a pas de raison à cela pendant la construction, car les constructeurs ne sont pas des méthodes et ne peuvent pas utiliser la répartition virtuelle de manière significative: ce n’est pas si clair pour moi que les destructeurs sont de véritables méthodes.

A ma connaissance, toutes les implémentations C ++ utilisent un pointeur vtable, bien qu’il soit assez facile (et peut-être pas aussi malin que vous pourriez le penser de conserver des caches) de garder un petit index dans l’object (1-2 B) et obtenir ensuite la vtable et les informations de type avec une petite table de consultation.

Une autre approche intéressante pourrait être BIBOP (http://foldoc.org/BIBOP) – un gros sac de pages – même si cela aurait des problèmes pour C ++. Idée: mettre des objects du même type sur une page. Obtenez un pointeur sur le descripteur de type / vtable en haut de la page en retirant simplement les bits les moins significatifs du pointeur d’object. (Cela ne fonctionne pas bien pour les objects sur la stack, bien sûr!)

Une autre approche consiste à encoder certains types de balises / index dans les pointeurs d’object eux-mêmes. Par exemple, si par construction tous les objects sont alignés sur 16 octets, vous pouvez utiliser les 4 LSB pour y insérer une balise de type 4 bits. (Pas vraiment suffisant.) Ou (en particulier pour les systèmes embarqués) si vous avez garanti plus de bits significatifs inutilisés dans les adresses, vous pouvez y placer plus de bits de tags et les récupérer avec un décalage et un masque.

Bien que ces deux schémas soient intéressants (et parfois utilisés) pour d’autres implémentations de langage, ils posent problème pour C ++. Certaines sémantiques C ++, telles que les substitutions de fonctions virtuelles de la classe de base sont appelées pendant la construction et la destruction des objects (classe de base), vous amènent à un modèle dans lequel vous modifiez certains objects lorsque vous entrez des facteurs de base.

Vous pouvez trouver mon ancien tutoriel sur l’implémentation du modèle d’object Microsoft C ++ intéressant. http://www.openrce.org/articles/files/jangrayhood.pdf

Heureux piratage!

Existe-t-il des compilateurs qui implémentent le mécanisme virtuel d’une autre manière que le pointeur virtuel et le mécanisme de table virtuelle? Pour autant que je sache le plus (lisez g ++, Microsoft visual studio), implémentez-le via une table virtuelle, un mécanisme de pointeur. Existe-t-il pratiquement d’autres implémentations de compilateurs?

Tous les compilateurs actuels que je connais utilisent le mécanisme vtable.

Ceci est une optimisation possible car C ++ est statiquement vérifié.

Dans certains langages plus dynamics, il y a plutôt une recherche dynamic dans la ou les chaînes de classes de base, en recherchant une implémentation d’une fonction membre appelée virtuellement, commençant dans la classe la plus dérivée de l’object. Par exemple, c’est comme ça que ça fonctionnait dans Smalltalk original. Et la norme C ++ décrit l’effet d’un appel virtuel comme si une telle recherche avait été utilisée.

Dans Borland / Turbo Pascal dans les années 1990, une telle recherche dynamic était utilisée pour trouver des gestionnaires de “messages de fenêtre” de l’API Windows. Et je pense peut-être la même chose dans Borland C ++. Il s’ajoutait au mécanisme vtable normal, utilisé uniquement pour les gestionnaires de messages.

S’il était utilisé dans Borland / Turbo C ++ – je ne me souviens plus -, il était alors compatible avec les extensions de langage qui vous permettaient d’associer des identifiants de message aux fonctions du gestionnaire de messages.

La taille de n’importe quelle classe avec juste une fonction virtuelle sera la taille d’un pointeur (vptr dans le this) sur ce compilateur. Donc, étant donné que le mécanisme virtuel ptr et tbl est lui-même implémenté, cette déclaration sera-t-elle toujours vraie?

Formellement non (même en supposant un mécanisme vtable), cela dépend du compilateur. Comme la norme ne nécessite pas le mécanisme vtable, elle ne dit rien sur le placement du pointeur vtable dans chaque object. Et d’autres règles permettent au compilateur d’append librement un remplissage, des octets inutilisés, à la fin.

Mais dans la pratique peut-être. 😉

Cependant, ce n’est pas quelque chose sur lequel vous devez compter ou sur lequel vous devez vous appuyer. Mais dans l’autre sens, vous pouvez en avoir besoin , par exemple si vous définissez un ABI. Ensuite, tout compilateur qui ne fonctionne pas ne répond pas à vos exigences.

Cheers & hth.,

  1. Je ne pense pas qu’il existe des compilateurs modernes avec une approche autre que vptr / vtable. En effet, il serait difficile de trouver autre chose qui ne soit pas simplement inefficace.

    Cependant, il existe encore une grande marge de manœuvre pour les compromis de conception dans cette approche. Peut-être en particulier en ce qui concerne la gestion de l’inheritance virtuel. Il est donc logique de définir cette implémentation.

    Si vous êtes intéressé par ce genre de choses, je vous suggère fortement de lire Inside the C ++ Object Model .

  2. sizeof class dépend du compilateur. Si vous voulez un code portable, ne faites aucune hypothèse.

En essayant d’imaginer un schéma alternatif, j’ai trouvé ce qui suit, à l’instar de la réponse d’ Ytsortingl . A ma connaissance, aucun compilateur ne l’utilise!

Étant donné un espace d’adressage virtuel suffisamment grand et des routines d’allocation de mémoire de système d’exploitation flexibles, il serait possible pour new d’atsortingbuer des objects de types différents dans des plages d’adresses fixes ne se chevauchant pas. Ensuite, le type d’un object peut être déduit rapidement de son adresse en utilisant une opération de décalage à droite , et le résultat est utilisé pour indexer une table de vtables, économisant ainsi 1 pointeur vtable par object.

À première vue, ce schéma peut sembler présenter des problèmes avec les objects alloués à la stack, mais cela peut être géré correctement:

  1. Pour chaque object alloué à la stack, le compilateur ajoute du code qui ajoute un enregistrement à un tableau global de paires (address range, type) lorsque l’object est créé et supprime l’enregistrement lorsqu’il est détruit.
  2. La plage d’adresses comprenant la stack correspondrait à une seule vtable contenant un grand nombre de thunks qui lisent le pointeur this , parsingnt le tableau pour trouver le type correspondant (vptr) de l’object à cette adresse et appellent la méthode correspondante dans la vtable. souligné. (Si le 42ème thunk appelle la méthode 42nd dans la vtable – si la plupart des fonctions virtuelles utilisées dans une classe est n , alors au moins n thunks sont requirejs.)

Ce schéma implique évidemment une surcharge non sortingviale (au moins O (log n) pour la recherche) pour les appels de méthode virtuels sur des objects basés sur des stacks. En l’absence de tableaux ou de composition (confinement dans un autre object) d’objects basés sur une stack, une approche plus simple et plus rapide peut être utilisée dans laquelle le vptr est placé sur la stack immédiatement avant l’object (notez qu’il n’est pas considéré comme faisant partie du object et ne consortingbue pas à sa taille telle que mesurée par sizeof ). Dans ce cas, les thunks soustraient simplement la sizeof (vptr) de this pour trouver le vptr correct à utiliser et le transférer comme précédemment.

  1. Je n’ai jamais entendu parler ni vu aucun compilateur utilisant une implémentation alternative. La raison pour laquelle vtables est si populaire est que non seulement c’est l’implémentation la plus efficace, mais c’est aussi la conception la plus simple et l’implémentation la plus évidente.

  2. Sur presque tous les compilateurs que vous souhaitez utiliser, c’est certainement vrai. Cependant, ce n’est pas garanti et pas toujours vrai – vous ne pouvez pas en dépendre, même si c’est toujours le cas. Votre compilateur préféré pourrait également modifier son alignement, en augmentant sa taille, pour les funsies, sans vous le dire. De la mémoire, il peut également insérer n’importe quelle information de débogage et tout ce qui lui plaît.

Existe-t-il des compilateurs qui implémentent le mécanisme virtuel d’une autre manière que le pointeur virtuel et le mécanisme de table virtuelle? Pour autant que je sache le plus (lisez g ++, Microsoft visual studio), implémentez-le via une table virtuelle, un mécanisme de pointeur. Existe-t-il pratiquement d’autres implémentations de compilateurs?

Je ne connais aucun compilateur utilisant C ++, bien que vous puissiez trouver intéressant de lire Batch Tree Dispatch. Si vous souhaitez exploiter les attentes des tables de répartition virtuelles de quelque manière que ce soit, sachez que les compilateurs peuvent parfois, lorsqu’ils sont connus au moment de la compilation, résoudre des appels de fonction virtuels au moment de la compilation.

La taille de n’importe quelle classe avec juste une fonction virtuelle sera la taille d’un pointeur (vptr dans le this) sur ce compilateur. Donc, étant donné que le mécanisme virtuel ptr et tbl est lui-même implémenté, cette déclaration sera-t-elle toujours vraie?

En supposant qu’aucune classe de base avec ses propres membres virtuels, et aucune classe de base virtuelle, il est très probable que ce soit vrai. Des alternatives peuvent être envisagées – telles que l’parsing de programmes complets ne révélant qu’un seul membre dans la hiérarchie des classes, et un passage à l’envoi à la compilation. Si une répartition au moment de l’exécution est requirejse, il est difficile d’imaginer pourquoi un compilateur introduirait une indirection supplémentaire. Cependant, la norme ne stipule pas ces choses de manière délibérée, de sorte que les mises en œuvre peuvent varier ou être modifiées à l’avenir.

C ++ / CLI s’écarte des deux hypothèses. Si vous définissez une classe de référence, elle n’est pas du tout compilée dans le code machine; à la place, le compilateur le comstack dans le code managé .NET. Dans le langage intermédiaire, les classes sont une fonctionnalité intégrée et l’ensemble des méthodes virtuelles est défini dans les métadonnées, plutôt que dans un tableau de méthodes.

La stratégie spécifique pour implémenter la mise en page et la répartition des objects dépend de la machine virtuelle. Dans Mono, un object contenant une seule méthode virtuelle n’a pas la taille d’un pointeur, mais nécessite deux pointeurs dans la structure MonoObject ; le second pour la synchronisation de l’object. Comme ceci est défini par l’implémentation et n’est pas vraiment utile à connaître, sizeof n’est pas pris en charge pour les classes ref en C ++ / CLI.

IIRC Eiffel utilise une approche différente et tous les remplacements d’une méthode finissent par être fusionnés et compilés dans la même adresse avec un prolog où le type d’object est vérifié (chaque object doit donc avoir un ID de type, mais ce n’est pas un pointeur VMT). Ceci pour C ++ exigerait bien sûr que la fonction finale soit créée au moment du lien. Cependant, je ne connais aucun compilateur C ++ qui utilise cette approche.

La réponse de Tony D souligne à juste titre que les compilateurs sont autorisés à utiliser l’parsing de programmes complets pour remplacer un appel de fonction virtuel par un appel statique à l’implémentation de fonction unique possible; ou pour comstackr obj->method() dans l’équivalent de

 if (auto frobj = dynamic_cast(obj)) { frobj->FrequentlyOccurringType::method(); // static dispatch on hot path } else { obj->method(); // vtable dispatch on cold path } 

En 1996, Karel Driesen et Urs Hölzle ont écrit un article vraiment fascinant dans lequel ils simulaient l’effet de l’optimisation parfaite de programmes complets sur des applications C ++ classiques: «Le coût direct des appels de fonction virtuelle en C ++» . (Le PDF est disponible gratuitement si vous utilisez Google pour cela.) Malheureusement, ils n’ont évalué que la dissortingbution vtable et la dissortingbution statique parfaite; ils ne l’ont pas comparé à l’envoi d’arbres binarys.

Ils ont fait remarquer qu’il existe en réalité deux types de vtables, lorsque vous parlez de langages (comme C ++) qui prennent en charge l’inheritance multiple. Avec l’inheritance multiple, lorsque vous appelez une méthode virtuelle héritée de la deuxième classe de base, vous devez “corriger” le pointeur d’object afin qu’il pointe vers une instance de la deuxième classe de base. Ce décalage de correction peut être stocké sous forme de données dans la vtable ou stocké sous forme de code dans un “thunk”. (Voir le papier pour plus de détails.)

Je crois que tous les compilateurs décents de nos jours utilisent des thunks, mais il a fallu 10 ou 20 ans pour que cette pénétration du marché atteigne 100%.

Tout d’abord, il a été mentionné l’extension propriétaire de Borland à C ++, Dynamic Dispatch Virtual Tables (DDVT), et vous pouvez en lire quelque chose dans un fichier nommé DDISPATC.ZIP . Borland Pascal avait des méthodes à la fois virtuelles et dynamics , et Delphi a introduit une autre syntaxe de “message” , similaire à la dynamic, mais pour les messages. À ce stade, je ne suis pas sûr que Borland C ++ ait les mêmes fonctionnalités. Il n’y avait pas d’inheritance multiple dans Pascal ou Delphi, donc Borland C ++ DDVT pourrait être différent de Pascal ou de Delphi.

Deuxièmement, dans les années 1990 et un peu plus tôt, il y avait des expériences avec différents modèles d’objects, et Borland n’était pas le plus avancé. Personnellement, je pense que la fermeture d’IBM SOMobjects a endommagé le monde dont nous souffrons tous. Avant de fermer SOM, il y avait des expériences avec des compilateurs C ++ Direct-to-SOM. Donc, à la place de la méthode de C ++ pour appeler les méthodes, SOM est utilisé. Il est à bien des égards similaire à C ++ vtable, à quelques exceptions près. Tout d’abord, pour éviter les problèmes de classe de base fragiles, les programmes n’utilisent pas les décalages à l’intérieur de vtable, car ils ne connaissent pas ce décalage. Il peut changer si la classe de base introduit de nouvelles méthodes. Au lieu de cela, les appelants invoquent un thunk créé au moment de l’exécution qui a cette connaissance dans son code assembleur. Et il y a une différence de plus. En C ++, lorsqu’un inheritance multiple est utilisé, un object peut contenir plusieurs VMTs IIRC. Contrairement à C ++, chaque object SOM ne possède qu’un seul VMT, le code de répartition doit donc être différent de “appeler dword ptr [VMT + offset]”.

Il existe un document lié à SOM, la compatibilité binary Release-to-Release dans SOM . Vous pouvez trouver une comparaison de SOM avec d’autres projets que je connais peu, comme Delta / C ++ et Sun OBI . Ils résolvent un sous-ensemble de problèmes que SOM résout et, ce faisant, ils ont également un code d’invocation quelque peu modifié.

J’ai récemment trouvé que le fragment du compilateur Visual Age C ++ v3.5 pour Windows était suffisamment puissant pour que tout fonctionne correctement. La plupart des utilisateurs ne sont pas susceptibles d’obtenir la VM OS / 2 juste pour jouer avec DTS C ++, mais le compilateur Windows est complètement différent. VAC v3.5 est la première et la dernière version à prendre en charge la fonctionnalité Direct-to-SOM C ++. VAC v3.6.5 et v4.0 ne sont pas appropriés.

  1. Téléchargez VAC 3.5 fixpak 9 depuis IBM FTP. Ce patch contient beaucoup de fichiers, vous n’avez donc pas besoin de comstackr complètement (j’ai la dissortingbution 3.5.7, mais fixpak 9 était assez gros pour faire des tests).
  2. Décompresser par ex. C: \ home \ OCTAGRAM \ DTS
  3. Démarrer la ligne de commande et y exécuter les commandes suivantes
  4. Exécuter: définir SOMBASE = C: \ home \ OCTAGRAM \ DTS \ ibmcppw
  5. Exécutez: C: \ home \ OCTAGRAM \ DTS \ ibmcppw \ bin \ SOMENV.BAT
  6. Exécutez: cd C: \ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ comstackr \ dts
  7. Exécuter: nmake clean
  8. Exécuter: nmake
  9. hhmain.exe et sa DLL se trouvent dans des répertoires différents, nous devons donc les amener à se trouver en quelque sorte; comme je faisais plusieurs expériences, j’ai exécuté “set PATH =% PATH%; C: \ home \ OCTAGRAM \ DTS \ ibmcppw \ samples \ comstackr \ dts \ xhmain \ dtsdll” une fois, mais vous pouvez simplement copier dll près de hhmain. EXE
  10. Exécuter: hhmain.exe

J’ai un résultat de cette façon:

 Local anInfo->x = 5 Local anInfo->_get_x() = 5 Local anInfo->y = A Local anInfo->_get_y() = B {An instance of class info at address 0092E318 }