Domanda

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

È possibile modificare o addirittura accedere direttamente al vtable in fase di esecuzione?

Esiste vtable 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 una singola funzione virtuale rallenta l'intera classe?Oppure solo la chiamata alla funzione virtuale?E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, oppure ciò non ha alcun effetto finché è virtuale.

È stato utile?

Soluzione

Come vengono implementate le funzioni virtuali a livello profondo?

Da "Funzioni virtuali in C++":

Ogni volta che in un programma viene dichiarata una funzione virtuale, viene costruita una tabella v per la classe.La tabella v è costituita dagli indirizzi delle funzioni virtuali per le classi che contengono una o più funzioni virtuali.L'oggetto della classe contenente la funzione virtuale contiene un puntatore virtuale che punta all'indirizzo base della tabella virtuale in memoria.Ogni volta che c'è una chiamata di funzione virtuale, la tabella v 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 all'inizio dell'oggetto in memoria.Quindi la dimensione dell'oggetto in questo caso aumenta della dimensione del puntatore.Questo vptr contiene l'indirizzo base della tabella virtuale in memoria.Si noti che le tabelle virtuali sono specifiche della classe, ovvero esiste una sola tabella virtuale per una classe indipendentemente dal numero di funzioni virtuali che contiene.Questa tabella virtuale a sua volta contiene gli indirizzi base di una o più funzioni virtuali della classe.Nel momento in cui viene richiamata una funzione virtuale su un oggetto, il vptr di quell'oggetto fornisce l'indirizzo 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.Ecco come viene risolta l'associazione dinamica durante una chiamata di funzione virtuale.

È possibile modificare o addirittura accedere direttamente al vtable in fase di esecuzione?

Universalmente, credo che la risposta sia "no".Potresti modificare la memoria per trovare vtable ma non sapresti comunque 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.Tieni inoltre presente le specifiche del linguaggio C++ non specificare che sono richiesti vtables, tuttavia è così che la maggior parte dei compilatori implementa le funzioni virtuali.

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

IO credere la risposta qui è "dipende dall'implementazione" poiché le specifiche non richiedono 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.Esiste 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 della lingua, quindi dipende dall'implementazione.La chiamata alla funzione virtuale pura comporta un comportamento indefinito se non è definita (cosa che di solito non è) (ISO/IEC 14882:2003 10.4-2).In pratica alloca uno slot nella vtable per la funzione ma non le assegna un indirizzo.Ciò lascia la vtable incompleta, il che richiede che le classi derivate implementino la funzione e completino la vtable.Alcune implementazioni inseriscono semplicemente un puntatore NULL nella voce vtable;altre implementazioni inseriscono 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 tale funzione può essere chiamata solo con una sintassi con ID qualificato (ovvero, specificando completamente la classe nel nome del metodo, in modo simile alla chiamata di un metodo della classe base da un classe derivata).Questo viene fatto per fornire un'implementazione predefinita facile da usare, richiedendo comunque che una classe derivata fornisca un override.

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

Questo sta arrivando al limite delle mie conoscenze, quindi qualcuno per favore mi aiuti qui se sbaglio!

IO credere che solo le funzioni virtuali nella classe subiscono il calo di prestazioni in termini di tempo correlato alla chiamata di una funzione virtuale rispetto a quella virtuale.una funzione non virtuale.Lo spazio per la classe c'è in ogni caso.Tieni presente che se è presente una vtable, ce n'è solo 1 per classe, non uno per oggetto.

La velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, oppure ciò non ha alcun effetto finché è virtuale?

Non credo che il tempo di esecuzione di una funzione virtuale sovrascritta diminuisca rispetto alla chiamata della funzione virtuale di base.Tuttavia, esiste un ulteriore sovraccarico di spazio per la classe associato alla definizione di un'altra vtable per la classe derivata rispetto alla classe base.

Risorse addizionali:

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

Altri suggerimenti

  • È possibile modificare o addirittura accedere direttamente al vtable in fase di esecuzione?

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

AVVERTIMENTO:Questa tecnica non è consigliata per l'uso da parte di bambini e adulti di età inferiore a 969, o piccole creature pelose di Alpha Centauri.Gli effetti collaterali possono includere demoni che ti escono dal naso, la comparsa improvvisa di Yog-Sothoth come approvatore richiesto in tutte le successive revisioni del codice o nell'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 i contenuti vtbl sono semplicemente un array di puntatori ai membri lì (generalmente nell'ordine in cui sono stati dichiarati, con il primo della classe base).Naturalmente ci sono altri layout possibili, ma questo è 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 facciamo qualche imbroglio...

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 (monkeypatch di una classe)

Questo è un po' più complicato, dal momento che il vtbl stesso è 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

Quest'ultimo è molto probabile che faccia sì che i rilevatori di virus e il collegamento si attivino e se ne accorgano a causa delle manipolazioni di mprotect.In un processo che utilizza il bit NX potrebbe fallire.

Avere una singola funzione virtuale rallenta l'intera classe?

Oppure solo la chiamata alla funzione virtuale?E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, oppure ciò non ha alcun effetto finché è virtuale.

Avere funzioni virtuali rallenta l'intera classe nella misura in cui un ulteriore 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 circa, la differenza dovrebbe essere trascurabile.Per una classe che contiene solo un singolo char membro, 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 funzioni virtuali.Se si dispone di un oggetto di un tipo noto, il compilatore può emettere codice per una normale invocazione di funzione e può anche incorporare detta funzione se ne ha voglia.È solo quando esegui chiamate polimorfiche, tramite un puntatore o un riferimento che potrebbe puntare a un oggetto della classe base o a un oggetto di qualche classe derivata, che hai bisogno dell'indirizzamento indiretto vtable e lo paghi 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 sostanzialmente gli stessi, indipendentemente dal fatto che la funzione venga sovrascritta o meno.L'indirizzo della vtable viene letto dall'oggetto, il puntatore alla funzione recuperato dallo slot appropriato e la funzione chiamata dal puntatore.In termini di prestazioni effettive, le previsioni sui rami potrebbero avere un certo impatto.Quindi, ad esempio, se la maggior parte dei tuoi oggetti si riferisce alla stessa implementazione di una determinata 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:potrebbero 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 utilizzando un'implementazione fittizia.Ma utilizzerei 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 che una vtable è solo un blocco statico in memoria, contenente principalmente puntatori a funzioni.Ogni oggetto di una classe polimorfica punterà alla vtable corrispondente al suo tipo dinamico.Ciò rende anche più chiara la connessione tra RTTI e le funzioni virtuali:puoi verificare di che tipo è una classe semplicemente guardando a quale vtable punta.Quanto sopra è semplificato in molti modi, come ad es.ereditarietà multipla, ma il concetto generale è valido.

Se arg è di tipo Foo* e tu prendi arg->vtable, ma in realtà è un oggetto di tipo Bar, ottieni comunque l'indirizzo corretto del vtable.Questo perché il vtable è sempre il primo elemento all'indirizzo dell'oggetto, non importa se viene chiamato vtable O base.vtable in un'espressione digitata correttamente.

Di solito con una VTable, un array di puntatori a funzioni.

Questa risposta è stata incorporata nel Risposta del Wiki della comunità

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

La risposta è che non è specificata: richiamare la funzione virtuale pura comporta 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 inseriscono 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 tale funzione può essere chiamata solo con una sintassi con ID qualificato (ovvero, specificando completamente la classe nel nome del metodo, in modo simile alla chiamata di un metodo della classe base da un classe derivata).Questo viene fatto per fornire un'implementazione predefinita facile da usare, richiedendo comunque che una classe derivata fornisca un override.

È 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 di funzioni virtuali sono solo una comodità di notazione.In effetti l'ereditarietà è solo una comodità notazionale...tutto può essere implementato senza utilizzare le funzionalità del linguaggio per l'ereditarietà.:)

Quello che segue è un codice schifoso non testato, probabilmente pieno di bug, ma si spera 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; }
...
};

Cercherò di renderlo semplice :)

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

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

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

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

È possibile modificare o addirittura accedere direttamente al vtable in fase di esecuzione?

Non è un metodo standard: non esiste alcuna API per accedervi.I compilatori potrebbero avere alcune estensioni o API private per accedervi, ma potrebbe trattarsi solo di un'estensione.

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

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

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

Questa è un'implementazione possibile, 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 ciò può verificarsi se si tenta di chiamare il metodo astratto nel costruttore o nel distruttore.

Avere una singola funzione virtuale rallenta l'intera classe?Oppure solo la chiamata alla funzione virtuale?E la velocità viene influenzata se la funzione virtuale viene effettivamente sovrascritta o meno, oppure ciò non ha alcun effetto finché è 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 implementata come chiamata virtuale, perché il compilatore non potrà mai sapere che tipo di oggetto verrà assegnato a questo puntatore in runtime e se si tratta di un classe in cui questo metodo viene 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ò "risolverla" in fase di compilazione .
  • Se il metodo virtuale è dichiarato final nella classe a cui hai un puntatore o 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 nel dereferenziare due puntatori.L'uso di RTTI (sebbene disponibile solo per le classi polimorfiche) è più lento rispetto alla chiamata di metodi virtuali, nel caso in cui 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 fornirebbe la possibilità di chiamare if (anim->HasHoof()) sarà più veloce che provarci if(dynamic_cast<Horse*>(anim)).Questo è perché dynamic_cast deve percorrere la gerarchia delle classi in alcuni casi 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.

Ecco un percorribile implementazione manuale 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 membro 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
}

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

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

Le risposte di Burly sono corrette qui tranne 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 ce n'è bisogno 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 di 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 pippo() che non viene sovrascritto da D e da un metodo virtuale sbarra() che viene sovrascritto, il vtbl di D avrà un puntatore a B pippo() e al proprio sbarra().Non esiste ancora alcun vtbl creato per B.

prova di concetto molto carina che ho fatto un po 'prima (per vedere se l'ordine di ereditarietà è importante);fammi sapere se la tua implementazione di C++ lo rifiuta effettivamente (la mia versione di gcc fornisce solo un avviso per l'assegnazione di strutture anonime, ma è 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

principale.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 che poiché non assegno mai il mio oggetto falso, non è necessario eseguire alcuna distruzione;i distruttori vengono automaticamente inseriti alla fine dell'ambito degli oggetti allocati dinamicamente per recuperare la memoria dell'oggetto letterale stesso e del puntatore vtable.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top