Je croyais avoir recherché à plusieurs reprises au sujet des destructeurs virtuels, la plupart mentionnant le but des destructeurs virtuels et la raison pour laquelle vous avez besoin de destructeurs virtuels. De plus, je pense que dans la plupart des cas, les destructeurs doivent être virtuels.
Alors la question est: Pourquoi c ++ ne définit-il pas tous les destructeurs virtuels par défaut? ou dans d'autres questions:
Quand n'ai-je PAS besoin d'utiliser de destructeurs virtuels?
Dans quel cas je ne devrais PAS utiliser de destructeurs virtuels?
Quel est le coût d'utilisation de destructeurs virtuels si je l'utilise même si cela n'est pas nécessaire?
Réponses:
Si vous ajoutez un destructeur virtuel à une classe:
Dans la plupart des (toutes?) implémentations C ++ actuelles, chaque instance d'objet de cette classe doit stocker un pointeur sur la table de répartition virtuelle pour le type d'exécution, et cette table de répartition virtuelle elle-même est ajoutée à l'image exécutable.
l'adresse de la table de répartition virtuelle n'est pas nécessairement valide pour tous les processus, ce qui peut empêcher le partage sécurisé de tels objets dans la mémoire partagée
avoir un pointeur virtuel intégré frustrant de créer une classe dont la disposition de la mémoire correspond à un format d'entrée ou de sortie connu (par exemple, une
Price_Tick*
cible peut être dirigée directement vers la mémoire alignée de manière appropriée dans un paquet UDP entrant et utilisée pour analyser / accéder aux données, ou modifier les données, ou placernew
une telle classe pour écrire des données dans un paquet sortant)sous certaines conditions, les appels de destructeurs doivent parfois être envoyés virtuellement et donc hors ligne, tandis que les destructeurs non virtuels peuvent être en ligne ou optimisés si cela est trivial ou sans rapport avec l'appelant.
L'argument "pas conçu pour être hérité de" ne serait pas une raison pratique de ne pas toujours avoir un destructeur virtuel s'il n'était pas aussi pire de manière pratique, comme expliqué ci-dessus; mais étant donné que c'est pire, c'est un critère majeur pour savoir quand payer le coût: utiliser un destructeur virtuel par défaut si votre classe doit être utilisée comme classe de base . Ce n'est pas toujours nécessaire, mais cela garantit que les classes de la hiérarchie peuvent être utilisées plus librement sans comportement indéfini accidentel si un destructeur de classe dérivée est appelé à l'aide d'un pointeur ou d'une référence de classe de base.
Pas si… beaucoup de classes n'ont pas un tel besoin. Il y a tellement d'exemples où il est inutile de les énumérer, mais il suffit de regarder dans votre bibliothèque standard ou de dire boost et vous verrez qu'il y a une grande majorité de classes qui n'ont pas de destructeurs virtuels. Dans le boost 1.53, je compte 72 destructeurs virtuels sur 494.
la source
BTW,
Pour une classe de base avec suppression polymorphe.
la source
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:
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:
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. Lavptr
par instance stockée pointe ensuite vers cette classe spécifiquevtable
. 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. Cetvtable
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 aInteger
également tendance à nécessiter 16 octets de mémoire sur les plates-formes 64 bits du fait de cesvptr
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 simpleint
dans une classe sans payer une exécution. coût de performance pour cela.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.
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:
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: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).
la source
Pourquoi c ++ ne définit-il pas tous les destructeurs virtuels par défaut? Coût du stockage supplémentaire et appel de la table de méthode virtuelle. Le C ++ est utilisé pour la programmation système, à faible temps de latence et rt où cela pourrait être un fardeau.
la source
Voici un bon exemple de l’absence d’utilisation du destructeur virtuel: De Scott Meyers:
Si une classe ne contient aucune fonction virtuelle, cela indique souvent qu'elle n'est pas destinée à être utilisée comme classe de base. Lorsqu'une classe n'est pas destinée à être utilisée comme classe de base, rendre le destructeur virtuel est généralement une mauvaise idée. Considérons cet exemple, basé sur une discussion dans ARM:
Si un int court occupe 16 bits, un objet Point peut s'intégrer dans un registre 32 bits. En outre, un objet Point peut être transmis en tant que quantité 32 bits à des fonctions écrites dans d'autres langages tels que C ou Fortran. Si le destructeur de Point devient virtuel, la situation change.
Dès que vous ajoutez un membre virtuel, un pointeur virtuel est ajouté à votre classe et pointe vers une table virtuelle pour cette classe.
la source
If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.
Wut. Quelqu'un d'autre se souvient-il du bon vieux temps, où nous étions autorisés à utiliser des classes et un héritage pour construire des couches successives de membres et de comportements réutilisables, sans avoir à nous soucier des méthodes virtuelles? Allez, Scott. Je comprends le point essentiel, mais ce "souvent" est vraiment utile.Un destructeur virtuel ajoute un coût d'exécution. Le coût est particulièrement élevé si la classe n'a pas d'autres méthodes virtuelles. Le destructeur virtuel n'est également nécessaire que dans un scénario spécifique, dans lequel un objet est supprimé ou autrement détruit via un pointeur sur une classe de base. Dans ce cas, le destructeur de classe de base doit être virtuel et le destructeur de toute classe dérivée sera implicitement virtuel. Il existe quelques scénarios dans lesquels une classe de base polymorphe est utilisée de telle sorte que le destructeur n'ait pas besoin d'être virtuel:
std::unique_ptr<Derived>
, et le polymorphisme intervient uniquement via des pointeurs et des références non propriétaires. Un autre exemple concerne l'allocation d'objets à l'aide destd::make_shared<Derived>()
. Il est bon d’utiliserstd::shared_ptr<Base>
aussi longtemps que le pointeur initial était unstd::shared_ptr<Derived>
. En effet, les pointeurs partagés ont leur propre dispatch dynamique pour les destructeurs (le suppresseur) qui ne repose pas nécessairement sur un destructeur de classe de base virtuelle.Bien entendu, toute convention visant à utiliser des objets uniquement de la manière susmentionnée peut facilement être rompue. Par conséquent, le conseil de Herb Sutter reste toujours valable: "Les destructeurs de classe de base doivent être publics et virtuels, ou protégés et non virtuels". Ainsi, si quelqu'un tente de supprimer un pointeur sur une classe de base avec un destructeur non virtuel, il recevra probablement une erreur de violation d'accès lors de la compilation.
Là encore, il existe des classes qui ne sont pas conçues pour être des classes de base (publiques). Ma recommandation personnelle est de les rendre
final
en C ++ 11 ou supérieur. Si elle est conçue pour être une cheville carrée, alors il est probable que cela ne fonctionnera pas très bien comme une cheville ronde. Cela est lié à ma préférence pour un contrat d'héritage explicite entre la classe de base et la classe dérivée, pour le modèle de conception NVI (interface non virtuelle), pour les classes de base abstraites plutôt que concrètes, et mon horreur des variables de membre protégées, entre autres , mais je sais que tous ces points de vue sont controversés dans une certaine mesure.la source
La déclaration d'un destructeur
virtual
n'est nécessaire que lorsque vous envisagez de rendre votreclass
héritage possible. Habituellement, les classes de la bibliothèque standard (telles questd::string
) ne fournissent pas de destructeur virtuel et ne sont donc pas conçues pour le sous-classement.la source
delete
d'un pointeur vers une classe de base.Il y aura une surcharge dans le constructeur pour créer la table vtable (si vous n'avez pas d'autres fonctions virtuelles, auquel cas vous devriez PROBABLEMENT, mais pas toujours, avoir un destructeur virtuel également). Et si vous ne possédez aucune autre fonction virtuelle, la taille de votre objet est supérieure à celle d'un pointeur. De toute évidence, l’augmentation de la taille peut avoir un impact important sur les petits objets.
Une mémoire supplémentaire est lue pour obtenir la table vtable, puis appeler la fonction indirectory par le biais de cette fonction, ce qui représente une surcharge par rapport au destructeur non virtuel lorsque le destructeur est appelé. Et bien entendu, un petit code supplémentaire est généré pour chaque appel au destructeur. Cela concerne les cas où le compilateur ne peut pas déduire le type réel - dans les cas où il peut déduire le type actuel, le compilateur n'utilisera pas la vtable, mais appellera directement le destructeur.
Vous devriez avoir un destructeur virtuel si votre classe est conçue comme une classe de base, en particulier si elle peut être créée / détruite par une autre entité que le code qui sait de quel type il est à la création, alors vous avez besoin d' un destructeur virtuel.
Si vous n'êtes pas sûr, utilisez le destructeur virtuel. Il est plus facile de supprimer le virtuel s'il apparaît comme un problème que d'essayer de trouver le bogue causé par "le bon destructeur n'est pas appelé".
En bref, vous ne devriez pas avoir de destructeur virtuel si: 1. Vous n’avez aucune fonction virtuelle. 2. Ne dérivez pas de la classe (marquez-la
final
en C ++ 11, le compilateur dira ainsi si vous essayez d'en dériver).Dans la plupart des cas, la création et la destruction ne représentent pas la majeure partie du temps passé à utiliser un objet particulier, sauf s'il y a "beaucoup de contenu" (la création d'une chaîne de 1 Mo va évidemment prendre un certain temps, car au moins 1 Mo de données doivent être copié de l'endroit où il se trouve actuellement). La destruction d’une chaîne de 1Mo n’est pas pire que celle d’une chaîne de 150B. Les deux opérations nécessiteront la désallocation du stockage de la chaîne, et pas grand chose d’autre. Le temps passé ici est généralement le même "modèle poison" - mais ce n'est pas ainsi que vous allez exécuter votre application réelle en production].
En bref, il y a une petite surcharge, mais pour les petits objets, cela peut faire la différence.
Notez également que les compilateurs peuvent optimiser la recherche virtuelle dans certains cas, c'est donc une pénalité.
Comme toujours en matière de performances, d’encombrement de la mémoire, etc.: effectuez un benchmark, profilez et mesurez, comparez les résultats avec des alternatives et examinez l’essentiel de la perte de temps / de mémoire et n’essayez pas d’optimiser 90% des performances. un code peu exécuté [la plupart des applications ont environ 10% du code qui a une grande influence sur le temps d'exécution et 90% du code qui n'a pas beaucoup d'influence]. Faites cela dans un niveau d'optimisation élevé, de sorte que vous ayez déjà l'avantage du compilateur qui fait du bon travail! Et répétez, vérifiez encore et améliorez pas à pas. N'essayez pas d'être intelligent et d'essayer de déterminer ce qui est important et ce qui ne l'est pas, à moins que vous n'ayez beaucoup d'expérience avec ce type d'application.
la source
You **should** have a virtual destructor if your class is intended as a base-class
s’agit d’une simplification excessive - et d’une pessimisation prématurée . Cela n'est nécessaire que si quelqu'un est autorisé à supprimer une classe dérivée via un pointeur sur la base. Dans de nombreuses situations, tel n'est pas le cas. Si vous le savez, alors bien sûr, encourez les frais généraux. Ce qui, d'ailleurs, est toujours ajouté, même si les appels réels peuvent être résolus de manière statique par le compilateur. Sinon, quand vous contrôlez correctement ce que les gens peuvent faire avec vos objets, ça ne vaut pas la peine