web-dev-qa-db-fra.com

Comment les fonctions virtuelles et vtable sont-elles implémentées?

Nous savons tous quelles sont les fonctions virtuelles en C++, mais comment sont-elles implémentées à un niveau profond?

La table virtuelle peut-elle être modifiée ou même accessible directement lors de l'exécution?

La vtable existe-t-elle pour toutes les classes, ou seulement celles qui ont au moins une fonction virtuelle?

Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

Le fait d'avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement remplacée ou non, ou cela n'a-t-il aucun effet tant qu'elle est virtuelle.

100
Brian R. Bondy

Comment les fonctions virtuelles sont-elles implémentées à un niveau profond?

De "Fonctions virtuelles en C++" :

Chaque fois qu'un programme a une fonction virtuelle déclarée, une v-table est construite pour la classe. La v-table se compose d'adresses vers les fonctions virtuelles pour les classes qui contiennent une ou plusieurs fonctions virtuelles. L'objet de la classe contenant la fonction virtuelle contient un pointeur virtuel qui pointe vers l'adresse de base de la table virtuelle en mémoire. Chaque fois qu'il y a un appel de fonction virtuelle, la v-table est utilisée pour résoudre l'adresse de la fonction. Un objet de la classe qui contient une ou plusieurs fonctions virtuelles contient un pointeur virtuel appelé vptr au tout début de l'objet dans la mémoire. Par conséquent, la taille de l'objet dans ce cas augmente de la taille du pointeur. Ce vptr contient l'adresse de base de la table virtuelle en mémoire. Notez que les tables virtuelles sont spécifiques à une classe, c'est-à-dire qu'il n'y a qu'une seule table virtuelle pour une classe quel que soit le nombre de fonctions virtuelles qu'elle contient. Cette table virtuelle contient à son tour les adresses de base d'une ou plusieurs fonctions virtuelles de la classe. Au moment où une fonction virtuelle est appelée sur un objet, le vptr de cet objet fournit l'adresse de base de la table virtuelle pour cette classe en mémoire. Cette table est utilisée pour résoudre l'appel de fonction car elle contient les adresses de toutes les fonctions virtuelles de cette classe. C'est ainsi que la liaison dynamique est résolue lors d'un appel de fonction virtuelle.

La table virtuelle peut-elle être modifiée ou même accessible directement lors de l'exécution?

Universellement, je crois que la réponse est "non". Vous pouvez faire un peu de mémoire pour trouver la table virtuelle, mais vous ne savez toujours pas à quoi ressemble la signature de fonction pour l'appeler. Tout ce que vous voudriez réaliser avec cette capacité (que le langage prend en charge) devrait être possible sans accéder directement à la table virtuelle ou le modifier au moment de l'exécution. Notez également que la spécification du langage C++ ne spécifie pas que des vtables sont requis - cependant c'est ainsi que la plupart des compilateurs implémentent des fonctions virtuelles.

La vtable existe-t-elle pour tous les objets, ou seulement ceux qui ont au moins une fonction virtuelle?

I croyez la réponse ici est "cela dépend de l'implémentation" puisque la spécification ne nécessite pas de vtables en premier lieu. Cependant, dans la pratique, je pense que tous les compilateurs modernes ne créent une table virtuelle que si une classe a au moins 1 fonction virtuelle. Il existe une surcharge d'espace associée à la table virtuelle et une surcharge de temps associée à l'appel d'une fonction virtuelle par rapport à une fonction non virtuelle.

Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

La réponse est qu'elle n'est pas spécifiée par la spécification de langue, donc cela dépend de l'implémentation. L'appel de la fonction virtuelle pure entraîne un comportement indéfini si elle n'est pas définie (ce qui n'est généralement pas le cas) (ISO/IEC 14882: 2003 10.4-2). En pratique, il alloue un emplacement dans la table virtuelle pour la fonction mais ne lui attribue pas d'adresse. Cela laisse la vtable incomplète, ce qui nécessite que les classes dérivées implémentent la fonction et terminent la vtable. Certaines implémentations placent simplement un pointeur NULL dans l'entrée vtable; d'autres implémentations placent un pointeur sur une méthode factice qui fait quelque chose de similaire à une assertion.

Notez qu'une classe abstraite peut définir une implémentation pour une fonction virtuelle pure, mais cette fonction ne peut être appelée qu'avec une syntaxe d'identifiant qualifié (c'est-à-dire, en spécifiant complètement la classe dans le nom de la méthode, comme pour appeler une méthode de classe de base à partir d'un Classe dérivée). Ceci est fait pour fournir une implémentation par défaut facile à utiliser, tout en exigeant qu'une classe dérivée fournisse un remplacement.

Le fait d'avoir une seule fonction virtuelle ralentit-il toute la classe ou uniquement l'appel à la fonction virtuelle?

Cela arrive à la limite de mes connaissances, alors quelqu'un m'aide s'il vous plaît ici si je me trompe!

I croyez que seules les fonctions qui sont virtuelles dans la classe connaissent le temps de performance lié à l'appel d'une fonction virtuelle par rapport à une fonction non virtuelle. Les frais généraux d'espace pour la classe sont là de toute façon. Notez que s'il y a une table virtuelle, il n'y en a qu'une par classe, pas une par objet.

La vitesse est-elle affectée si la fonction virtuelle est réellement remplacée ou non, ou cela n'a-t-il aucun effet tant qu'elle est virtuelle?

Je ne crois pas que le temps d'exécution d'une fonction virtuelle surchargée diminue par rapport à l'appel de la fonction virtuelle de base. Cependant, il existe une surcharge d'espace supplémentaire pour la classe associée à la définition d'une autre table virtuelle pour la classe dérivée par rapport à la classe de base.

Ressources supplémentaires:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (via la machine de retour)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

115
Zach Burlingame
  • La table virtuelle peut-elle être modifiée ou même accessible directement lors de l'exécution?

Pas portable, mais si cela ne vous dérange pas, c'est sûr!

[~ # ~] avertissement [~ # ~] : cette technique n'est pas recommandée pour les enfants, les adultes de moins de 969 , ou de petites créatures à fourrure d'Alpha Centauri. Les effets secondaires peuvent inclure des démons qui volent hors de votre nez , l'apparition soudaine de Yog-Sothoth en tant qu'approbateur requis sur toutes les révisions de code suivantes, ou l'ajout rétroactif de - IHuman::PlayPiano() à toutes les instances existantes]

Dans la plupart des compilateurs que j'ai vus, le vtbl * est les 4 premiers octets de l'objet, et le contenu de vtbl est simplement un tableau de pointeurs membres là-bas (généralement dans l'ordre où ils ont été déclarés, avec le premier de la classe de base). Il existe bien sûr d'autres dispositions possibles, mais c'est ce que j'ai généralement observé.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Maintenant, pour tirer quelques manigances ...

Changement de classe à l'exécution:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Remplacement d'une méthode pour toutes les instances (monkeypatching une classe)

Celui-ci est un peu plus délicat, car le vtbl lui-même est probablement en mémoire morte.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

Ce dernier est plutôt susceptible de faire se réveiller les virus et le lien se réveiller et prendre note, en raison des manipulations de mprotect. Dans un processus utilisant le bit NX, il peut très bien échouer.

28
puetzk

Le fait d'avoir une seule fonction virtuelle ralentit-il toute la classe?

Ou seulement l'appel à la fonction virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement remplacée ou non, ou cela n'a-t-il aucun effet tant qu'elle est virtuelle.

Le fait d'avoir des fonctions virtuelles ralentit toute la classe dans la mesure où un élément de données supplémentaire doit être initialisé, copié,… lorsqu'il s'agit d'un objet d'une telle classe. Pour une classe avec une demi-douzaine de membres environ, la différence devrait être négligeable. Pour une classe qui ne contient qu'un seul membre char, ou aucun membre du tout, la différence peut être notable.

En dehors de cela, il est important de noter que tous les appels à une fonction virtuelle ne sont pas des appels de fonction virtuelle. Si vous avez un objet d'un type connu, le compilateur peut émettre du code pour une invocation de fonction normale, et peut même incorporer ladite fonction s'il en a envie. Ce n'est que lorsque vous effectuez des appels polymorphes, via un pointeur ou une référence qui pourrait pointer vers un objet de la classe de base ou vers un objet d'une classe dérivée, que vous avez besoin de l'indirection vtable et que vous payez en termes de performances.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

Les étapes que le matériel doit suivre sont essentiellement les mêmes, que la fonction soit écrasée ou non. L'adresse de la table virtuelle est lue à partir de l'objet, le pointeur de fonction récupéré à partir de l'emplacement approprié et la fonction appelée par le pointeur. En termes de performances réelles, les prévisions de branche peuvent avoir un certain impact. Ainsi, par exemple, si la plupart de vos objets font référence à la même implémentation d'une fonction virtuelle donnée, il est possible que le prédicteur de branche prédise correctement la fonction à appeler avant même que le pointeur n'ait été récupéré. Mais peu importe la fonction qui est la plus courante: il peut s'agir de la plupart des objets déléguant au cas de base non écrasé, ou de la plupart des objets appartenant à la même sous-classe et donc déléguant au même cas écrasé.

comment sont-ils mis en œuvre à un niveau profond?

J'aime l'idée de jheriko pour le démontrer en utilisant une implémentation fictive. Mais j'utiliserais C pour implémenter quelque chose de semblable au code ci-dessus, afin que le bas niveau soit plus facilement visible.

classe parent Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

barre de classe dérivée

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

fonction f effectuant un appel de fonction virtuelle

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Comme vous pouvez le voir, une table virtuelle n'est qu'un bloc statique en mémoire, contenant principalement des pointeurs de fonction. Chaque objet d'une classe polymorphe pointera vers la table virtuelle correspondant à son type dynamique. Cela rend également la connexion entre RTTI et les fonctions virtuelles plus claire: vous pouvez vérifier de quel type est une classe simplement en regardant sur quelle table elle pointe. Ce qui précède est simplifié à bien des égards, comme par exemple héritage multiple, mais le concept général est solide.

Si arg est de type Foo* et vous prenez arg->vtable, mais est en fait un objet de type Bar, alors vous obtenez toujours l'adresse correcte du vtable. En effet, le vtable est toujours le premier élément à l'adresse de l'objet, qu'il s'appelle vtable ou base.vtable dans une expression correctement saisie.

17
MvG

Voici une implémentation manuelle exécutable de la table virtuelle en C++ moderne. Il a une sémantique bien définie, pas de hacks et pas de void*.

Remarque: .* et ->* sont des opérateurs différents de * et ->. Les pointeurs de fonction membre fonctionnent différemment.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.Push_back(std::make_unique<cat>("grumpy"));
    animals.Push_back(std::make_unique<cat>("nyan"));
    animals.Push_back(std::make_unique<dog>("doge"));
    animals.Push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}
2
Xeverous

Je vais essayer de faire simple :)

Nous savons tous quelles sont les fonctions virtuelles en C++, mais comment sont-elles implémentées à un niveau profond?

Il s'agit d'un tableau avec des pointeurs vers des fonctions, qui sont des implémentations d'une fonction virtuelle particulière. Un index dans ce tableau représente un index particulier d'une fonction virtuelle définie pour une classe. Cela inclut des fonctions virtuelles pures.

Lorsqu'une classe polymorphe dérive d'une autre classe polymorphe, nous pouvons avoir les situations suivantes:

  • La classe dérivée n'ajoute pas de nouvelles fonctions virtuelles ni n'en remplace aucune. Dans ce cas, cette classe partage la table virtuelle avec la classe de base.
  • La classe dérivée ajoute et remplace les méthodes virtuelles. Dans ce cas, il obtient sa propre table virtuelle, où les fonctions virtuelles ajoutées ont un index commençant après la dernière dérivée.
  • Plusieurs classes polymorphes dans l'héritage. Dans ce cas, nous avons un décalage d'index entre la deuxième et la prochaine base et son index dans la classe dérivée

La table virtuelle peut-elle être modifiée ou même accessible directement lors de l'exécution?

Pas de manière standard - il n'y a pas d'API pour y accéder. Les compilateurs peuvent avoir des extensions ou des API privées pour y accéder, mais il peut ne s'agir que d'une extension.

La vtable existe-t-elle pour toutes les classes, ou seulement celles qui ont au moins une fonction virtuelle?

Seuls ceux qui ont au moins une fonction virtuelle (que ce soit même destructeur) ou dérivent au moins une classe qui a sa vtable ("est polymorphe").

Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

C'est une implémentation possible, mais plutôt non pratiquée. Au lieu de cela, il y a généralement une fonction qui imprime quelque chose comme "une fonction virtuelle pure appelée" et fait abort(). L'appel à cela peut se produire si vous essayez d'appeler la méthode abstraite dans le constructeur ou le destructeur.

Le fait d'avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement remplacée ou non, ou cela n'a-t-il aucun effet tant qu'elle est virtuelle.

Le ralentissement dépend uniquement du fait que l'appel soit résolu en appel direct ou en appel virtuel. Et rien d'autre n'a d'importance. :)

Si vous appelez une fonction virtuelle via un pointeur ou une référence à un objet, elle sera toujours implémentée en tant qu'appel virtuel - car le compilateur ne pourra jamais savoir quel type d'objet sera affecté à ce pointeur lors de l'exécution, et s'il s'agit d'un objet classe dans laquelle cette méthode est remplacée ou non. Dans deux cas seulement, le compilateur peut résoudre l'appel à une fonction virtuelle en tant qu'appel direct:

  • Si vous appelez la méthode via une valeur (une variable ou le résultat d'une fonction qui retourne une valeur) - dans ce cas, le compilateur n'a aucun doute sur la classe réelle de l'objet et peut la "résoudre en dur" au moment de la compilation .
  • Si la méthode virtuelle est déclarée final dans la classe vers laquelle vous avez un pointeur ou une référence par laquelle vous l'appelez (niquement en C++ 11). Dans ce cas, le compilateur sait que cette méthode ne peut plus subir de substitution et ne peut être que la méthode de cette classe.

Notez cependant que les appels virtuels ne nécessitent que le déréférencement de deux pointeurs. L'utilisation de RTTI (bien que disponible uniquement pour les classes polymorphes) est plus lente que d'appeler des méthodes virtuelles, si vous trouvez un cas pour implémenter la même chose de deux manières. Par exemple, définir virtual bool HasHoof() { return false; } puis remplacer uniquement comme bool Horse::HasHoof() { return true; } vous donnerait la possibilité d'appeler if (anim->HasHoof()) ce sera plus rapide que d'essayer if(dynamic_cast<Horse*>(anim)) . Ceci est dû au fait dynamic_cast doit parcourir la hiérarchie des classes dans certains cas même de manière récursive pour voir s'il peut être construit le chemin à partir du type de pointeur réel et du type de classe souhaité. Alors que l'appel virtuel est toujours le même - déréférencer deux pointeurs.

2
Ethouris

Cette réponse a été intégrée dans la Réponse Wiki de la communauté

  • Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

La réponse à cela est qu'elle n'est pas spécifiée - appeler la fonction virtuelle pure entraîne un comportement indéfini si elle n'est pas définie (ce qui n'est généralement pas le cas) (ISO/IEC 14882: 2003 10.4-2). Certaines implémentations placent simplement un pointeur NULL dans l'entrée vtable; d'autres implémentations placent un pointeur sur une méthode factice qui fait quelque chose de similaire à une assertion.

Notez qu'une classe abstraite peut définir une implémentation pour une fonction virtuelle pure, mais cette fonction ne peut être appelée qu'avec une syntaxe d'identifiant qualifié (c'est-à-dire, en spécifiant complètement la classe dans le nom de la méthode, comme pour appeler une méthode de classe de base à partir d'un Classe dérivée). Ceci est fait pour fournir une implémentation par défaut facile à utiliser, tout en exigeant qu'une classe dérivée fournisse un remplacement.

2
Michael Burr

Vous pouvez recréer la fonctionnalité des fonctions virtuelles en C++ à l'aide de pointeurs de fonction en tant que membres d'une classe et de fonctions statiques en tant qu'implémentations, ou en utilisant le pointeur sur les fonctions membres et les fonctions membres pour les implémentations. Il n'y a que des avantages de notation entre les deux méthodes ... en fait, les appels de fonction virtuels ne sont eux-mêmes qu'une commodité de notation. En fait, l'héritage n'est qu'une commodité de notation ... tout peut être implémenté sans utiliser les fonctionnalités du langage pour l'héritage. :)

Ce qui suit est un code non testé, probablement bogué, mais j'espère que l'idée est illustrée.

par exemple.

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};
2
jheriko

Habituellement, avec un VTable, un tableau de pointeurs vers des fonctions.

2
Lou Franco

Quelque chose qui n'est pas mentionné ici dans toutes ces réponses est qu'en cas d'héritage multiple, où les classes de base ont toutes des méthodes virtuelles. La classe héritée a plusieurs pointeurs vers un vmt. Le résultat est que la taille de chaque instance d'un tel objet est plus grande. Tout le monde sait qu'une classe avec des méthodes virtuelles a 4 octets supplémentaires pour le vmt, mais en cas d'héritage multiple, c'est pour chaque classe de base qui a des méthodes virtuelles multipliées par 4. 4 étant la taille du pointeur.

1
Philip Stuyck

Chaque objet a un pointeur vtable qui pointe vers un tableau de fonctions membres.

1
who

preuve de concept très mignonne que j'ai faite un peu plus tôt (pour voir si l'ordre d'héritage est important); faites-moi savoir si votre implémentation de C++ la rejette réellement (ma version de gcc ne donne qu'un avertissement pour assigner des structures anonymes, mais c'est un bug), je suis curieux.

CCPolite.h:

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h:

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c:

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

sortie:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

notez que je n'attribue jamais mon faux objet, il n'y a pas besoin de faire de destruction; les destructeurs sont automatiquement placés à la fin de la portée des objets alloués dynamiquement pour récupérer la mémoire du littéral objet lui-même et le pointeur vtable.

0
Dmitry

Les réponses de Burly sont correctes ici, sauf pour la question:

Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

La réponse est qu'aucune table virtuelle n'est créée du tout pour les classes abstraites. Il n'y a aucun besoin car aucun objet de ces classes ne peut être créé!

En d'autres termes, si nous avons:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

Le pointeur vtbl accessible via pB sera le vtbl de classe D. C'est exactement comme cela que le polymorphisme est implémenté. Autrement dit, comment les méthodes D sont accessibles via pB. Il n'y a pas besoin d'un vtbl pour la classe B.

En réponse au commentaire de Mike ci-dessous ...

Si la classe B dans ma description a une méthode virtuelle foo () qui n'est pas surchargée par D et une méthode virtuelle bar () qui est surchargée, alors le vtbl de D aura un pointeur vers B foo () et vers son propre bar (). Il n'y a toujours pas de vtbl créé pour B.

0
Andrew Stein