Quel est le coût d'utilisation de destructeurs virtuels si je l'utilise même si cela n'est pas nécessaire?
Le coût d'introduction d' une fonction virtuelle dans une classe (héritée ou faisant partie de la définition de classe) est un coût initial potentiellement très élevé (ou dépendant de l'objet) d'un pointeur virtuel stocké par objet, comme suit:
struct Integer
{
virtual ~Integer() {}
int value;
};
Dans ce cas, le coût en mémoire est relativement énorme. La taille réelle de la mémoire d'une instance de classe ressemblera souvent à ceci sur les architectures 64 bits:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
Le total est de 16 octets pour cette Integer
classe, par opposition à 4 octets seulement. Si nous en stockons un million dans un tableau, la mémoire utilisée est de 16 mégaoctets: deux fois la taille du cache de processeur L3 typique de 8 Mo et une itération répétée dans un tel tableau peuvent être plusieurs fois plus lentes que l'équivalent de 4 mégaoctets. sans le pointeur virtuel en raison d’erreurs de cache supplémentaires et de défauts de page.
Ce coût de pointeur virtuel par objet n'augmente toutefois pas avec davantage de fonctions virtuelles. Vous pouvez avoir 100 fonctions de membre virtuel dans une classe et le temps système par instance serait toujours un pointeur virtuel unique.
Le pointeur virtuel est généralement la préoccupation la plus immédiate du point de vue de la surcharge. Cependant, en plus d'un pointeur virtuel par instance, il existe un coût par classe. Chaque classe avec des fonctions virtuelles génère une vtable
mémoire en mémoire qui stocke les adresses des fonctions qu’elle doit appeler (répartition virtuelle / dynamique) lors de l’appel d’une fonction virtuelle. La vptr
par instance stockée pointe ensuite vers cette classe spécifique vtable
. Cette surcharge est généralement moins préoccupante, mais elle peut gonfler votre taille binaire et ajouter un peu de coût d’exécution si cette surcharge était payée inutilement pour un millier de classes dans une base de code complexe, par exemple. Cet vtable
aspect du coût augmente en fait plus de fonctions virtuelles dans le mix.
Les développeurs Java travaillant dans des domaines critiques en termes de performances comprennent très bien ce type de surcharge (bien que souvent décrit dans le contexte de la boxe), puisqu'un type Java défini par l'utilisateur hérite implicitement d'une object
classe de base centrale et que toutes les fonctions en Java sont implicitement virtuelles (remplaçables). ) en nature, sauf indication contraire. De ce fait, un Java a Integer
également tendance à nécessiter 16 octets de mémoire sur les plates-formes 64 bits du fait de ces vptr
métadonnées de style de type associé par instance, et il est généralement impossible en Java d’emballer quelque chose comme un simple int
dans une classe sans payer une exécution. coût de performance pour cela.
Alors la question est: Pourquoi c ++ ne définit-il pas tous les destructeurs virtuels par défaut?
Le C ++ favorise vraiment les performances avec un état d'esprit "pay-as-you-go" ainsi que de nombreuses conceptions basées sur du matériel "nu-metal" héritées de C. Il ne veut pas inclure inutilement les frais généraux requis pour la génération de vtable et la répartition dynamique chaque classe / instance impliquée. Si les performances ne sont pas l’une des principales raisons pour lesquelles vous utilisez un langage tel que C ++, vous pourriez bénéficier davantage des autres langages de programmation existants, car une grande partie du langage C ++ est moins sûre et plus difficile qu’elle pourrait être idéalement avec des performances souvent la raison principale pour favoriser un tel design.
Quand n'ai-je PAS besoin d'utiliser de destructeurs virtuels?
Assez souvent. Si une classe n'est pas conçue pour être héritée, elle n'a pas besoin d'un destructeur virtuel et finira par payer uniquement une surcharge importante pour quelque chose dont elle n'a pas besoin. De même, même si une classe est conçue pour être héritée mais que vous ne supprimez jamais d'instances de sous-type via un pointeur de base, elle ne nécessite pas non plus de destructeur virtuel. Dans ce cas, une pratique sûre consiste à définir un destructeur non virtuel protégé, comme suit:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
Dans quel cas je ne devrais PAS utiliser de destructeurs virtuels?
Il est effectivement plus facile de couverture lorsque vous devez utiliser Destructeurs virtuels. Bien souvent, plus de classes dans votre base de code ne seront pas conçues pour l'héritage.
std::vector
, par exemple, n’est pas conçu pour être hérité et ne doit généralement pas l'être (conception très fragile), car cela risque alors de provoquer ce problème de suppression du pointeur de base ( std::vector
évite délibérément un destructeur virtuel) en plus des problèmes de découpage d’objet maladroits si votre La classe dérivée ajoute tout nouvel état.
En général, une classe héritée doit avoir un destructeur virtuel public ou un destructeur virtuel non protégé. À partir du C++ Coding Standards
chapitre 50:
50. Rendez les destructeurs de classe de base publics et virtuels, ou protégés et non virtuels. Supprimer ou ne pas supprimer; Telle est la question: si la suppression via un pointeur vers une base doit être autorisée, le destructeur de la base doit être public et virtuel. Sinon, il devrait être protégé et non virtuel.
Une des choses que C ++ a tendance à souligner implicitement (car les conceptions ont tendance à devenir vraiment fragiles et maladroites et peut-être même dangereuses, sinon) est l'idée que l'héritage n'est pas un mécanisme conçu pour être utilisé après coup. Il s’agit d’un mécanisme d’extensibilité avec à l’esprit le polymorphisme, mais qui nécessite de la prévoyance quant à l’extensibilité requise. Par conséquent, vos classes de base doivent être conçues en tant que racines d’une hiérarchie d’héritage, et non de quelque chose dont vous hériterez ultérieurement, après coup, sans aucune prévision de ce type.
Dans les cas où vous souhaitez simplement hériter pour réutiliser le code existant, la composition est souvent fortement encouragée (principe de réutilisation composite).