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 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 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 vtable
o base.vtable
in un'espressione digitata correttamente.
Inside the C++ Object Model
diStanley B. Lippman
. (Sezione 4.2, pagine 124-131)