web-dev-qa-db-fra.com

Quand dois-je utiliser l'héritage privé C ++?

Contrairement à l'héritage protégé, l'héritage privé C++ a trouvé sa place dans le développement C++ traditionnel. Cependant, je n'en ai toujours pas trouvé une bonne utilisation.

Quand les utilisez-vous?

110
Sasha

Remarque après acceptation de la réponse: Ce n'est PAS une réponse complète. Lisez d'autres réponses comme ici (conceptuellement) et ici (à la fois théorique et pratique) si vous sont intéressés par la question. C'est juste une astuce fantaisiste qui peut être réalisée avec l'héritage privé. Bien que ce soit fantaisie ce n'est pas la réponse à la question.

Outre l'utilisation de base de l'héritage privé uniquement indiqué dans C++ FAQ (lié dans les commentaires des autres), vous pouvez utiliser une combinaison d'héritage privé et virtuel pour sceller a (dans la terminologie .NET) ou pour créer une classe final (dans Java). Ce n'est pas un usage courant, mais de toute façon je l'ai trouvé intéressant :

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

Scellé peut être instancié. Il dérive de ClassSealer et peut appeler le constructeur privé directement car c'est un ami.

FailsToDerive ne se compilera pas car il doit appeler directement le constructeur ClassSealer (exigence d'héritage virtuel), mais il ne peut pas car il est privé dans le Sealed class et dans ce cas FailsToDerive n'est pas un ami de ClassSealer.


[~ # ~] modifier [~ # ~]

Il a été mentionné dans les commentaires que cela ne pouvait pas être rendu générique à l'époque à l'aide du CRTP. La norme C++ 11 supprime cette limitation en fournissant une syntaxe différente pour se lier d'amitié avec les arguments de modèle:

template <typename T>
class Seal {
   friend T;          // not: friend class T!!!
   Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...

Bien sûr, tout cela est théorique, car C++ 11 fournit un mot-clé contextuel final à cet effet:

class Sealed final // ...

Je l'utilise tout le temps. Quelques exemples du haut de ma tête:

  • Quand je veux exposer une partie mais pas la totalité de l'interface d'une classe de base. L'héritage public serait un mensonge, car substituabilité Liskov est cassé, tandis que la composition signifierait l'écriture d'un tas de fonctions de transfert.
  • Quand je veux dériver d'une classe concrète sans destructeur virtuel. L'héritage public inviterait les clients à supprimer via un pointeur vers la base, invoquant un comportement non défini.

Un exemple typique dérive en privé d'un conteneur STL:

class MyVector : private vector<int>
{
public:
    // Using declarations expose the few functions my clients need 
    // without a load of forwarding functions. 
    using vector<int>::Push_back;
    // etc...  
};
  • Lors de l'implémentation du modèle d'adaptateur, l'héritage privé de la classe Adapted évite d'avoir à transférer vers une instance incluse.
  • Pour implémenter une interface privée. Cela revient souvent avec le modèle Observer. Typiquement, ma classe Observer, MyClass dit, s'abonne à un sujet. Ensuite, seule MyClass doit effectuer la conversion MyClass -> Observer. Le reste du système n'a pas besoin de le savoir, donc l'héritage privé est indiqué.
133
fizzer

L'utilisation canonique de l'héritage privé est la relation "implémentée en termes de" (grâce au "C++ efficace" de Scott Meyers pour cette formulation). En d'autres termes, l'interface externe de la classe héritée n'a aucune relation (visible) avec la classe héritée, mais elle l'utilise en interne pour implémenter ses fonctionnalités.

27
Harper Shelby

Une utilisation utile de l'héritage privé est lorsque vous avez une classe qui implémente une interface, qui est ensuite enregistrée avec un autre objet. Vous rendez cette interface privée afin que la classe elle-même doive s'enregistrer et seul l'objet spécifique auprès duquel elle est enregistrée peut utiliser ces fonctions.

Par exemple:

class FooInterface
{
public:
    virtual void DoSomething() = 0;
};

class FooUser
{
public:
    bool RegisterFooInterface(FooInterface* aInterface);
};

class FooImplementer : private FooInterface
{
public:
    explicit FooImplementer(FooUser& aUser)
    {
        aUser.RegisterFooInterface(this);
    }
private:
    virtual void DoSomething() { ... }
};

Par conséquent, la classe FooUser peut appeler les méthodes privées de FooImplementer via l'interface FooInterface, contrairement aux autres classes externes. Il s'agit d'un excellent modèle pour gérer des rappels spécifiques définis comme des interfaces.

21
Daemin

Je pense que la section critique du C++ FAQ Lite est:

Une utilisation légitime et à long terme de l'héritage privé est lorsque vous souhaitez créer une classe Fred qui utilise du code dans une classe Wilma, et que le code de la classe Wilma doit appeler les fonctions membres de votre nouvelle classe, Fred. Dans ce cas, Fred appelle des non-virtuels dans Wilma, et Wilma appelle (généralement des purs virtuels) en soi, qui sont remplacés par Fred. Ce serait beaucoup plus difficile à faire avec la composition.

En cas de doute, vous devriez préférer la composition à l'héritage privé.

17
Bill the Lizard

Je le trouve utile pour les interfaces (c'est-à-dire les classes abstraites) dont j'hérite lorsque je ne veux pas qu'un autre code touche l'interface (uniquement la classe héritée).

[édité dans un exemple]

Prenez le exemple lié à ce qui précède. Dire que

[...] classe Wilma doit invoquer les fonctions membres de votre nouvelle classe, Fred.

c'est dire que Wilma demande à Fred de pouvoir invoquer certaines fonctions membres, ou plutôt il dit que Wilma est une interface . Par conséquent, comme mentionné dans l'exemple

l'héritage privé n'est pas mauvais; c'est juste plus cher à entretenir, car cela augmente la probabilité que quelqu'un change quelque chose qui cassera votre code.

des commentaires sur l'effet souhaité des programmeurs devant répondre à nos exigences d'interface, ou casser le code. Et, puisque fredCallsWilma () est protégé, seuls les amis et les classes dérivées peuvent le toucher, c'est-à-dire une interface héritée (classe abstraite) que seule la classe héritée peut toucher (et les amis).

[édité dans un autre exemple]

Cette page discute brièvement des interfaces privées (sous un autre angle encore).

4
bias

Parfois, je trouve utile d'utiliser l'héritage privé lorsque je souhaite exposer une interface plus petite (par exemple une collection) dans l'interface d'une autre, où l'implémentation de la collection nécessite un accès à l'état de la classe exposante, de manière similaire aux classes internes de Java.

class BigClass;

struct SomeCollection
{
    iterator begin();
    iterator end();
};

class BigClass : private SomeCollection
{
    friend struct SomeCollection;
    SomeCollection &GetThings() { return *this; }
};

Ensuite, si SomeCollection doit accéder à BigClass, il peut static_cast<BigClass *>(this). Pas besoin d'avoir un membre de données supplémentaire prenant de la place.

2
Robert Tuck

J'ai trouvé une belle application d'héritage privé, bien qu'elle ait une utilisation limitée.

Problème à résoudre

Supposons que vous disposez de l'API C suivante:

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct
    {
        /* raw owning pointer, it's C after all */
        char const * name;

        /* more variables that need resources
         * ...
         */
    } Widget;

    Widget const * loadWidget();

    void freeWidget(Widget const * widget);

#ifdef __cplusplus
} // end of extern "C"
#endif

Maintenant, votre travail consiste à implémenter cette API à l'aide de C++.

Approche C-ish

Bien sûr, nous pourrions choisir un style d'implémentation C-ish comme ceci:

Widget const * loadWidget()
{
    auto result = std::make_unique<Widget>();
    result->name = strdup("The Widget name");
    // More similar assignments here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    free(result->name);
    // More similar manual freeing of resources
    delete widget;
}

Mais il y a plusieurs inconvénients:

  • Gestion manuelle des ressources (par exemple la mémoire)
  • Il est facile de configurer le struct mal
  • Il est facile d'oublier la libération des ressources lors de la libération de struct
  • C'est C-ish

Approche C++

Nous sommes autorisés à utiliser C++, alors pourquoi ne pas utiliser ses pleins pouvoirs?

Présentation de la gestion automatisée des ressources

Les problèmes ci-dessus sont essentiellement liés à la gestion manuelle des ressources. La solution qui vient à l'esprit est d'hériter de Widget et d'ajouter une instance de gestion des ressources à la classe dérivée WidgetImpl pour chaque variable:

class WidgetImpl : public Widget
{
public:
    // Added bonus, Widget's members get default initialized
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

private:
    std::string m_nameResource;
};

Cela simplifie la mise en œuvre comme suit:

Widget const * loadWidget()
{
    auto result = std::make_unique<WidgetImpl>();
    result->setName("The Widget name");
    // More similar setters here
    return result.release();
}

void freeWidget(Widget const * const widget)
{
    // No virtual destructor in the base class, thus static_cast must be used
    delete static_cast<WidgetImpl const *>(widget);
}

Comme cela, nous avons résolu tous les problèmes ci-dessus. Mais un client peut encore oublier les setters de WidgetImpl et les affecter directement aux membres Widget.

L'héritage privé entre en scène

Pour encapsuler les membres Widget, nous utilisons l'héritage privé. Malheureusement, nous avons maintenant besoin de deux fonctions supplémentaires pour lancer entre les deux classes:

class WidgetImpl : private Widget
{
public:
    WidgetImpl()
        : Widget()
    {}

    void setName(std::string newName)
    {
        m_nameResource = std::move(newName);
        name = m_nameResource.c_str();
    }

    // More similar setters to follow

    Widget const * toWidget() const
    {
        return static_cast<Widget const *>(this);
    }

    static void deleteWidget(Widget const * const widget)
    {
        delete static_cast<WidgetImpl const *>(widget);
    }

private:
    std::string m_nameResource;
};

Cela nécessite les adaptations suivantes:

Widget const * loadWidget()
{
    auto widgetImpl = std::make_unique<WidgetImpl>();
    widgetImpl->setName("The Widget name");
    // More similar setters here
    auto const result = widgetImpl->toWidget();
    widgetImpl.release();
    return result;
}

void freeWidget(Widget const * const widget)
{
    WidgetImpl::deleteWidget(widget);
}

Cette solution résout tous les problèmes. Aucune gestion manuelle de la mémoire et Widget est bien encapsulé de sorte que WidgetImpl n'a plus de membres de données publiques. Il rend l'implémentation facile à utiliser correctement et difficile (impossible?) À mal utiliser.

Les extraits de code forment un exemple de compilation sur Colir .

1
Matthäus Brandl

Si la classe dérivée - doit réutiliser le code et - vous ne pouvez pas changer la classe de base et - protège ses méthodes en utilisant les membres de la base sous un verrou.

alors vous devez utiliser l'héritage privé, sinon vous risquez d'exporter des méthodes de base déverrouillées via cette classe dérivée.

1
sportswithin.com

Parfois, cela pourrait être une alternative à agrégation, par exemple si vous voulez l'agrégation mais avec un comportement modifié de l'entité agrégable (remplaçant les fonctions virtuelles).

Mais vous avez raison, il n'a pas beaucoup d'exemples du monde réel.

1
bayda

Héritage privé à utiliser lorsque la relation n'est pas "est un", mais une nouvelle classe peut être "implémentée en termes de classe existante" ou une nouvelle classe "fonctionner comme" une classe existante.

exemple de "normes de codage C++ par Andrei Alexandrescu, Herb Sutter": - Considérez que deux classes Square et Rectangle ont chacune des fonctions virtuelles pour définir leur hauteur et leur largeur. Ensuite, Square ne peut pas hériter correctement de Rectangle, car le code qui utilise un rectangle modifiable supposera que SetWidth ne modifie pas la hauteur (que Rectangle documente explicitement ce contrat ou non), tandis que Square :: SetWidth ne peut pas conserver ce contrat et sa propre invariance d'équerrage à le même temps. Mais Rectangle ne peut pas hériter correctement de Square non plus, si les clients de Square supposent par exemple que la surface d'un carré est sa largeur au carré, ou s'ils s'appuient sur une autre propriété qui ne s'applique pas aux rectangles.

Un carré "est-un" rectangle (mathématiquement) mais un carré n'est pas un rectangle (comportemental). Par conséquent, au lieu de "est-a", nous préférons dire "fonctionne comme un" (ou, si vous préférez, "utilisable comme un") pour rendre la description moins sujette à des malentendus.

0
Rahul_cs12

Une classe contient un invariant. L'invariant est établi par le constructeur. Cependant, dans de nombreuses situations, il est utile d'avoir une vue de l'état de représentation de l'objet (que vous pouvez transmettre sur le réseau ou enregistrer dans un fichier - DTO si vous préférez). REST est mieux fait en termes de AggregateType. Cela est particulièrement vrai si vous avez raison. Considérez:

struct QuadraticEquationState {
   const double a;
   const double b;
   const double c;

   // named ctors so aggregate construction is available,
   // which is the default usage pattern
   // add your favourite ctors - throwing, try, cps
   static QuadraticEquationState read(std::istream& is);
   static std::optional<QuadraticEquationState> try_read(std::istream& is);

   template<typename Then, typename Else>
   static std::common_type<
             decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
             decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
   if_read(std::istream& is, Then then, Else els);
};

// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);

// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);

struct QuadraticEquationCache {
   mutable std::optional<double> determinant_cache;
   mutable std::optional<double> x1_cache;
   mutable std::optional<double> x2_cache;
   mutable std::optional<double> sum_of_x12_cache;
};

class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
                          private QuadraticEquationCache {
public:
   QuadraticEquation(QuadraticEquationState); // in general, might throw
   QuadraticEquation(const double a, const double b, const double c);
   QuadraticEquation(const std::string& str);
   QuadraticEquation(const ExpressionTree& str); // might throw
}

À ce stade, vous pouvez simplement stocker des collections de cache dans des conteneurs et les rechercher lors de la construction. Pratique s'il y a un vrai traitement. Notez que le cache fait partie du QE: les opérations définies sur le QE peuvent signifier que le cache est partiellement réutilisable (par exemple, c n'affecte pas la somme); pourtant, quand il n'y a pas de cache, cela vaut la peine de le rechercher.

L'héritage privé peut presque toujours être modélisé par un membre (stockant la référence à la base si nécessaire). Ce n'est pas toujours la peine de modéliser de cette façon; parfois l'héritage est la représentation la plus efficace.

0
lorro