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.
Avoir des fonctions virtuelles ralentit toute la classe dans la mesure où un élément de données supplémentaire doit être initialisé, copié,… lorsqu'il s'agit d'un objet d'une telle classe. Pour une classe comptant une demi-douzaine de membres environ, la différence devrait être négligeable. Pour une classe qui ne contient qu'un seul char
membre, ou aucun membre du tout, la différence peut être notable.
En dehors de cela, il est important de noter que tous les appels à une fonction virtuelle ne sont pas des appels de fonction virtuelle. Si vous avez un objet d'un type connu, le compilateur peut émettre du code pour un appel de fonction normal, et peut même incorporer ladite fonction s'il en a envie. Ce n'est que lorsque vous effectuez des appels polymorphes, via un pointeur ou une référence qui pourrait pointer vers un objet de la classe de base ou vers un objet d'une classe dérivée, que vous avez besoin de l'indirection vtable et que vous la payez en termes de performances.
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
Les étapes que le matériel doit suivre sont essentiellement les mêmes, que la fonction soit écrasée ou non. L'adresse de la vtable est lue à partir de l'objet, le pointeur de fonction extrait de l'emplacement approprié et la fonction appelée par le pointeur. En termes de performances réelles, les prédictions de branche peuvent avoir un certain impact. Ainsi, par exemple, si la plupart de vos objets font référence à la même implémentation d'une fonction virtuelle donnée, il y a une certaine chance que le prédicteur de branche prédise correctement la fonction à appeler avant même que le pointeur n'ait été récupéré. Mais peu importe la fonction qui est la plus courante: il peut s'agir de la plupart des objets déléguant au cas de base non écrasé, ou de la plupart des objets appartenant à la même sous-classe et donc déléguant au même cas écrasé.
comment sont-ils mis en œuvre à un niveau profond?
J'aime l'idée de jheriko pour démontrer cela en utilisant une implémentation fictive. Mais j'utiliserais C pour implémenter quelque chose qui ressemble au code ci-dessus, afin que le niveau bas soit plus facilement visible.
classe parent Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
classe dérivée 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
}
fonction f exécution d'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);
}
Vous pouvez donc voir qu'une vtable n'est qu'un bloc statique en mémoire, contenant principalement des pointeurs de fonction. Chaque objet d'une classe polymorphe pointera vers la vtable correspondant à son type dynamique. Cela rend également plus claire la connexion entre RTTI et les fonctions virtuelles: vous pouvez vérifier le type d'une classe simplement en regardant vers quelle vtable elle pointe. Ce qui précède est simplifié de plusieurs manières, comme par exemple l'héritage multiple, mais le concept général est solide.
Si arg
est de type Foo*
et que vous prenez arg->vtable
, mais est en fait un objet de type Bar
, vous obtenez toujours l'adresse correcte du vtable
. En effet, le vtable
est toujours le premier élément à l'adresse de l'objet, qu'il soit appelé vtable
ou base.vtable
dans une expression correctement typée.
Inside the C++ Object Model
parStanley B. Lippman
. (Section 4.2, page 124-131)