Sous-classe / hériter des conteneurs standard?

Je lis souvent ces déclarations sur le débordement de stack. Personnellement, je ne trouve aucun problème avec ceci, sauf si je l’utilise de manière polymorphe; c’est-à-dire où je dois utiliser virtual destructeur virtual .

Si je veux étendre / append les fonctionnalités d’un conteneur standard, quelle est la meilleure façon d’en hériter? Emballer ces conteneurs dans une classe personnalisée demande beaucoup plus d’effort et rest impur.

Il y a un certain nombre de raisons pour lesquelles c’est une mauvaise idée.

Tout d’abord, c’est une mauvaise idée car les conteneurs standard n’ont pas de destructeurs virtuels . Vous ne devez jamais utiliser quelque chose de polymorphe sans destructeur virtuel, car vous ne pouvez pas garantir le nettoyage de votre classe dérivée.

Règles de base pour les développeurs virtuels

Deuxièmement, c’est vraiment un mauvais design. Et il y a en fait plusieurs raisons pour lesquelles c’est une mauvaise conception. Tout d’abord, vous devez toujours étendre les fonctionnalités des conteneurs standard via des algorithmes fonctionnant de manière générique. C’est une raison de complexité simple – si vous devez écrire un algorithme pour chaque conteneur auquel il s’applique et que vous avez des conteneurs M et N algorithmes, cela signifie M x N méthodes que vous devez écrire. Si vous écrivez vos algorithmes de manière générique, vous ne disposez que de N algorithmes. Donc, vous obtenez beaucoup plus de réutilisation.

C’est aussi une mauvaise conception car vous brisez une bonne encapsulation en héritant du conteneur. Une bonne règle de base est la suivante: si vous pouvez effectuer ce dont vous avez besoin en utilisant l’interface publique d’un type, faites en sorte que ce nouveau comportement soit externe au type. Cela améliore l’encapsulation. Si vous souhaitez implémenter un nouveau comportement, faites-en une fonction de scope d’espace de noms (comme les algorithmes). Si vous avez un nouvel invariant à imposer, utilisez le confinement dans une classe.

Une description classique de l’encapsulation

Enfin, en général, vous ne devriez jamais penser à l’inheritance comme moyen d’étendre le comportement d’une classe. C’est l’un des grands et mauvais mensonges de la théorie de la POO au début, due à une reflection imprécise sur la réutilisation, et elle continue d’être enseignée et promue jusqu’à ce jour, même si une théorie claire explique pourquoi elle est mauvaise. Lorsque vous utilisez l’inheritance pour étendre le comportement, vous attachez ce comportement étendu à votre contrat d’interface d’une manière qui lie les utilisateurs aux modifications futures. Par exemple, disons que vous avez une classe de type Socket qui communique en utilisant le protocole TCP et que vous étendez son comportement en dérivant une classe SSLSocket de Socket et en implémentant le comportement du protocole de stack SSL supérieur sur Socket. Maintenant, supposons que vous ayez une nouvelle exigence pour avoir le même protocole de communication, mais sur une ligne USB ou sur la téléphonie. Vous devez couper et coller tout ce qui fonctionne dans une nouvelle classe dérivée d’une classe USB ou d’une classe de téléphonie. Et maintenant, si vous trouvez un bogue, vous devez le réparer aux trois endroits, ce qui ne se produira pas toujours, ce qui signifie que les bogues prendront plus de temps et ne seront pas toujours corrigés …

Ceci est général pour toute hiérarchie d’inheritance A-> B-> C -> … Lorsque vous voulez utiliser les comportements que vous avez étendus dans des classes dérivées, comme B, C, .. sur des objects qui ne sont pas de la classe de base A, vous devez redessiner ou vous dupliquez la mise en œuvre. Cela conduit à des conceptions très monolithiques qui sont très difficiles à changer (pensez au MFC de Microsoft ou à leur .NET, ou – eh bien, elles commettent beaucoup cette erreur). Au lieu de cela, vous devriez presque toujours penser à une extension par la composition chaque fois que possible. L’inheritance doit être utilisé lorsque vous pensez “Principe ouvert / fermé”. Vous devez avoir des classes de base abstraites et un runtime de polymorphism dynamic via la classe héritée, chacune ayant des implémentations complètes. Les hiérarchies ne devraient pas être profondes – presque toujours à deux niveaux. N’utilisez plus que deux lorsque vous avez différentes catégories dynamics qui se rapportent à diverses fonctions nécessitant cette distinction pour le type de sécurité. Dans ces cas, utilisez des bases abstraites jusqu’aux classes feuille, qui ont l’implémentation.

Peut-être que beaucoup de gens ici n’apprécieront pas cette réponse, mais il est temps qu’une hérésie soit dite et que oui, on vous dise aussi que “le roi est nu!”

Toutes les motivations contre la dérivation sont faibles. La dérivation n’est pas différente de la composition. C’est juste un moyen de “mettre les choses ensemble”. La composition met les choses ensemble en leur donnant des noms, l’inheritance le fait sans donner de noms explicites.

Si vous avez besoin d’un vecteur ayant la même interface et l’implémentation de std :: vect plus quelque chose de plus, vous pouvez:

utiliser la composition et réécrire tous les prototypes de la fonction d’object incorporée qui implémentent la fonction qui les délègue (et s’ils sont 10000 … oui: soyez prêt à réécrire tous ces 10000) ou …

héritez-le et ajoutez simplement ce dont vous avez besoin (et réécrivez simplement les constructeurs, jusqu’à ce que les avocats C ++ décident de les laisser également être héritables: je me souviens encore il y a 10 ans de discussions zélées sur “pourquoi les est une “mauvaise mauvaise mauvaise chose” … jusqu’à ce que C ++ 11 le permette et soudain tous ces zélotes se taisent!) et laisse le nouveau destructeur non virtuel tel qu’il était à l’origine.

Tout comme pour toutes les classes qui ont une méthode virtuelle et d’ autres non, vous savez que vous ne pouvez pas prétendre invoquer la méthode non virtuelle de dérivée en vous adressant à la base. Il en va de même pour la suppression. Il n’y a pas de raison de supprimer pour prétendre à un soin particulier. Un programmeur qui sait que ce qui n’est pas virtuel ne peut être appelé par la base sait également qu’il ne faut pas utiliser la suppression sur votre base après l’allocation de votre base.

Tous les “éviter ceci” “ne font pas cela” sonnent toujours comme une “moralisation” de quelque chose qui est nativement agnostique. Toutes les fonctionnalités d’une langue existent pour résoudre certains problèmes. Le fait qu’un moyen donné de résoudre le problème soit bon ou mauvais dépend du contexte et non de l’entité elle-même. Si ce que vous faites a besoin de servir plusieurs conteneurs, l’inheritance n’est probablement pas la solution (vous devez tout refaire). Si c’est pour un cas spécifique … l’inheritance est un moyen de composer. Oubliez les purismes de la POO: C ++ n’est pas une “POO pure” et le conteneur n’est pas une POO.

Vous devriez vous abstenir de dériver publiquement des contianers standard. Vous pouvez choisir entre inheritance privé et composition et il me semble que toutes les directives générales indiquent que la composition est meilleure ici puisque vous ne remplacez aucune fonction. Ne tirez pas publiquement des conteneurs STL – il n’y en a pas vraiment besoin.

Soit dit en passant, si vous voulez append un tas d’algorithmes au conteneur, envisagez de les append en tant que fonctions autonomes prenant une plage d’iterators.

Le problème est que vous, ou une autre personne, risque de transmettre accidentellement votre classe étendue à une fonction qui attend une référence à la classe de base. Cela va efficacement (et silencieusement!) Couper les extensions et créer des bogues difficiles à trouver.

Devoir écrire certaines fonctions de transfert semble être un petit prix à payer en comparaison.

L’inheritance public est un problème pour toutes les raisons que d’autres ont énoncées, à savoir que votre conteneur peut être redirigé vers la classe de base qui n’a pas de destructeur virtuel ou d’opérateur d’affectation virtuelle, ce qui peut entraîner des problèmes de segmentation .

L’inheritance privé, par contre, est moins problématique. Prenons l’exemple suivant:

 #include  #include  // private inheritance, nobody else knows about the inheritance, so nobody is upcasting my // container to a std::vector template  class MyVector : private std::vector { private: // in case I changed to boost or something later, I don't have to update everything below typedef std::vector base_vector; public: typedef typename base_vector::size_type size_type; typedef typename base_vector::iterator iterator; typedef typename base_vector::const_iterator const_iterator; using base_vector::operator[]; using base_vector::begin; using base_vector::clear; using base_vector::end; using base_vector::erase; using base_vector::push_back; using base_vector::reserve; using base_vector::resize; using base_vector::size; // custom extension void reverse() { std::reverse(this->begin(), this->end()); } void print_to_console() { for (auto it = this->begin(); it != this->end(); ++it) { std::cout << *it << '\n'; } } }; int main(int argc, char** argv) { MyVector intArray; intArray.resize(10); for (int i = 0; i < 10; ++i) { intArray[i] = i + 1; } intArray.print_to_console(); intArray.reverse(); intArray.print_to_console(); for (auto it = intArray.begin(); it != intArray.end();) { it = intArray.erase(it); } intArray.print_to_console(); return 0; } 

SORTIE:

 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 

Propre et simple et vous donne la liberté d'étendre les conteneurs std sans trop d'effort.

Et si vous pensez à faire quelque chose de stupide, comme ceci:

 std::vector* stdVector = &intArray; 

Vous obtenez ceci:

 error C2243: 'type cast': conversion from 'MyVector *' to 'std::vector> *' exists, but is inaccessible 

Parce que vous ne pouvez jamais garantir que vous ne les avez pas utilisés de manière polymorphe. Vous suppliez pour des problèmes. Prendre l’effort d’écrire quelques fonctions n’est pas une mince affaire, et même le fait de vouloir le faire est au mieux douteux. Qu’est-il arrivé à l’encapsulation?

La raison la plus courante pour vouloir hériter des conteneurs est que vous voulez append une fonction membre à la classe. Puisque stdlib lui-même n’est pas modifiable, on pense que l’inheritance est le substitut. Cela ne fonctionne pas cependant. Il est préférable de faire une fonction gratuite prenant un vecteur comme paramètre:

 void f(std::vector &v) { ... } 

Je hérite parfois de types de collection simplement pour mieux nommer les types.
Je n’aime pas le typedef par préférence personnelle. Je vais donc faire quelque chose comme:

 class GizmoList : public std::vector { /* No Body & no changes. Just a more descriptive name */ }; 

Ensuite, il est beaucoup plus facile et plus clair d’écrire:

 GizmoList aList = GetGizmos(); 

Si vous commencez à append des méthodes à GizmoList, vous risquez de rencontrer des problèmes.

À mon humble avis, je ne trouve aucun mal à hériter des conteneurs STL s’ils sont utilisés comme extensions de fonctionnalité . (C’est pour ça que j’ai posé cette question :))

Le problème potentiel peut se produire lorsque vous essayez de transmettre le pointeur / référence de votre conteneur personnalisé à un conteneur standard.

 template struct MyVector : std::vector {}; std::vector* p = new MyVector; //.... delete p; // oops "Undefined Behavior"; as vector::~vector() is not 'virtual' 

Ces problèmes peuvent être évités consciemment , à condition de suivre une bonne pratique de programmation.

Si je veux prendre un soin extrême, je peux aller jusqu’à ceci:

 #include template struct MyVector : std::vector {}; #define vector DONT_USE 

Ce qui interdira l’utilisation du vector entièrement.