Question

Quelqu'un sait-il pourquoi les conteneurs STL ne disposent pas de destructeurs virtuels?

Autant que je sache, les seuls avantages sont les suivants:

  • réduit la taille d'une instance d'un pointeur (vers la table de méthode virtuelle) et
  • cela accélère un peu la destruction et la construction.

L’inconvénient est qu’il est dangereux de sous-classer les conteneurs de la manière habituelle.

MODIFIER: Peut-être que ma question pourrait être reformulée & «Pourquoi les conteneurs STL n’ont-ils pas été conçus pour permettre l’héritage? &»;

Parce qu’ils ne prennent pas en charge l’héritage, les choix suivants s’imposent lorsqu'on souhaite disposer d'un nouveau conteneur qui requiert la fonctionnalité STL, ainsi qu'un petit nombre de fonctionnalités supplémentaires (par exemple, un constructeur spécialisé ou de nouveaux accesseurs avec des valeurs par défaut pour une carte, ou autre chose):

  • Composition et réplication d'interface : créez un nouveau modèle ou une nouvelle classe possédant le conteneur STL en tant que membre privé et disposant d'une méthode en ligne directe pour chaque méthode STL. Ceci est aussi performant que l'héritage, évite le coût d'une table de méthode virtuelle (dans les cas où cela compte). Malheureusement, les conteneurs STL ont des interfaces assez larges, cela nécessite donc plusieurs lignes de code, ce qui devrait être facile à faire.
  • Créer des fonctions : utilisez des fonctions de fichier nues (éventuellement basées sur un modèle) au lieu d’essayer d’ajouter des fonctions membres. À certains égards, cela peut être une bonne approche, mais les avantages de l’encapsulation sont perdus.
  • Composition avec accès STL public : demandez au propriétaire du conteneur STL de laisser les utilisateurs accéder au conteneur STL lui-même (éventuellement protégé par des accesseurs). Cela demande le moins de code possible pour le rédacteur de bibliothèque, mais c'est beaucoup moins pratique pour les utilisateurs. L'un des principaux arguments de vente de la composition est la réduction du couplage dans votre code, mais cette solution couple complètement le conteneur STL au conteneur propriétaire (car le propriétaire renvoie un véritable conteneur STL).
  • Polymorphisme au moment de la compilation : L’écriture est parfois délicate, elle nécessite de la gymnastique de code et ne convient pas à toutes les situations.

Autre question: existe-t-il un moyen de sous-classer conforme aux normes avec des destructeurs non virtuels (supposons que je ne souhaite pas écraser de méthodes, mais simplement en ajouter de nouvelles)? Mon impression est qu'il n'y a pas de moyen générique et sûr de le faire si on n'a pas le pouvoir de changer le code définissant la classe non virtuelle.

EDIT 2:

Comme le font @doc , les déclarations plus sophistiquées de C ++ 11 using réduisent quelque peu le coût de la composition.

Était-ce utile?

La solution

Je suppose que cela découle de la philosophie C ++ qui consiste à ne pas payer pour des fonctionnalités que vous n'utilisez pas. En fonction de la plate-forme, un pointeur sur la table virtuelle peut être un lourd tribut à payer si l’absence d’un destructeur virtuel ne vous intéresse pas.

Autres conseils

Un destructeur virtuel n’est utile que pour les scénarios d’héritage. Les conteneurs STL ne sont pas conçus pour être hérités (le scénario n'est pas pris en charge). Par conséquent, ils n'ont pas de destructeurs virtuels.

Je pense que Stroustrup a répondu indirectement à cette question dans son article fantastique: Pourquoi C ++ n'est pas simplement un langage de programmation ObjectOriented :

  

7 Remarques de clôture
  Sont les divers   installations présentées ci-dessus   objectivé ou non? Lesquels?   En utilisant quelle définition de   objectivé? Dans la plupart des contextes, je   pense que ce sont les mauvaises questions.   Ce qui compte, ce sont les idées que vous pouvez   exprimer clairement, avec quelle facilité vous pouvez   combiner des logiciels de différents   sources, et comment efficace et   maintenable les programmes résultants   sont. En d'autres termes, comment vous soutenez   bonnes techniques de programmation et bon   les techniques de conception comptent plus que   étiquettes et mots à la mode. Les fondamentaux   l'idée est simplement d'améliorer la conception et   programmation par abstraction. Vous   voulez cacher les détails, vous voulez   exploiter toute similitude dans un système,   et vous voulez rendre cela abordable.   Je voudrais vous encourager à ne pas   faire d'objectorient un sens   terme. La notion de & # 8216; & # 8216; objectivée & # 8217; & # 8217;   est trop souvent dégradé

     

& # 8211; par   l'assimilant avec le bien,

     

& # 8211; en assimilant   avec une seule langue, ou

     

& # 8211; par   tout accepter comme   objectivé.

     

J'ai soutenu que   il y a & # 8211; et doit être & # 8211; utile   techniques au-delà des objectifs   programmation et design. Cependant, pour   éviter d'être totalement incompris, je   tiens à souligner que je   ne voudrait & # 8217; pas tenter un projet sérieux   en utilisant un langage de programmation qui   n'a pas & # 8217; t au moins soutenu le classique   notion de programmation orientée objet.   En plus des installations qui soutiennent   programmation orientée objectivement, je veux & # 8211;   et C ++ fournit & # 8211; caractéristiques qui vont   au-delà de ceux dans leur soutien à   expression directe de concepts et   relations.

STL a été construit principalement avec trois outils conceptuels. Programmation générique + Style fonctionnel + Abstraction de données == Style STL . Il n’est pas étonnant que la programmation orientée objet ne soit pas le meilleur moyen de représenter une structure de données &; Bibliothèque d'algorithmes. Bien que la POO soit utilisée dans d'autres parties de la bibliothèque standard, le concepteur de STL a constaté que la combinaison des trois techniques mentionnées était meilleure que la POO seule . En bref, la bibliothèque n'a pas été conçue avec la POO en tête, et en C ++, si vous ne l'utilisez pas, elle n'est pas fournie avec votre code. Vous ne payez pas pour ce que vous n'utilisez pas. Les classes std :: vector, std :: list, ... ne sont pas des concepts de POO au sens de Java / C #. Ce ne sont que Types de données abstraits dans la meilleure interprétation.

  

Pourquoi les conteneurs STL n'ont-ils pas été conçus pour permettre l'héritage?

À mon humble avis, ils le sont. S'ils ne le voulaient pas, ils avaient été qualifiés de finale . Et lorsque je regarde les stl_vector.h sources, je constate que mon implémentation STL utilise l'héritage protégé de _Vector_base<_Tp, _Alloc> pour accorder l'accès aux classes dérivées:

 template<typename _Tp, typename _Alloc = allocator<_Tp> >
 class vector : protected _Vector_base<_Tp, _Alloc>

N'utiliserait-il pas l'héritage privé si les sous-classes n'étaient pas les bienvenues?

  

existe-t-il un moyen conforme aux normes de sous-classer avec des destructeurs non virtuels (supposons que je ne souhaite pas remplacer de méthodes, mais simplement en ajouter de nouvelles)?

Pourquoi ne pas utiliser protected ou private l'héritage et exposer la partie souhaitée de l'interface avec using mot clé?

class MyVector : private std::vector<int>
{
     typedef std::vector<int> Parent;

     public:
        using Parent::size;
        using Parent::push_back;
        using Parent::clear;
        //and so on + of course required ctors, dtors and operators.
};

Cette approche garantit que l'utilisateur de la classe ne va pas transférer l'instance à std::vector<int> et qu'il est en sécurité, car le seul problème avec le destructeur non virtuel est qu'il n'appellera pas celui qui est dérivé, lorsque l'objet est supprimé en tant que instance de la classe parente.

... J'ai aussi une idée fausse, que vous pouvez même hériter en public si votre classe n'a pas de destructeur. Hérésie?

Comme il a été souligné, les conteneurs STL ne sont pas conçus pour pouvoir être hérités. Aucune méthode virtuelle, tous les membres de données sont privés, aucun getters / setters / helpers protégés .. Et comme vous l'avez découvert, aucun destructeur virtuel ..

Je suggérerais que vous devriez vraiment utiliser les conteneurs via la composition plutôt que l'héritage d'implémentation, dans un & "a-a &"; manière plutôt qu'un & "; est-un &"; un.

vous n'êtes pas censé ajouter aveuglément un destructeur virtuel à chaque classe. Si tel était le cas, le langage ne vous autoriserait aucune autre option. Lorsque vous ajoutez une méthode virtuelle à une classe qui ne comporte aucune autre méthode virtuelle, vous venez d'augmenter la taille des instances de la classe par la taille d'un pointeur, généralement 4 octets. C'est cher en fonction de ce que vous faites. L'augmentation de la taille se produit car une v-table est créée pour contenir la liste des méthodes virtuelles et chaque instance nécessite un pointeur sur la v-table. Il est généralement situé dans la première cellule de l'instance.

Une autre solution permettant de sous-classer à partir de conteneurs STL est celle proposée par Bo Qian à l’aide de pointeurs intelligents.

C ++ avancé: Destructeur virtuel et Smart Destructor

class Dog {
public:
   ~Dog() {cout << "Dog is destroyed"; }
};

class Yellowdog : public Dog {
public:
   ~Yellowdog() {cout << "Yellow dog destroyed." << endl; }
};


class DogFactory {
public:
   static shared_ptr<Dog> createYellowDog() { 
      return shared_ptr<Yellowdog>(new Yellowdog()); 
   }    
};

int main() {
    shared_ptr<Dog> pd = DogFactory::createYellowDog();

    return 0;
}

Cela évite complètement le dillema avec destructeurs virtuels.

Si vous avez vraiment besoin d'un destructeur virtuel, vous pouvez l'ajouter à une classe dérivée du vecteur < > ;, puis utiliser cette classe comme classe de base partout où vous avez besoin d'une interface virtuelle. En faisant cela, le compilateur appellera un destructeur virtuel de votre classe de base, qui à son tour appellera un destructeur non virtuel de la classe de vecteur.

Exemple:

#include <vector>
#include <iostream>

using namespace std;

class Test
{
    int val;
public:
    Test(int val) : val(val)
    {
        cout << "Creating Test " << val << endl;
    }
    Test(const Test& other) : val(other.val)
    {
        cout << "Creating copy of Test " << val << endl;
    }
    ~Test()
    {
        cout << "Destructing Test " << val << endl;
    }
};

class BaseVector : public vector<Test>
{
public:
    BaseVector()
    {
        cout << "Creating BaseVector" << endl;
    }
    virtual ~BaseVector()
    {
        cout << "Destructing BaseVector" << endl;
    }
};

class FooVector : public BaseVector
{
public:
    FooVector()
    {
        cout << "Creating FooVector" << endl;
    }
    virtual ~FooVector()
    {
        cout << "Destructing FooVector" << endl;
    }
};

int main()
{
    BaseVector* ptr = new FooVector();
    ptr->push_back(Test(1));
    delete ptr;

    return 0;
}

Ce code donne la sortie suivante:

Creating BaseVector
Creating FooVector
Creating Test 1
Creating copy of Test 1
Destructing Test 1
Destructing FooVector
Destructing BaseVector
Destructing Test 1

Aucun destructeur virtuel n'empêche la classe d'être correctement sous-classe.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top