Question

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

Peut-on modifier la vtable ou même y accéder directement au moment de l'exécution?

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

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

Avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction qui est virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement écrasée ou non, ou n’a-t-elle aucun effet tant qu’elle est virtuelle?

Était-ce utile?

La solution

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 est composée d'adresses vers les fonctions virtuelles pour les classes contenant 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.

Peut-on modifier la vtable ou même y accéder directement au moment de l'exécution?

Universellement, je pense que la réponse est & "non &"; Vous pouvez faire un peu de mémoire pour trouver la vtable mais vous ne savez toujours pas à quoi ressemble la signature de la fonction. Tout ce que vous souhaitez réaliser avec cette capacité (prise en charge par la langue) doit être possible sans accès direct à vtable ni modification à l’exécution. Notez également que la spécification du langage C ++ ne spécifie pas que des tables vtables sont requises. Cependant, c'est la façon dont la plupart des compilateurs implémentent des fonctions virtuelles.

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

Je crois que la réponse est & «Cela dépend de la mise en œuvre &»; puisque la spécification ne nécessite pas 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 une fonction virtuelle. Il existe une surcharge d'espace associée à vtable 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 que cela n’est pas spécifié par la spécification du langage, donc cela dépend de la mise en oeuvre. 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 vtable pour la fonction, mais ne lui attribue pas d'adresse. Cela laisse la vtable incomplète, ce qui oblige les classes dérivées à implémenter la fonction et à compléter 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 qualifiée-id (c'est-à-dire, spécifiant complètement la classe dans le nom de la méthode, similaire à l'appel d'une classe de base. méthode à partir d'une classe dérivée). Ceci est fait pour fournir une implémentation par défaut facile à utiliser, tout en nécessitantà une classe dérivée fournir un remplacement.

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

Cela va à la limite de mes connaissances, alors quelqu'un s'il vous plaît aidez-moi ici si je me trompe!

Je crois que seules les fonctions virtuelles de la classe subissent l'impact des performances temporelles liées à l'appel d'une fonction virtuelle par rapport à une fonction non virtuelle. L'espace en surnombre pour la classe existe de toute façon. Notez que s'il existe une table virtuelle, il n'y a qu'une seule par classe , pas une par objet .

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

Je ne crois pas que le temps d'exécution d'une fonction virtuelle remplacé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://fr.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/ abi.html # vtable

Autres conseils

  • Peut-on modifier la vtable ou même y accéder directement au moment de l'exécution?

Pas de manière portable, mais si ça ne vous dérange pas, c'est sûr!

  

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

Dans la plupart des compilateurs que j'ai vus, vtbl * est les 4 premiers octets de l'objet et le contenu de vtbl est simplement un tableau de pointeurs membres (généralement dans l'ordre dans lequel ils ont été déclarés, avec le premier de la classe de base) . Il existe bien entendu d'autres configurations 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, tirez 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 d'une classe)

Celui-ci est un peu plus compliqué, car le vtbl lui-même est probablement en mémoire en lecture seule.

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 réveiller et de prendre en compte les antivirus et le lien, en raison des manipulations mprotect. Dans un processus utilisant le bit NX, il peut échouer.

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

  

Ou seulement l'appel de la fonction qui est virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement écrasée ou non, ou n’a-t-elle aucun effet tant qu’elle est virtuelle?

La présence de fonctions virtuelles ralentit toute la classe dans la mesure où un élément de données supplémentaire doit être initialisé, copié, & # 8230; lorsqu'il s'agit d'un objet d'une telle classe. Pour une classe comptant une demi-douzaine de membres environ, la différence devrait être négligeable. Pour une classe ne contenant qu'un seul char membre, ou aucun membre, la différence peut être notable.

À part cela, il est important de noter que tous les appels à une fonction virtuelle ne sont pas des appels de fonctions virtuelles. 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 insérer cette fonction en ligne s'il le souhaite. Ce n'est que lorsque vous effectuez des appels polymorphes, via un pointeur ou une référence pouvant pointer sur un objet de la classe de base ou sur 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 à suivre par le matériel sont essentiellement les mêmes, que la fonction soit écrasée ou non. L'adresse de la vtable est lue à partir de l'objet, le pointeur de fonction extrait de l'emplacement approprié et la fonction appelée par pointeur. En termes de performances réelles, les prévisions de branche pourraient avoir un impact. Ainsi, par exemple, si la plupart de vos objets se réfèrent à la même implémentation d'une fonction virtuelle donnée, le prédicteur de branche risque alors de prédire correctement quelle fonction appeler avant même que le pointeur ait été récupéré. Mais peu importe la fonction commune: il peut s’agir de la plupart des objets qui délèguent au cas de base non écrasé, ou de la plupart des objets appartenant à la même sous-classe et, partant, qui délèguent au même cas écrasé.

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

J'aime l'idée de jheriko pour démontrer cela à l'aide d'une implémentation factice. Mais j’utiliserais C pour implémenter quelque chose qui ressemble au code ci-dessus, pour que le niveau bas soit plus facilement visible.

classe parente 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 constater, une vtable n’est qu’un bloc statique en mémoire, contenant principalement des pointeurs de fonction. Chaque objet d'une classe polymorphe désignera la vtable correspondant à son type dynamique. Cela permet également de clarifier la connexion entre RTTI et les fonctions virtuelles: vous pouvez vérifier le type d’une classe en regardant simplement la table vtable à laquelle 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 bon.

Si arg est du type Foo* et que vous prenez arg->vtable, mais qu’il s’agit en fait d’un objet de type Bar, vous obtenez toujours l’adresse correcte du vtable. En effet, le base.vtable est toujours le premier élément de l'adresse de l'objet, qu'il soit appelé <=> ou <=> dans une expression correctement typée.

Généralement, avec une VTable, un tableau de pointeurs vers des fonctions.

Cette réponse a été intégrée dans les Réponse du wiki de la communauté

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

La réponse à cela est qu'il est indéterminé - appeler la fonction virtuelle pure entraîne un comportement indéfini s'il n'est pas défini (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 qualifiée-id (c'est-à-dire, spécifiant complètement la classe dans le nom de la méthode, similaire à l'appel d'une classe de base. méthode à partir d'une 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.

Vous pouvez recréer la fonctionnalité des fonctions virtuelles en C ++ en utilisant les pointeurs de fonction en tant que membres d'une classe et les fonctions statiques en tant qu'implémentations, ou en utilisant un 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 fonctions virtuelles 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 de la merde non testée, probablement du code bogué, mais qui, espérons-le, illustre l'idé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; }
...
};

Je vais essayer de rendre les choses simples:)

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

Ceci est 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 les fonctions virtuelles pures.

Lorsqu'une classe polymorphe dérive d'une autre classe polymorphe, les situations suivantes peuvent apparaître:

  • La classe dérivée n'ajoute pas de nouvelles fonctions virtuelles ni n'en remplace aucune. Dans ce cas, cette classe partage la vtable 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 vtable, où les fonctions virtuelles ajoutées ont un index commençant après la dernière fonction 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 suivante base et son index dans la classe dérivée

Peut-on modifier la vtable ou même y accéder directement au moment de l'exécution?

Pas 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 cela ne peut être qu'une extension.

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

Seuls ceux qui ont au moins une fonction virtuelle (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 & "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.

Avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction qui est virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement écrasée ou non, ou est-ce que cela n’a 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. En effet, le compilateur ne peut jamais savoir quel type d'objet sera affecté à ce pointeur au moment de l'exécution, et si appartient à une classe dans laquelle cette méthode est remplacée ou non. Dans deux cas seulement, le compilateur peut résoudre l'appel d'une fonction virtuelle en appel direct:

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

Notez cependant que les appels virtuels ont uniquement pour effet de déréférencer deux pointeurs. L'utilisation de RTTI (bien que disponible uniquement pour les classes polymorphes) est plus lente que l'appel de 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()) plus rapidement que d'essayer if(dynamic_cast<Horse*>(anim)). Cela est dû au fait que dynamic_cast doit parcourir la hiérarchie des classes dans certains cas, même de manière récursive, pour voir s’il est possible de construire le chemin à partir du type de pointeur actuel et du type de classe souhaité. Bien que l’appel virtuel soit toujours le même - déréférenciez deux pointeurs.

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 des fonctions membres 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
}

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

Ce qui n’a pas été mentionné ici dans toutes ces réponses, c’est que dans le cas d’un héritage multiple, où les classes de base ont toutes des méthodes virtuelles. La classe qui hérite a plusieurs pointeurs sur 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 la vmt, mais en cas d'héritage multiple, c'est pour chaque classe de base qui a des méthodes virtuelles fois 4. 4 étant la taille du pointeur.

Les réponses de Burly sont correctes à l'exception de la question suivante:

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

La réponse est qu’aucune table virtuelle n’est créée pour les classes abstraites. Il n'y a aucun besoin puisque 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 auquel on accède via pB sera le vtbl de la classe D. C'est exactement comment le polymorphisme est implémenté. C'est-à-dire comment les méthodes D sont accessibles via pB. Il n’est pas nécessaire de créer 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 remplacée par D et une méthode virtuelle bar () qui est remplacée, alors D vtbl aura un pointeur sur foo () de B et sur sa propre barre () . Il n'y a toujours pas de vtbl créé pour B.

preuve de concept très mignonne que j'ai faite un peu plus tôt (pour voir si l'ordre de succession 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 l'attribution de structures anonymes, mais c'est un bogue), 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 comme je n'attribue jamais mon faux objet, il n'est pas nécessaire 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 d'objet lui-même et du pointeur vtable.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top