Come vengono implementate le funzioni virtuali e vtable?


109

Sappiamo tutti cosa sono le funzioni virtuali in C ++, ma come vengono implementate a un livello profondo?

È possibile modificare o persino accedere direttamente a vtable in fase di esecuzione?

La vtable esiste per tutte le classi o solo per quelle che hanno almeno una funzione virtuale?

Le classi astratte hanno semplicemente un NULL per il puntatore a funzione di almeno una voce?

Avere un'unica funzione virtuale rallenta l'intera classe? O solo la chiamata alla funzione che è virtuale? E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, o questo non ha alcun effetto fintanto che è virtuale.


2
Suggerisci di leggere il capolavoro Inside the C++ Object Modeldi Stanley B. Lippman. (Sezione 4.2, pagine 124-131)
smwikipedia

Risposte:


123

Come vengono implementate le funzioni virtuali a livello profondo?

Da "Funzioni virtuali in C ++" :

Ogni volta che un programma ha una funzione virtuale dichiarata, av - table viene costruito per la classe. La tabella v è costituita da indirizzi alle funzioni virtuali per classi che contengono una o più funzioni virtuali. L'oggetto della classe contenente la funzione virtuale contiene un puntatore virtuale che punta all'indirizzo di base della tabella virtuale in memoria. Ogni volta che c'è una chiamata di funzione virtuale, la v-table viene utilizzata per risolvere l'indirizzo della funzione. Un oggetto della classe che contiene una o più funzioni virtuali contiene un puntatore virtuale chiamato vptr proprio all'inizio dell'oggetto nella memoria. Quindi la dimensione dell'oggetto in questo caso aumenta della dimensione del puntatore. Questo vptr contiene l'indirizzo di base della tabella virtuale in memoria. Tieni presente che le tabelle virtuali sono specifiche della classe, ovvero c'è solo una tabella virtuale per una classe indipendentemente dal numero di funzioni virtuali che contiene. Questa tabella virtuale a sua volta contiene gli indirizzi di base di una o più funzioni virtuali della classe. Nel momento in cui una funzione virtuale viene chiamata su un oggetto, il vptr di quell'oggetto fornisce l'indirizzo di base della tabella virtuale per quella classe in memoria. Questa tabella viene utilizzata per risolvere la chiamata di funzione poiché contiene gli indirizzi di tutte le funzioni virtuali di quella classe. Questo è il modo in cui l'associazione dinamica viene risolta durante una chiamata di funzione virtuale. il vptr di quell'oggetto fornisce l'indirizzo di base della tabella virtuale per quella classe in memoria. Questa tabella viene utilizzata per risolvere la chiamata di funzione poiché contiene gli indirizzi di tutte le funzioni virtuali di quella classe. Questo è il modo in cui l'associazione dinamica viene risolta durante una chiamata di funzione virtuale. il vptr di quell'oggetto fornisce l'indirizzo di base della tabella virtuale per quella classe in memoria. Questa tabella viene utilizzata per risolvere la chiamata di funzione poiché contiene gli indirizzi di tutte le funzioni virtuali di quella classe. Questo è il modo in cui l'associazione dinamica viene risolta durante una chiamata di funzione virtuale.

È possibile modificare o persino accedere direttamente a vtable in fase di esecuzione?

Universalmente, credo che la risposta sia "no". Potresti manipolare la memoria per trovare vtable ma non sapresti ancora come appare la firma della funzione per chiamarla. Tutto ciò che vorresti ottenere con questa capacità (supportata dal linguaggio) dovrebbe essere possibile senza accedere direttamente a vtable o modificarlo in fase di esecuzione. Si noti inoltre che le specifiche del linguaggio C ++ non specificano che sono richiesti vtables, tuttavia è così che la maggior parte dei compilatori implementa le funzioni virtuali.

La vtable esiste per tutti gli oggetti o solo per quelli che hanno almeno una funzione virtuale?

Credo che la risposta qui sia "dipende dall'implementazione" poiché la specifica non richiede vtables in primo luogo. Tuttavia, in pratica, credo che tutti i compilatori moderni creino una vtable solo se una classe ha almeno 1 funzione virtuale. C'è un sovraccarico di spazio associato a vtable e un sovraccarico di tempo associato alla chiamata di una funzione virtuale rispetto a una funzione non virtuale.

Le classi astratte hanno semplicemente un NULL per il puntatore a funzione di almeno una voce?

La risposta è che non è specificato dalle specifiche del linguaggio, quindi dipende dall'implementazione. Chiamare la funzione virtuale pura produce un comportamento indefinito se non è definito (cosa che di solito non è) (ISO / IEC 14882: 2003 10.4-2). In pratica alloca uno slot nella tabella v per la funzione ma non le assegna un indirizzo. Ciò lascia il vtable incompleto che richiede alle classi derivate di implementare la funzione e completare il vtable. Alcune implementazioni inseriscono semplicemente un puntatore NULL nella voce vtable; altre implementazioni posizionano un puntatore a un metodo fittizio che fa qualcosa di simile a un'asserzione.

Si noti che una classe astratta può definire un'implementazione per una funzione virtuale pura, ma quella funzione può essere chiamata solo con una sintassi di id qualificato (cioè, specificando completamente la classe nel nome del metodo, simile alla chiamata di un metodo di classe base da un classe derivata). Questo viene fatto per fornire un'implementazione predefinita facile da usare, pur richiedendo che una classe derivata fornisca un override.

Avere una singola funzione virtuale rallenta l'intera classe o solo la chiamata alla funzione che è virtuale?

Questo sta arrivando al limite della mia conoscenza, quindi qualcuno mi aiuti qui se sbaglio!

Io credo che solo le funzioni che sono virtuale nella esperienza di classe le prestazioni tempo ha colpito relative a chiamare una funzione virtuale contro una funzione non virtuale. Lo spazio in testa per la classe è presente in entrambi i casi. Nota che se c'è una tabella v, ce n'è solo 1 per classe , non uno per oggetto .

La velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, o questo non ha effetto fintanto che è virtuale?

Non credo che il tempo di esecuzione di una funzione virtuale che viene sovrascritta diminuisca rispetto alla chiamata della funzione virtuale di base. Tuttavia, c'è un sovraccarico di spazio aggiuntivo per la classe associato alla definizione di un'altra tabella v per la classe derivata rispetto alla classe base.

Risorse addizionali:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (tramite macchina di ritorno)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ CXX-abi / abi.html # vtable


2
Non sarebbe in linea con la filosofia C ++ di Stroustrup per un compilatore inserire un puntatore vtable non necessario in un oggetto che non ne ha bisogno. La regola è che non si ottiene un sovraccarico che non è in C a meno che non lo si chieda, ed è scortese che i compilatori lo violino.
Steve Jessop

3
Sono d'accordo che sarebbe sciocco per qualsiasi compilatore che si prende sul serio usare un vtable quando non esistono funzioni virtuali. Tuttavia, ho ritenuto importante sottolineare che, per quanto ne so, lo standard C ++ non / lo richiede /, quindi sii avvisato prima di dipendere da esso.
Zach Burlingame

8
Anche le funzioni virtuali possono essere chiamate non virtualmente. Questo è in effetti abbastanza comune: se l'oggetto è sullo stack, all'interno dell'ambito il compilatore conoscerà il tipo esatto e ottimizzerà la ricerca di vtable. Ciò è particolarmente vero per il dtor, che deve essere chiamato nello stesso ambito dello stack.
MSalters

1
Credo che quando una classe che ha almeno una funzione virtuale, ogni oggetto abbia una vtable e non una per l'intera classe.
Asaf R

3
Implementazione comune: ogni oggetto ha un puntatore a una tabella v; la classe possiede il tavolo. La magia di costruzione consiste semplicemente nell'aggiornamento del puntatore vtable nel ctor derivato, dopo che il ctor di base è terminato.
MSalters

31
  • È possibile modificare o persino accedere direttamente a vtable in fase di esecuzione?

Non portabile, ma se non ti dispiace trucchi sporchi, certo!

ATTENZIONE : questa tecnica non è consigliata per l'uso da parte di bambini, adulti di età inferiore ai 969 anni o piccole creature pelose di Alpha Centauri. Gli effetti collaterali possono includere demoni che volano via dal tuo naso , la comparsa improvvisa di Yog-Sothoth come approvatore richiesto in tutte le successive revisioni del codice o l'aggiunta retroattiva di IHuman::PlayPiano()a tutte le istanze esistenti]

Nella maggior parte dei compilatori che ho visto, vtbl * sono i primi 4 byte dell'oggetto e il contenuto di vtbl è semplicemente un array di puntatori ai membri (generalmente nell'ordine in cui sono stati dichiarati, con il primo della classe base). Ovviamente ci sono altri possibili layout, ma è quello che ho generalmente osservato.

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;

Ora per tirare fuori alcuni imbrogli ...

Modifica della classe in fase di esecuzione:

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

Sostituzione di un metodo per tutte le istanze (Monkeypatching di una classe)

Questo è un po 'più complicato, poiché lo stesso vtbl è probabilmente nella memoria di sola lettura.

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

È piuttosto probabile che quest'ultimo renda i virus-checker e il collegamento si riattivi e prenda nota, a causa delle manipolazioni di mprotect. In un processo che utilizza il bit NX potrebbe non riuscire.


6
Hmm. È inquietante che questo abbia ricevuto una taglia. Spero che questo non significhi che @Mobilewits pensi che simili imbrogli siano in realtà una buona idea ...
puetzk

1
Si prega di considerare di scoraggiare l'uso di questa tecnica, in modo chiaro e forte, piuttosto che "ammiccare".
einpoklum

" I contenuti vtbl sono semplicemente un array di puntatori di membri " in realtà è un record (uno struct) con voci diverse, che sono equamente spaziate
curioso

1
Puoi guardarlo in entrambi i modi; i puntatori a funzione hanno firme diverse e quindi diversi tipi di puntatori; in questo senso, è davvero simile a una struttura. Ma in altri contesti, l'idea dell'indice vtbl è utile (ad esempio ActiveX lo usa nel modo in cui descrive le doppie interfacce nelle librerie dei tipi), che è una vista più simile a un array.
puetzk

17

Avere un'unica funzione virtuale rallenta l'intera classe?

O solo la chiamata alla funzione che è virtuale? E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, o questo non ha alcun effetto fintanto che è virtuale.

Avere funzioni virtuali rallenta l'intera classe nella misura in cui un altro elemento di dati deve essere inizializzato, copiato, ... quando si ha a che fare con un oggetto di tale classe. Per una classe con una mezza dozzina di membri o giù di lì, la differenza dovrebbe essere trascurabile. Per una classe che contiene solo un singolo charmembro o nessun membro, la differenza potrebbe essere notevole.

A parte questo, è importante notare che non tutte le chiamate a una funzione virtuale sono chiamate a una funzione virtuale. Se si dispone di un oggetto di un tipo noto, il compilatore può emettere codice per una normale chiamata di funzione e può anche incorporare tale funzione se ne ha voglia. È solo quando si eseguono chiamate polimorfiche, tramite un puntatore o un riferimento che potrebbe puntare a un oggetto della classe base o a un oggetto di una classe derivata, che è necessario l'indirizzamento di vtable e pagarlo in termini di prestazioni.

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
}

I passaggi che l'hardware deve eseguire sono essenzialmente gli stessi, indipendentemente dal fatto che la funzione venga sovrascritta o meno. L'indirizzo della tabella v viene letto dall'oggetto, il puntatore alla funzione viene recuperato dallo slot appropriato e la funzione chiamata dal puntatore. In termini di prestazioni effettive, le previsioni di filiale potrebbero avere un certo impatto. Quindi, ad esempio, se la maggior parte dei tuoi oggetti fa riferimento alla stessa implementazione di una data funzione virtuale, allora c'è qualche possibilità che il predittore di ramo preveda correttamente quale funzione chiamare anche prima che il puntatore sia stato recuperato. Ma non importa quale sia la funzione comune: potrebbe essere la maggior parte degli oggetti che delegano al caso base non sovrascritto, o la maggior parte degli oggetti appartenenti alla stessa sottoclasse e quindi delegati allo stesso caso sovrascritto.

come vengono implementati a livello profondo?

Mi piace l'idea di jheriko di dimostrarlo usando un'implementazione fittizia. Ma userei C per implementare qualcosa di simile al codice sopra, in modo che il livello basso sia più facilmente visibile.

classe genitore 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!
}

classe derivata Bar

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
}

funzione f che esegue la chiamata di funzione virtuale

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);
}

Quindi puoi vedere, un vtable è solo un blocco statico in memoria, che contiene principalmente puntatori a funzione. Ogni oggetto di una classe polimorfica punterà alla vtable corrispondente al suo tipo dinamico. Questo rende anche più chiara la connessione tra RTTI e le funzioni virtuali: puoi controllare che tipo è una classe semplicemente guardando a quale vtable punta. Quanto sopra è semplificato in molti modi, come ad esempio l'ereditarietà multipla, ma il concetto generale è valido.

Se argè di tipo Foo*e prendi arg->vtable, ma in realtà è un oggetto di tipo Bar, ottieni comunque l'indirizzo corretto del file vtable. Questo perché vtableè sempre il primo elemento all'indirizzo dell'oggetto, indipendentemente dal fatto che sia chiamato vtableo base.vtablein un'espressione digitata correttamente.


"Ogni oggetto di una classe polimorfica punterà alla propria vtable." Stai dicendo che ogni oggetto ha la sua vtable? AFAIK vtable è condiviso tra tutti gli oggetti della stessa classe. Fammi sapere se sbaglio.
Bhuwan

1
@ Bhuwan: No, hai ragione: c'è solo una tabella vtable per tipo (che potrebbe essere per istanza di modello in caso di modelli). Intendevo dire che ogni oggetto di una classe polimorfica con punto alla tabella v che si applica ad esso, quindi ogni oggetto ha un tale puntatore, ma per oggetti dello stesso tipo punterà alla stessa tabella. Probabilmente dovrei riformulare questo.
MvG

1
@MvG " oggetti dello stesso tipo punterà alla stessa tabella " non durante la costruzione di classi base con classi base virtuali! (un caso molto speciale)
Curiousguy

1
@curiousguy: Lo archiverei in "quanto sopra è semplificato in molti modi", in particolare perché l'applicazione principale delle basi virtuali è l'ereditarietà multipla, che non ho modellato neanche io. Ma grazie per il commento, è utile averlo qui per le persone che potrebbero aver bisogno di maggiore profondità.
MvG

3

Di solito con una tabella V, una matrice di puntatori a funzioni.


2

Questa risposta è stata incorporata nella risposta Wiki della comunità

  • Le classi astratte hanno semplicemente un NULL per il puntatore a funzione di almeno una voce?

La risposta è che non è specificato: la chiamata alla funzione virtuale pura produce un comportamento indefinito se non è definita (cosa che di solito non è) (ISO / IEC 14882: 2003 10.4-2). Alcune implementazioni inseriscono semplicemente un puntatore NULL nella voce vtable; altre implementazioni posizionano un puntatore a un metodo fittizio che fa qualcosa di simile a un'asserzione.

Si noti che una classe astratta può definire un'implementazione per una funzione virtuale pura, ma quella funzione può essere chiamata solo con una sintassi di id qualificato (cioè, specificando completamente la classe nel nome del metodo, simile alla chiamata di un metodo di classe base da un classe derivata). Questo viene fatto per fornire un'implementazione predefinita facile da usare, pur richiedendo che una classe derivata fornisca un override.


Inoltre, non penso che una classe astratta possa definire un'implementazione per una funzione virtuale pura. Per definizione, una funzione virtuale pura non ha corpo (es. Bool my_func () = 0;). È tuttavia possibile fornire implementazioni per normali funzioni virtuali.
Zach Burlingame

Una funzione virtuale pura può avere una definizione. Vedi "Effective C ++, 3rd Ed" Item # 34 di Scott Meyers, ISO 14882-2003 10.4-2, o bytes.com/forum/thread572745.html
Michael Burr

2

È possibile ricreare la funzionalità delle funzioni virtuali in C ++ utilizzando puntatori a funzione come membri di una classe e funzioni statiche come implementazioni oppure utilizzando puntatori a funzioni membro e funzioni membro per le implementazioni. Ci sono solo vantaggi notazionali tra i due metodi ... infatti le chiamate a funzioni virtuali sono solo una comodità di annotazione. In effetti l'ereditarietà è solo una comodità notazionale ... tutto può essere implementato senza utilizzare le caratteristiche del linguaggio per l'ereditarietà. :)

Quanto segue è un codice non testato, probabilmente difettoso, ma si spera che dimostri l'idea.

per esempio

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; }
...
};

void(*)(Foo*) MyFunc;è questa una sintassi Java?
Curiousguy

no, la sua sintassi C / C ++ per i puntatori a funzione. Per citare me stesso "Puoi ricreare la funzionalità delle funzioni virtuali in C ++ usando i puntatori a funzione". è un po 'sgradevole di sintassi, ma qualcosa da conoscere se ti consideri un programmatore C.
jheriko

il puntatore alla funzione ac sarebbe più simile a: int ( PROC) (); e un puntatore a una funzione membro di una classe sarebbe simile a: int (ClassName :: MPROC) ();
Minaccia

1
@menace, hai dimenticato un po 'di sintassi lì ... stai pensando al typedef forse? typedef int (* PROC) (); quindi puoi semplicemente eseguire PROC foo più tardi invece di int (* foo) ()?
jheriko

2

Cercherò di renderlo semplice :)

Sappiamo tutti cosa sono le funzioni virtuali in C ++, ma come vengono implementate a un livello profondo?

Questo è un array con puntatori a funzioni, che sono implementazioni di una particolare funzione virtuale. Un indice in questo array rappresenta un particolare indice di una funzione virtuale definita per una classe. Ciò include funzioni virtuali pure.

Quando una classe polimorfica deriva da un'altra classe polimorfica, potremmo avere le seguenti situazioni:

  • La classe derivante non aggiunge nuove funzioni virtuali né sovrascrive alcuna. In questo caso, questa classe condivide la vtable con la classe base.
  • La classe derivante aggiunge e sovrascrive i metodi virtuali. In questo caso ottiene la propria vtable, dove le funzioni virtuali aggiunte hanno un indice che inizia dopo l'ultima derivata.
  • Più classi polimorfiche nell'ereditarietà. In questo caso abbiamo uno spostamento dell'indice tra la seconda base e la successiva e l'indice di esso nella classe derivata

È possibile modificare o persino accedere direttamente a vtable in fase di esecuzione?

Non è un modo standard: non ci sono API per accedervi. I compilatori possono avere alcune estensioni o API private per accedervi, ma potrebbe essere solo un'estensione.

La vtable esiste per tutte le classi o solo per quelle che hanno almeno una funzione virtuale?

Solo quelli che hanno almeno una funzione virtuale (anche distruttore) o derivano almeno una classe che ha la sua vtable ("è polimorfico").

Le classi astratte hanno semplicemente un NULL per il puntatore a funzione di almeno una voce?

Questa è una possibile implementazione, ma piuttosto non praticata. Invece di solito c'è una funzione che stampa qualcosa come "pura funzione virtuale chiamata" e lo fa abort(). La chiamata a quello può verificarsi se si tenta di chiamare il metodo astratto nel costruttore o nel distruttore.

Avere un'unica funzione virtuale rallenta l'intera classe? O solo la chiamata alla funzione che è virtuale? E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, o questo non ha alcun effetto fintanto che è virtuale.

Il rallentamento dipende solo dal fatto che la chiamata venga risolta come chiamata diretta o come chiamata virtuale. E nient'altro importa. :)

Se chiami una funzione virtuale tramite un puntatore o un riferimento a un oggetto, verrà sempre implementato come chiamata virtuale, poiché il compilatore non può mai sapere quale tipo di oggetto verrà assegnato a questo puntatore in runtime e se si tratta di un classe in cui questo metodo è sovrascritto o meno. Solo in due casi il compilatore può risolvere la chiamata a una funzione virtuale come chiamata diretta:

  • Se chiami il metodo tramite un valore (una variabile o il risultato di una funzione che restituisce un valore) - in questo caso il compilatore non ha dubbi su quale sia la classe effettiva dell'oggetto e può "risolverlo" in fase di compilazione .
  • Se il metodo virtuale è dichiarato finalnella classe a cui hai un puntatore o un riferimento attraverso il quale lo chiami ( solo in C ++ 11 ). In questo caso il compilatore sa che questo metodo non può subire ulteriori sovrascritture e può essere solo il metodo di questa classe.

Si noti tuttavia che le chiamate virtuali hanno solo un sovraccarico per la dereferenziazione di due puntatori. L'uso di RTTI (sebbene disponibile solo per le classi polimorfiche) è più lento rispetto alla chiamata di metodi virtuali, se dovessi trovare un caso per implementare la stessa cosa in due modi simili. Ad esempio, definire virtual bool HasHoof() { return false; }e quindi sovrascrivere solo come bool Horse::HasHoof() { return true; }ti fornirà la possibilità di chiamare if (anim->HasHoof())che sarà più veloce del tentativo if(dynamic_cast<Horse*>(anim)). Questo perché dynamic_castin alcuni casi deve attraversare la gerarchia delle classi anche in modo ricorsivo per vedere se è possibile costruire il percorso dal tipo di puntatore effettivo e dal tipo di classe desiderato. Mentre la chiamata virtuale è sempre la stessa - dereferenziare due puntatori.


2

Ecco un'implementazione manuale eseguibile della tabella virtuale nel moderno C ++. Ha una semantica ben definita, nessun hack e no void*.

Nota: .*e ->*sono operatori diversi da *e ->. I puntatori alle funzioni dei membri funzionano in modo diverso.

#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
}

1

Ogni oggetto ha un puntatore vtable che punta a un array di funzioni membro.


1

Qualcosa non menzionato qui in tutte queste risposte è che in caso di ereditarietà multipla, in cui le classi base hanno tutte metodi virtuali. La classe che eredita ha più puntatori a un vmt. Il risultato è che la dimensione di ogni istanza di un tale oggetto è maggiore. Tutti sanno che una classe con metodi virtuali ha 4 byte extra per vmt, ma in caso di ereditarietà multipla è per ogni classe base che ha metodi virtuali per 4. 4 è la dimensione del puntatore.


0

Le risposte di Burly qui sono corrette tranne che per la domanda:

Le classi astratte hanno semplicemente un NULL per il puntatore a funzione di almeno una voce?

La risposta è che non viene creata alcuna tabella virtuale per le classi astratte. Non è necessario poiché non è possibile creare oggetti di queste classi!

In altre parole se abbiamo:

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

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

Il puntatore vtbl a cui si accede tramite pB sarà il vtbl della classe D. Questo è esattamente il modo in cui viene implementato il polimorfismo. Cioè, come si accede ai metodi D tramite pB. Non è necessario un vtbl per la classe B.

In risposta al commento di Mike qui sotto ...

Se la classe B nella mia descrizione ha un metodo virtuale foo () che non è sovrascritto da D e una barra del metodo virtuale () che è sovrascritta, allora il vtbl di D avrà un puntatore a foo () di B e alla sua barra () . Non c'è ancora nessun vtbl creato per B.


Ciò non è corretto per 2 ragioni: 1) una classe astratta può avere metodi virtuali regolari oltre a metodi virtuali puri e 2) metodi virtuali puri possono facoltativamente avere una definizione che può essere chiamata con un nome completo.
Michael Burr

Giusto - a pensarci bene, immagino che se tutti i metodi virtuali fossero puri virtuali il compilatore potrebbe ottimizzare vtable (sarebbe necessario aiuto per formare il linker per assicurarsi che non ci fossero anche definizioni).
Michael Burr,

1
" La risposta è che non viene creata alcuna tabella virtuale per le classi astratte. " " Non è necessario poiché non è possibile creare oggetti di queste classi! "
curioso

Posso seguire la tua logica per cui non B dovrebbe essere necessario alcun vtable . Solo perché alcuni dei suoi metodi hanno implementazioni (predefinite) non significa che debbano essere archiviati in una tabella v. Ma ho appena eseguito il tuo codice (modulo alcune correzioni per farlo compilare) gcc -Sseguito da c++filte c'è chiaramente una tabella v per Binclusa. Immagino che ciò possa essere dovuto al fatto che vtable memorizza anche dati RTTI come nomi di classi ed ereditarietà. Potrebbe essere richiesto per un file dynamic_cast<B*>. Nemmeno -fno-rttiil vtable va via. Con clang -O3invece di gccè improvvisamente andato.
MvG

@MvG " Solo perché alcuni dei suoi metodi hanno implementazioni (predefinite) non significa che debbano essere archiviati in una tabella v " Sì, significa proprio questo.
Curiousguy

0

prova di concetto molto carina che ho fatto un po 'prima (per vedere se l'ordine di eredità è importante); fammi sapere se la tua implementazione di C ++ effettivamente lo rifiuta (la mia versione di gcc dà solo un avviso per l'assegnazione di strutture anonime, ma questo è un bug), sono curioso.

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;
}

produzione:

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!!!!

nota poiché non sto mai allocando il mio oggetto falso, non è necessario eseguire alcuna distruzione; i distruttori vengono automaticamente posti alla fine dell'ambito degli oggetti allocati dinamicamente per recuperare la memoria dell'oggetto letterale stesso e del puntatore vtable.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.