Quand NE PAS utiliser les destructeurs virtuels?

48

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?

ggrr
la source
6
Et si votre classe n'est pas supposée être héritée? Regardez beaucoup de classes de bibliothèques standard, peu ont des fonctions virtuelles car elles ne sont pas conçues pour être héritées.
Certains programmeur mec
4
De plus, je pense que dans la plupart des cas, les destructeurs doivent être virtuels. Nan. Pas du tout. Seuls ceux qui abusent de l'héritage (plutôt que de favoriser la composition) le pensent. J'ai vu des applications entières avec seulement une poignée de classes de base et de fonctions virtuelles.
Matthieu M.
1
@underscore_d Avec des implémentations typiques, un code supplémentaire serait généré pour toute classe polymorphe, à moins que tous ces éléments implicites aient été dévirtualisés et optimisés. Dans les ABI communes, cela implique au moins une table virtuelle pour chaque classe. La disposition de la classe doit également être modifiée. Vous ne pouvez pas être de retour fiable une fois que vous avez publié une classe de ce type en tant qu'éléments d'une interface publique, car le changer à nouveau briserait la compatibilité ABI, car il est évidemment (voire jamais possible) d'attendre une dévirtualisation sous forme de contrat d'interface en général.
FrankHB
1
@underscore_d La phrase "au moment de la compilation" est inexacte, mais je pense que cela signifie qu'un destructeur virtuel ne peut pas être trivial ni spécifié par constexpr. Il est donc difficile d'éviter la génération de code supplémentaire (à moins d'éviter totalement la destruction de tels objets). cela nuirait plus ou moins aux performances d'exécution.
FrankHB
2
@underscore_d Le "pointeur" semble être un fil rouge. Ce devrait éventuellement être un pointeur sur membre (qui n'est pas un pointeur par définition). Avec les ABI habituelles, un pointeur sur membre n'est généralement pas adapté à un mot machine (en tant que pointeurs classiques), et le passage d'une classe de non polymorphe à polymorphe changerait souvent la taille du pointeur en membre de cette classe.
FrankHB

Réponses:

41

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 placer newune 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.

"dans la plupart des cas, les destructeurs doivent être virtuels"

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.

Tony
la source
23

Dans quel cas je ne devrais PAS utiliser de destructeurs virtuels?

  1. Pour une classe concrète qui ne veut pas être héritée.
  2. Pour une classe de base sans suppression polymorphe. Les deux clients ne doivent pas pouvoir supprimer de manière polymorphe à l'aide d'un pointeur sur Base.

BTW,

Dans quel cas doit utiliser des destructeurs virtuels?

Pour une classe de base avec suppression polymorphe.

Songyuanyao
la source
7
+1 pour # 2, spécifiquement sans suppression polymorphe . Si votre destructeur ne peut jamais être invoqué via un pointeur de base, le rendre virtuel est inutile et redondant, en particulier si votre classe n'était pas virtuelle auparavant (elle devient donc complètement surchargée de RTTI). Comme le recommande Herb Sutter, pour vous protéger contre tout utilisateur qui enfreint cette règle, vous feriez en sorte que le dtor de la classe de base soit protégé et non virtuel, de sorte qu'il ne puisse être invoqué que par / après un destructeur dérivé.
underscore_d
@underscore_d imho C'est un point important qui m'a échappé dans les réponses, comme en présence d'héritage, le seul cas où je n'ai pas besoin de constructeur virtuel, c'est quand je peux m'assurer qu'il ne sera jamais nécessaire
anciennementknownas_463035818 le
14

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 Integerclasse, 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 vtablemé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 vptrpar 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 vtableaspect 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 objectclasse 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 vptrmé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 intdans 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 Standardschapitre 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).

ChrisF
la source
9

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.

ML
la source
Les destructeurs ne devraient pas être utilisés en premier lieu dans les systèmes temps réel difficiles, car de nombreuses ressources, telles que la mémoire dynamique, ne peuvent pas être utilisées pour offrir des garanties de délai élevées
Marco A.,
9
@MarcoA. Depuis quand les destructeurs impliquent-ils une allocation de mémoire dynamique?
chbaker0
@ chbaker0 J'ai utilisé un 'like'. Ils ne sont tout simplement pas utilisés dans mon expérience.
Marco A.
6
Il est également insensé que la mémoire dynamique ne puisse pas être utilisée dans des systèmes temps réel difficiles. Il est assez simple de prouver qu'un segment de mémoire préconfiguré avec des tailles d'allocation fixes et un bitmap d'allocation allouera de la mémoire ou retournera une condition de manque de mémoire dans le temps nécessaire à l'analyse de ce bitmap.
MSalters
@msalters, cela me fait réfléchir: imaginez un programme dans lequel le coût de chaque opération était stocké dans le système de types. Permettre les vérifications à la compilation des garanties en temps réel.
Yakk
5

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:

// class for representing 2D points
class Point {
public:
    Point(short int xCoord, short int yCoord);
    ~Point();
private:
    short int x, y;
};

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.
underscore_d
3

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:

  • Si des instances de classes dérivées ne sont pas allouées sur le tas, par exemple uniquement directement sur la pile ou à l'intérieur d'autres objets. (Sauf si vous utilisez la mémoire non initialisée et l'opérateur de placement nouveau.)
  • Si des instances de classes dérivées sont allouées sur le segment de mémoire, la suppression s'effectue uniquement via des pointeurs vers la classe la plus dérivée, par exemple 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 de std::make_shared<Derived>(). Il est bon d’utiliser std::shared_ptr<Base>aussi longtemps que le pointeur initial était un std::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 finalen 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.

Arne Vogel
la source
1

La déclaration d'un destructeur virtualn'est nécessaire que lorsque vous envisagez de rendre votre classhéritage possible. Habituellement, les classes de la bibliothèque standard (telles que std::string) ne fournissent pas de destructeur virtuel et ne sont donc pas conçues pour le sous-classement.

Constantinius
la source
3
La raison en est le sous-classement + utilisation du polymorphisme. Un destructeur virtuel n’est requis que si une résolution dynamique est nécessaire, c’est-à-dire une référence / un pointeur / quelle que soit la classe principale pouvant faire référence à une instance d’une sous-classe.
Michel Billaud
2
@MichelBillaud en fait, vous pouvez toujours avoir un polymorphisme sans dtors virtuels. Un dtor virtuel est UNIQUEMENT requis pour la suppression polymorphe, c'est-à-dire l'appel deleted'un pointeur vers une classe de base.
chbaker0
1

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 finalen 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.

Mats Petersson
la source
1
"sera un overhead dans le constructeur pour créer la vtable" - le vtable est généralement "créé" classe par classe par le compilateur, le constructeur ne disposant que de overhead pour stocker un pointeur dans l'instance d'objet en construction.
Tony
En outre ... je veux éviter l’optimisation prématurée, mais à l’inverse, il You **should** have a virtual destructor if your class is intended as a base-classs’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
underscore_d