Est-il correct d’hériter de l’implémentation à partir de conteneurs STL, plutôt que de déléguer?

J’ai une classe qui adapte std :: vector pour modéliser un conteneur d’objects spécifiques à un domaine. Je veux exposer la plus grande partie de l’API std :: vector à l’utilisateur afin qu’il utilise des méthodes familières (taille, clear, at, etc …) et des algorithmes standard sur le conteneur. Cela semble être un motif récurrent pour moi dans mes créations:

class MyContainer : public std::vector { public: // Redeclare all container traits: value_type, iterator, etc... // Domain-specific constructors // (more useful to the user than std::vector ones...) // Add a few domain-specific helper methods... // Perhaps modify or hide a few methods (domain-related) }; 

Je suis conscient de la pratique consistant à préférer la composition à l’inheritance lors de la réutilisation d’une classe pour l’implémentation – mais il doit y avoir une limite! Si je devais tout déléguer à std :: vector, il y aurait (par mon compte) 32 fonctions de transfert!

Donc, mes questions sont … Est-ce vraiment si grave d’hériter la mise en œuvre dans de tels cas? Quels sont les risques? Y a-t-il un moyen plus sûr de le mettre en œuvre sans trop taper? Suis-je un hérétique pour utiliser l’inheritance d’implémentation? 🙂

Modifier:

Qu’en est-il de préciser que l’utilisateur ne doit pas utiliser MyContainer via un pointeur std :: vector :

 // non_api_header_file.h namespace detail { typedef std::vector MyObjectBase; } // api_header_file.h class MyContainer : public detail::MyObjectBase { // ... }; 

Les bibliothèques de boost semblent faire ce genre de choses tout le temps.

Edit 2:

L’une des suggestions était d’utiliser des fonctions gratuites. Je le montrerai ici comme pseudo-code:

 typedef std::vector MyCollection; void specialCollectionInitializer(MyCollection& c, arguments...); result specialCollectionFunction(const MyCollection& c); etc... 

Une manière plus OO de le faire:

 typedef std::vector MyCollection; class MyCollectionWrapper { public: // Constructor MyCollectionWrapper(arguments...) {construct coll_} // Access collection directly MyCollection& collection() {return coll_;} const MyCollection& collection() const {return coll_;} // Special domain-related methods result mySpecialMethod(arguments...); private: MyCollection coll_; // Other domain-specific member variables used // in conjunction with the collection. } 

Le risque se désalloue par le biais d’un pointeur vers la classe de base ( delete , delete [] , et éventuellement d’autres méthodes de désallocation). Comme ces classes ( deque , map , ssortingng , etc.) n’ont pas de dtors virtuels, il est impossible de les nettoyer correctement avec seulement un pointeur sur ces classes:

 struct BadExample : vector {}; int main() { vector* p = new BadExample(); delete p; // this is Undefined Behavior return 0; } 

Cela dit, si vous voulez vous assurer de ne jamais le faire accidentellement, il n’ya pas d’inconvénient majeur à les hériter, mais dans certains cas, c’est un gros problème. Parmi les autres inconvénients, citons l’affrontement avec les spécificités et les extensions de l’implémentation (dont certaines peuvent ne pas utiliser d’identificateurs réservés) et le traitement des interfaces surchargées (en particulier les chaînes ). Cependant, l’inheritance est prévu dans certains cas, car les adaptateurs de conteneur tels que la stack ont un membre protégé c (le conteneur sous-jacent qu’ils adaptent), et il est presque uniquement accessible à partir d’une instance de classe dérivée.

À la place de l’inheritance ou de la composition, envisagez d’écrire des fonctions libres qui prennent soit une paire d’iterators, soit une référence de conteneur, et opèrent sur cela. Pratiquement tout en est un exemple; et make_heap , pop_heap et push_heap , en particulier, sont un exemple d’utilisation de fonctions libres au lieu d’un conteneur spécifique à un domaine.

Par conséquent, utilisez les classes de conteneur pour vos types de données et appelez toujours les fonctions gratuites pour la logique de votre domaine. Mais vous pouvez toujours obtenir une certaine modularité en utilisant un typedef, ce qui vous permet à la fois de les simplifier et de fournir un seul point si une partie doit être modifiée:

 typedef std::deque Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier 

Notez que value_type et allocator peuvent changer sans affecter le code ultérieur en utilisant le typedef, et même le conteneur peut passer d’un deque à un vecteur .

Vous pouvez combiner l’inheritance privé et le mot clé “using” pour contourner la plupart des problèmes mentionnés ci-dessus: L’inheritance privé est “est-implémenté en termes de” et comme il est privé, vous ne pouvez pas placer un pointeur sur la classe de base

 #include  #include  class MySsortingng : private std::ssortingng { public: MySsortingng(std::ssortingng s) : std::ssortingng(s) {} using std::ssortingng::size; std::ssortingng fooMe(){ return std::ssortingng("Foo: ") + *this; } }; int main() { MySsortingng s("Hi"); std::cout << "MyString.size(): " << s.size() << std::endl; std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl; } 

Comme tout le monde l’a déjà dit, les conteneurs STL n’ont pas de destructeurs virtuels, de sorte que leur inheritance est au mieux dangereux. J’ai toujours considéré la programmation générique avec des modèles comme un style OO différent – un sans inheritance. Les algorithmes définissent l’interface dont ils ont besoin. Il est aussi proche de Duck Typing que vous pouvez obtenir dans un langage statique.

Quoi qu’il en soit, j’ai quelque chose à append à la discussion. La façon dont j’ai créé mes propres spécialisations de modèle est de définir des classes comme celles-ci à utiliser comme classes de base.

 template  class readonly_container_facade { public: typedef typename Container::size_type size_type; typedef typename Container::const_iterator const_iterator; virtual ~readonly_container_facade() {} inline bool empty() const { return container.empty(); } inline const_iterator begin() const { return container.begin(); } inline const_iterator end() const { return container.end(); } inline size_type size() const { return container.size(); } protected: // hide to force inherited usage only readonly_container_facade() {} protected: // hide assignment by default readonly_container_facade(readonly_container_facade const& other): : container(other.container) {} readonly_container_facade& operator=(readonly_container_facade& other) { container = other.container; return *this; } protected: Container container; }; template  class writable_container_facade: public readable_container_facade { public: typedef typename Container::iterator iterator; writable_container_facade(writable_container_facade& other) readonly_container_facade(other) {} virtual ~writable_container_facade() {} inline iterator begin() { return container.begin(); } inline iterator end() { return container.end(); } writable_container_facade& operator=(writable_container_facade& other) { readable_container_facade::operator=(other); return *this; } }; 

Ces classes exposent la même interface qu’un conteneur STL. J’ai aimé l’effet de séparer les opérations de modification et de non modification en classes de base distinctes. Cela a un très bon effet sur la const-correct. L’inconvénient est que vous devez étendre l’interface si vous souhaitez les utiliser avec des conteneurs associatifs. Je n’ai pas rencontré le besoin cependant.

Dans ce cas, l’inheritance est une mauvaise idée: les conteneurs STL n’ont pas de destructeurs virtuels, de sorte que vous pourriez rencontrer des memory leaks (en plus, cela indique que les conteneurs STL ne sont pas destinés à être hérités).

Si vous avez juste besoin d’append des fonctionnalités, vous pouvez le déclarer dans des méthodes globales, ou une classe légère avec un pointeur / référence de membre de conteneur. Bien sûr, cette méthode ne vous permet pas de masquer les méthodes: si c’est vraiment ce que vous recherchez, il n’y a pas d’autre option que de redéfinir la totalité de l’implémentation.

Mis à part les codes virtuels, la décision d’hériter contre contenir doit être une décision de conception basée sur la classe que vous créez. Vous ne devez jamais hériter des fonctionnalités de conteneur simplement parce que c’est plus simple de contenir un conteneur et d’append quelques fonctions add et remove qui semblent être des wrappers simplistes, sauf si vous pouvez dire que la classe que vous créez est une sorte de conteneur. Par exemple, une classe en classe contiendra souvent des objects d’élève, mais une classe n’est pas une sorte de liste d’étudiants pour la plupart des objectives, vous ne devriez donc pas hériter de la liste.

C’est plus facile à faire:

 typedef std::vector MyContainer; 

Les méthodes de transfert seront intégrées, de toute façon. Vous n’obtiendrez pas de meilleures performances de cette façon. En fait, vous obtiendrez probablement de moins bonnes performances.