web-dev-qa-db-fra.com

Quand NE PAS utiliser de destructeurs virtuels?

Je pensais avoir cherché à plusieurs reprises sur les destructeurs virtuels, la plupart mentionnent le but des destructeurs virtuels et pourquoi vous avez besoin de destructeurs virtuels. Je pense aussi que dans la plupart des cas, les destructeurs doivent être virtuels.

Ensuite, 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 des destructeurs virtuels?

Dans quel cas ne devrais-je PAS utiliser des destructeurs virtuels?

Quel est le coût d'utilisation des destructeurs virtuels si je l'utilise même s'il n'est pas nécessaire?

51
ggrr

Si vous ajoutez un destructeur virtuel à une classe:

  • dans la plupart (toutes?) des implémentations C++ actuelles, chaque instance d'objet de cette classe doit stocker un pointeur vers la table de répartition virtuelle pour le type d'exécution, et cette table de répartition virtuelle elle-même ajoutée à l'image exécutable

  • l'adresse de la table de répartition virtuelle n'est pas nécessairement valide entre les processus, ce qui peut empêcher le partage en toute sécurité de tels objets dans la mémoire partagée

  • avoir un pointeur virtuel intégré empêche de créer une classe avec une disposition de mémoire correspondant à un format d'entrée ou de sortie connu (par exemple, un Price_Tick* pourrait viser directement la mémoire convenablement alignée dans un paquet UDP entrant et être utilisée pour analyser/accéder ou modifier les données, ou le placement -newing une telle classe pour écrire des données dans un paquet sortant)

  • le destructeur appelle lui-même - dans certaines conditions - doit être distribué virtuellement et donc hors ligne, tandis que les destructeurs non virtuels peuvent être alignés ou optimisés s'ils sont triviaux ou non pertinents pour l'appelant

L'argument "non conçu pour être hérité de" ne serait pas une raison pratique pour ne pas toujours avoir un destructeur virtuel s'il n'était pas également pire d'une manière pratique comme expliqué ci-dessus; mais étant donné que c'est pire, c'est un critère majeur pour savoir quand payer: par défaut avoir un destructeur virtuel si votre classe est destinée à ê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é 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 tellement ... de nombreuses classes n'ont pas un tel besoin. Il y a tellement d'exemples de cas où il est inutile de les énumérer, mais regardez simplement dans votre bibliothèque standard ou dites 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.

41
Tony

Dans quel cas ne devrais-je PAS utiliser des 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. Soit les clients ne doivent pas être en mesure de supprimer de manière polymorphe à l'aide d'un pointeur vers Base.

BTW,

Dans quel cas utiliser des destructeurs virtuels?

Pour une classe de base avec suppression polymorphe.

25
songyuanyao

Quel est le coût d'utilisation des destructeurs virtuels si je l'utilise même s'il n'est pas nécessaire?

Le coût de l'introduction de la fonction virtuelle any dans une classe (héritée ou faisant partie de la définition de classe) est un coût initial éventuellement très élevé (ou non en fonction de l'objet) d'un pointeur virtuel stocké par objet, comme donc:

struct Integer
{
    virtual ~Integer() {}
    int value;
};

Dans ce cas, le coût de la mémoire est relativement énorme. La taille réelle de la mémoire d'une instance de classe ressemblera désormais 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 classe Integer au lieu de 4 octets. Si nous en stockons un million dans un tableau, nous nous retrouvons avec 16 mégaoctets d'utilisation de la mémoire: deux fois la taille du cache CPU L3 typique de 8 Mo, et l'itération à travers un tel tableau à plusieurs reprises peut être beaucoup plus lente que l'équivalent de 4 mégaoctets sans le pointeur virtuel à la suite de ratés de cache supplémentaires et de défauts de page.

Ce coût de pointeur virtuel par objet, cependant, n'augmente pas avec plus de fonctions virtuelles. Vous pouvez avoir 100 fonctions membres virtuelles dans une classe et la surcharge 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 des frais généraux. Cependant, en plus d'un pointeur virtuel par instance est un coût par classe. Chaque classe avec des fonctions virtuelles génère un vtable en mémoire qui stocke les adresses des fonctions qu'elle doit réellement appeler (répartition virtuelle/dynamique) lors d'un appel de fonction virtuelle. Le vptr stocké par instance pointe alors vers ce vtable spécifique à la classe. Cette surcharge est généralement une préoccupation moindre, mais elle peut gonfler votre taille binaire et ajouter un peu de coût d'exécution si cette surcharge a été payée inutilement pour mille classes dans une base de code complexe, par ex. Ce côté vtable du coût augmente en fait proportionnellement avec de plus en plus de fonctions virtuelles dans le mix.

Les développeurs Java travaillant dans des domaines critiques pour les performances comprennent très bien ce type de surcharge (bien que souvent décrit dans le contexte de la boxe), car un type défini par l'utilisateur Java hérite implicitement d'un _ object classe de base et toutes les fonctions de Java sont implicitement de nature virtuelle (remplaçable) sauf indication contraire. Par conséquent, un Java Integer a également tendance à nécessiter 16 octets de mémoire sur les plates-formes 64 bits à la suite de ces métadonnées de style vptr associées par instance, et il est généralement impossible dans Java d'envelopper quelque chose comme un seul int dans une classe sans payer de coût d'exécution pour cela.

Alors la question est: pourquoi c ++ ne définit-il pas tous les destructeurs virtuels par défaut?

C++ favorise vraiment les performances avec un état d'esprit de type "pay as you go" et aussi beaucoup de conceptions axées sur le matériel nues héritées de C. Il ne veut pas inclure inutilement les frais généraux requis pour la génération de vtables et la répartition dynamique pour chaque classe/instance impliquée. Si les performances ne sont pas l'une des principales raisons pour lesquelles vous utilisez un langage comme C++, vous pourriez bénéficier davantage d'autres langages de programmation, car une grande partie du langage C++ est moins sûre et plus difficile qu'elle ne pourrait l'être dans l'idéal, les performances étant souvent la raison principale de privilégier un tel design.

Quand n'ai-je PAS besoin d'utiliser des 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 ne paierait que des frais généraux éventuellement importants 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 ceci:

class BaseClass
{
protected:
    // Disallow deleting/destroying subclass objects through `BaseClass*`.
    ~BaseClass() {}
};

Dans quel cas ne devrais-je PAS utiliser des destructeurs virtuels?

Il est en fait plus facile à couvrir lorsque vous devriez utiliser des destructeurs virtuels. Très souvent, beaucoup plus de classes dans votre base de code ne sont pas conçues pour l'héritage.

std::vector, par exemple, n'est pas conçu pour être hérité et ne devrait généralement pas être hérité (conception très fragile), car cela sera alors sujet à ce problème de suppression du pointeur de base (std::vector évite délibérément un destructeur virtuel) en plus des problèmes maladroits découpage d'objet si votre classe dérivée ajoute un nouvel état.

En général, une classe héritée doit avoir un destructeur virtuel public ou un destructeur protégé non virtuel. De C++ Coding Standards, chapitre 50:

50. Rendre les destructeurs de classe de base publics et virtuels, ou protégés et non virtuels. Supprimer ou ne pas supprimer; c'est la question: si la suppression via un pointeur vers une base Base doit être autorisée, le destructeur de la base doit être public et virtuel. Sinon, il doit être protégé et non virtuel.

L'une des choses que le C++ a tendance à souligner implicitement (parce que les conceptions ont tendance à devenir vraiment fragiles et maladroites et peut-être même peu sûres autrement) est l'idée que l'héritage n'est pas un mécanisme conçu pour être utilisé après coup. C'est un mécanisme d'extensibilité avec le polymorphisme à l'esprit, mais qui nécessite une prévoyance quant à l'endroit où l'extensibilité est nécessaire. Par conséquent, vos classes de base doivent être conçues comme les racines d'une hiérarchie d'héritage dès le départ, et non comme quelque chose dont vous héritez plus tard après coup sans une telle prévoyance à l'avance.

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

16
user204677

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 des méthodes virtuelles. C++ est utilisé pour la programmation système, à faible latence et rt où cela pourrait être un fardeau.

9
M.L.

Voici un bon exemple du moment où ne pas utiliser le 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érez cet exemple, basé sur une discussion dans l'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 tenir dans un registre 32 bits. De plus, un objet Point peut être transmis sous forme de quantité 32 bits à des fonctions écrites dans d'autres langages tels que C ou FORTRAN. Cependant, si le destructeur de Point est rendu virtuel, la situation change.

Au moment où vous ajoutez un membre virtuel, un pointeur virtuel est ajouté à votre classe qui pointe vers la table virtuelle pour cette classe.

6
basav

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, où un objet est supprimé ou autrement détruit via un pointeur vers 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 manière que le destructeur n'a pas besoin d'être virtuel:

  • Si les 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 de la mémoire non initialisée et un opérateur de placement nouveau.)
  • Si des instances de classes dérivées sont allouées sur le tas, mais que la suppression se produit uniquement via des pointeurs vers la classe la plus dérivée, par exemple il y a un std::unique_ptr<Derived>, et le polymorphisme ne se produit que par des pointeurs et des références non propriétaires. Un autre exemple est lorsque les objets sont alloués à 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 répartition dynamique pour les destructeurs (le suppresseur) qui ne repose pas nécessairement sur un destructeur de classe de base virtuelle.

Bien sûr, toute convention visant à n'utiliser des objets que de la manière susmentionnée peut facilement être rompue. Par conséquent, les conseils de Herb Sutter restent aussi valables que jamais: "Les destructeurs de classe de base doivent être soit publics et virtuels, soit protégés et non virtuels." De cette façon, si quelqu'un tente de supprimer un pointeur vers une classe de base avec un destructeur non virtuel, il recevra très probablement une erreur de violation d'accès au moment 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 faire final en C++ 11 ou supérieur. S'il est conçu pour être une cheville carrée, 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 membres protégées, entre autres choses. , mais je sais que tous ces points de vue sont controversés dans une certaine mesure.

3
Arne Vogel

La déclaration d'un destructeur virtual n'est nécessaire que lorsque vous prévoyez de rendre votre class héritable. Généralement, les classes de la bibliothèque standard (telles que std::string) ne fournit pas de destructeur virtuel et n'est donc pas destiné au sous-classement.

1
Constantinius

Il y aura une surcharge dans le constructeur pour créer la table virtuelle (si vous n'avez pas d'autres fonctions virtuelles, auquel cas vous devriez PROBABLEMENT, mais pas toujours, avoir un destructeur virtuel aussi). Et si vous ne disposez d'aucune autre fonction virtuelle, cela rend votre objet d'une taille de pointeur plus grande qu'autrement. De toute évidence, l'augmentation de la taille peut avoir un impact important sur les petits objets.

Il y a une mémoire supplémentaire lue pour obtenir la vtable et appeler ensuite la fonction indirectory via celle-ci, ce qui est supérieur au destructeur non virtuel lorsque le destructeur est appelé. Et bien sûr, en conséquence, un peu de code supplémentaire généré pour chaque appel au destructeur. C'est pour les cas où le compilateur ne peut pas déduire le type réel - dans les cas où il peut déduire le type réel, 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 s'agit à 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 provoqué par "le bon destructeur n'est pas appelé".

En bref, vous ne devriez pas avoir un destructeur virtuel si: 1. Vous n'avez aucune fonction virtuelle. 2. Ne dérivez pas de la classe (marquez-la final en C++ 11, de cette façon le compilateur dira si vous essayez de dériver de celle-ci).

Dans la plupart des cas, la création et la destruction ne sont pas une partie importante 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 être copié de l'endroit où il se trouve actuellement). La destruction d'une chaîne de 1 Mo n'est pas pire que la destruction d'une chaîne de 150 bits, les deux nécessiteront la désallocation du stockage de la chaîne, et pas grand chose d'autre, donc le temps passé là-bas est généralement le même [sauf s'il s'agit d'une construction de débogage, où la désallocation remplit souvent la mémoire avec un "modèle empoisonné" - 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 une différence.

Notez également que les compilateurs peuvent optimiser la recherche virtuelle dans certains cas, ce n'est donc qu'une pénalité

Comme toujours en ce qui concerne les performances, l'empreinte mémoire, etc.: comparer et profiler et mesurer, comparer les résultats avec des alternatives et regarder où la majeure partie du temps/de la mémoire est dépensée, et n'essayez pas d'optimiser les 90% de code qui n'est pas beaucoup exécuté [la plupart des applications ont environ 10% de code qui a une grande influence sur le temps d'exécution, et 90% de code qui n'a pas beaucoup d'influence du tout]. Faites cela dans un niveau d'optimisation élevé, vous avez donc déjà l'avantage du compilateur qui fait du bon travail! Et répétez, vérifiez à nouveau et améliorez pas à pas. N'essayez pas d'être intelligent et essayez de comprendre ce qui est important et ce qui ne l'est pas, sauf si vous avez beaucoup d'expérience avec ce type particulier d'application.

1
Mats Petersson