web-dev-qa-db-fra.com

Est-il correct 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/elle puisse utiliser des méthodes familières (size, clear, at, etc ...) et des algorithmes standard sur le conteneur. Cela semble être un motif récurrent dans mes créations:

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 connais la pratique de 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 (à mon avis) 32 fonctions de transfert!

Donc mes questions sont ... Est-ce 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 de l'implémenter sans trop taper? Suis-je un hérétique pour l'utilisation de l'héritage 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<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.

Modifier 2:

L'une des suggestions était d'utiliser des fonctions gratuites. Je vais le montrer ici en 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.
}
72
Emile Cormier

Le risque est la désallocation via un pointeur vers la classe de base ( delete , delete [ ] , et éventuellement d'autres méthodes de désallocation). Puisque ces classes ( deque , map , les chaînes , etc.) n'ont pas de dtors virtuels, il est impossible de les nettoyer correctement avec seulement un pointeur vers ces classes:

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

Cela dit, si vous êtes prêt à vous assurer de ne jamais le faire accidentellement, il y a peu d'inconvénients majeurs à les hériter, mais dans certains cas, c'est un gros problème. si. D'autres inconvénients incluent le conflit avec les spécificités et extensions d'implémentation (dont certaines peuvent ne pas utiliser d'identificateurs réservés) et le traitement des interfaces gonflées ( chaîne en particulier). Cependant, l'héritage est prévu dans certains cas, car les adaptateurs de conteneur comme 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.

Au lieu d'héritage ou de composition, pensez à écrire des fonctions libres qui prennent soit une paire d'itérateurs soit une référence de conteneur, et opèrent sur cela. Pratiquement tout <algorithme> en est un exemple; et make_heap , pop_heap , et Push_heap , en particulier, est un exemple d'utilisation de fonctions gratuites au lieu d'un conteneur spécifique au domaine.

Utilisez donc les classes de conteneur pour vos types de données et appelez toujours les fonctions gratuites pour votre logique propre 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 fournit un point unique si une partie d'entre eux doit changer:

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

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

72
Roger Pate

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-implement-in-terms-of" et comme il est privé, vous ne pouvez pas conserver de pointeur sur 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;
}
31
Ben

Comme tout le monde l'a déjà dit, les conteneurs STL n'ont pas de destructeurs virtuels, donc hériter d'eux n'est au mieux pas sûr. J'ai toujours considéré la programmation générique avec des modèles comme un style différent de OO - un sans héritage. Les algorithmes définissent l'interface dont ils ont besoin. C'est aussi proche de Duck Typing 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èle précédemment 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 séparer les opérations de modification et de non-modification en classes de base distinctes. Cela a un effet vraiment agréable sur la constance. Le seul inconvénient est que vous devez étendre l'interface si vous souhaitez les utiliser avec des conteneurs associatifs. Je n'ai cependant pas rencontré le besoin.

14
D.Shawley

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

Si vous avez juste besoin d'ajouter des fonctionnalités, vous pouvez les déclarer dans des méthodes globales ou dans une classe légère avec un pointeur/référence de membre de conteneur. Ce cours 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éclarer l'intégralité de l'implémentation.

5
stijn

Les dtors virtuels mis à part, la décision d'hériter par rapport à 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é de conteneur simplement parce que c'est plus facile que de contenir un conteneur et d'ajouter quelques fonctions d'ajout et de suppression qui ressemblent à des wrappers simplistes sauf vous pouvez définitivement dire que la classe que vous créez est une sorte de le conteneur. Par exemple, une classe en classe contient souvent des objets étudiants, mais une classe n'est pas une sorte de liste d'étudiants dans la plupart des cas, vous ne devriez donc pas hériter de la liste.

3
Jherico

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

1
Charles Eli Cheese

C'est plus simple à faire:

typedef std::vector<MyObject> MyContainer;
1
Martin York