87 votes

Est-il possible d'hériter de l'implémentation des conteneurs STL, plutôt que de déléguer ?

J'ai une classe qui adapte std::vector pour modéliser un conteneur d'objets spécifiques au domaine. Je veux exposer la plupart de l'API std::vector à l'utilisateur, afin qu'il puisse utiliser des méthodes familières (size, clear, at, etc...) et des algorithmes standards sur le conteneur. Cela semble être un modèle récurrent pour moi dans mes conceptions :

class MyContainer : public std::vector<MyObject>
{
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'héritage 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 (d'après mes calculs) 32 fonctions de transfert !

Donc mes questions sont... Est-il vraiment si mauvais d'hériter de l'implémentation dans de tels cas ? Quels sont les risques ? Existe-t-il un moyen plus sûr d'implémenter ceci sans avoir à taper autant ? Suis-je un hérétique pour avoir utilisé l'héritage de l'implémentation :)

Editar:

Pourquoi ne pas 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<MyObject> 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 libres. Je vais le montrer ici sous forme de pseudo-code :

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

Une façon plus OO de le faire :

typedef std::vector<MyObject> 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.
}

6 votes

Oh goody ! Une autre chance de pousser mon blog à punchlet.wordpress.com - En gros, écrivez des fonctions libres et oubliez l'approche de l'enveloppe "plus OO". Ce n'est pas plus OO - si c'était le cas, il faudrait utiliser l'héritage, ce que vous ne devriez probablement pas faire dans ce cas. Rappelez-vous OO != classe.

2 votes

@Neil : Mais, mais les fonctions globales sont diaboliques !!! Tout est un objet ! ;)

4 votes

Ils ne seront pas globaux si vous les placez dans un espace de nom.

79voto

Le risque est de désallouer à travers un pointeur vers la classe de base ( supprimer , supprimer[] et éventuellement d'autres méthodes de désallocation). Puisque ces classes ( deque , carte , chaîne de caractères ) n'ont pas de dtors virtuels, il est impossible de les nettoyer correctement avec seulement un pointeur sur ces classes :

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

Cela dit, si si vous êtes prêt à faire en sorte de ne jamais le faire accidentellement, il n'y a pas d'inconvénient majeur à en hériter - mais dans certains cas, c'est un gros "si". Parmi les autres inconvénients, citons le conflit avec les implémentations spécifiques et les extensions (dont certaines n'utilisent pas les identifiants réservés) et la gestion d'interfaces trop volumineuses ( chaîne de caractères en particulier). Cependant, l'héritage est prévu dans certains cas, comme les adaptateurs de conteneurs tels que pile 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.

Au lieu de l'héritage ou de la composition, envisager d'écrire des fonctions libres qui prennent soit une paire d'itérateurs, soit une référence de conteneur, et opèrent sur ces éléments. Pratiquement tout <algorithme> est un exemple de ceci ; et make_heap , pop_heap y push_heap en particulier, sont un exemple d'utilisation de fonctions libres au lieu d'un conteneur spécifique au domaine.

Ainsi, utilisez les classes de conteneurs pour vos types de données, et appelez toujours les fonctions libres pour votre logique spécifique au domaine. Mais vous pouvez toujours obtenir une certaine modularité en utilisant un typedef, qui vous permet à la fois de simplifier leur déclaration et de fournir un point unique si une partie d'entre eux doit être modifiée :

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

Remarquez que le value_type et l'allocateur peuvent changer sans affecter le code ultérieur utilisant le typedef, et même le conteneur peut changer d'un deque à un vecteur .

41voto

Ben Points 444

Vous pouvez combiner l'héritage privé et le mot-clé "using" pour contourner la plupart des problèmes mentionnés ci-dessus : L'héritage privé est 'is-implemented-in-terms-of' et comme il est privé, vous ne pouvez pas détenir un pointeur vers la classe de base.

#include <string>
#include <iostream>

class MyString : private std::string
{
public:
    MyString(std::string s) : std::string(s) {}
    using std::string::size;
    std::string fooMe(){ return std::string("Foo: ") + *this; }
};

int main()
{
    MyString s("Hi");
    std::cout << "MyString.size(): " << s.size() << std::endl;
    std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl;
}

3 votes

Je ne peux m'empêcher de mentionner que private l'héritage reste l'héritage et constitue donc une relation plus forte que la composition. Notamment, cela signifie que changer l'implémentation de votre classe va nécessairement rompre la compatibilité binaire.

8 votes

L'héritage privé et les membres de données privés rompent tous deux la compatibilité binaire lorsqu'ils changent, et, à l'exception des amis (qui devraient être peu nombreux), il n'est généralement pas difficile de passer de l'un à l'autre --- ce qui est utilisé est souvent dicté par des détails d'implémentation. Voir aussi l'idiome "base-from-member".

0 votes

Pour les curieux - Base-from-Member Idiom : fr.wikibooks.org/wiki/More_C%2B%2B_Idioms/Base-from-Member

15voto

D.Shawley Points 30324

Comme tout le monde l'a déjà dit, les conteneurs STL n'ont pas de destructeurs virtuels et en hériter n'est pas sûr, au mieux. J'ai toujours considéré la programmation générique avec des modèles comme un style différent d'OO, sans héritage. Les algorithmes définissent l'interface dont ils ont besoin. C'est aussi proche de Dactylographie du canard que vous pouvez obtenir dans un langage statique.

Quoi qu'il en soit, j'ai quelque chose à ajouter à la discussion. La façon dont j'ai créé mes propres spécialisations de modèles auparavant est de définir des classes comme les suivantes à utiliser comme classes de base.

template <typename Container>
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 <typename Container>
class writable_container_facade: public readable_container_facade<Container> {
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<Container>::operator=(other);
        return *this;
    }
};

Ces classes exposent la même interface qu'un conteneur STL. J'ai aimé l'effet de la séparation des opérations de modification et de non-modification dans des classes de base distinctes. Cela a un effet très agréable sur la const-correctness. Le seul inconvénient est que vous devez étendre l'interface si vous voulez les utiliser avec des conteneurs associatifs. Je n'en ai pas eu besoin jusqu'à présent.

0 votes

Joli ! Je pourrais l'utiliser. Mais d'autres ont fait repenser à l'idée d'adapter les conteneurs, alors peut-être que je ne l'utiliserai pas. :)

0 votes

Cela dit, une programmation lourde de modèles peut conduire à un code spaghetti tout aussi mauvais, à des bibliothèques massives, à une mauvaise isolation des fonctionnalités et à des erreurs de compilation inintelligibles.

5voto

Jherico Points 12554

Mis à part les doreurs virtuels, la décision d'hériter ou de contenir doit être une décision de conception basée sur la classe que vous créez. Vous ne devriez jamais hériter de la fonctionnalité du conteneur simplement parce que c'est plus facile que de contenir un conteneur et d'ajouter quelques fonctions d'ajout et de retrait qui semblent être des enveloppes simplistes. sauf si vous pouvez définitivement dire que la classe que vous créez est une sorte de conteneur. Par exemple, une classe de cours contient souvent des objets élèves, mais une classe n'est pas une sorte de liste d'élèves dans la plupart des cas, donc vous ne devriez pas hériter de list.

5voto

stijn Points 13405

Dans ce cas, l'héritage est une mauvaise idée : les conteneurs STL n'ont pas de destructeurs virtuels et vous risquez d'avoir des fuites de mémoire (de plus, c'est une indication que les conteneurs STL ne sont pas censés être hérités en premier lieu).

Si vous avez juste besoin d'ajouter une fonctionnalité, vous pouvez la déclarer dans des méthodes globales, ou dans une classe légère avec un pointeur/référence de membre du conteneur. Bien entendu, cela ne vous permet pas de cacher des méthodes : si c'est vraiment ce que vous recherchez, il n'y a pas d'autre option que de redéclarer l'implémentation entière.

0 votes

Vous pouvez toujours cacher des méthodes en ne les déclarant pas dans l'en-tête mais uniquement dans l'implémentation, en en faisant des méthodes statiques non publiques dans une classe fictive (à partir de laquelle vous pouvez donner de l'amitié, et cela fonctionne pour les modèles qui doivent être uniquement en en-tête), ou en les plaçant dans un espace de noms "détail" ou de nom similaire. (Ces trois méthodes fonctionnent tout aussi bien que les méthodes privées conventionnelles).

0 votes

Je ne comprends pas comment vous pensez pouvoir cacher une méthode de 'vector' en ne la déclarant pas dans votre en-tête. Elle est déjà déclarée dans vector.

0 votes

Jherico : C'est à moi que tu parles ou à stijn ? Dans tous les cas, je pense que tu as mal compris l'un d'entre nous.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X