Quand dois-je utiliser C++ private inheritance?

contrairement à protected inheritance, C++ private inheritance s'est retrouvé dans le courant dominant du développement C++. Cependant, je n'ai toujours pas trouvé un bon usage.

quand l'utilisez-vous?

103
demandé sur Community 2009-03-18 01:06:06

13 réponses

note après acceptation de la réponse: il ne s'agit pas d'une réponse complète. Lisez d'autres réponses comme ici (conceptuellement) et ici (à la fois théorique et pratique) si vous êtes intéressé par la question. C'est juste un truc de fantaisie qui peut être réalisé avec l'héritage privé. Alors qu'il est fantaisie ce n'est pas la réponse à la question.

outre le utilisation de base de l'héritage privé juste montré dans la FAQ C++ (liée dans les commentaires d'autres) vous pouvez utiliser une combinaison d'héritage privé et virtuel à sceau une classe (dans la terminologie.net) ou de faire une classe final (dans la terminologie Java). Ce n'est pas une utilisation courante, 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 que c'est un ami.

FailsToDerive ne compile pas car il doit appeler le ClassSealer constructeur directement (l'héritage virtuel), mais il ne peut pas car il est privé dans la Scellé de la classe et dans ce cas FailsToDerive n'est pas un ami de ClassSealer .


modifier

il a été indiqué dans les observations qu'il n'était pas possible à l'époque d'en faire un usage générique en utilisant le système CRTP. La norme C++11 supprime cette limitation en fournissant une syntaxe différente pour les arguments de template befriend:

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

bien sûr, tout cela est discutable, puisque C++11 fournit un final mot-clé contextuel pour exactement ce but:

class Sealed final // ...
53
répondu David Rodríguez - dribeas 2017-05-23 10:31:29

je l'utilise tout le temps. Quelques exemples sur le dessus 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 la substituabilité Liskov est brisée, alors que la composition signifierait écrire un tas de fonctions de transit.
  • quand je veux dériver d'une classe de béton sans destructeur virtuel. L'héritage public inviterait des clients supprimer par un pointeur vers la base, en invoquant un comportement non défini.

un exemple typique est dérivé 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 la mise en œuvre du modèle D'Adaptateur, hériter en privé de la classe adaptée évite d'avoir à transmettre à une instance fermée.
  • pour implémenter une interface privée. Cela revient souvent avec le modèle de L'Observateur. Typiquement ma classe D'Observateur, MyClass say, s'abonne avec quelque sujet. Ensuite, seul MyClass a besoin de faire la conversion MyClass - > Observer. Le reste du système n'a pas besoin de le savoir, donc l'héritage privé est indiqué.
130
répondu fizzer 2017-11-15 10:42:02

l'usage canonique de l'héritage privé est la relation" mise en œuvre en termes de "(grâce à Scott Meyers' 'Effective C++' pour cette formulation). En d'autres termes, l'interface externe de la classe hériter n'a pas de relation (visible) avec la classe héritée, mais elle l'utilise en interne pour implémenter sa fonctionnalité.

26
répondu Harper Shelby 2009-03-17 22:09:07

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 faites que cette interface privée de sorte que la classe elle-même doit enregistrer et seul l'objet spécifique que son enregistré avec 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 à travers la FooInterface interface, alors que les autres classes externes ne le peuvent pas. Il s'agit d'un grand modèle pour la manipulation de callbacks spécifiques qui sont définis comme des interfaces.

18
répondu Daemin 2009-03-24 09:52:53

je pense que la section critique de la C++ FAQ Lite est:

une utilisation légitime et à long terme pour l'héritage privé est quand vous voulez construire une classe Fred qui utilise le code dans une classe Wilma, et le code de classe Wilma doit invoquer les fonctions de membre de votre nouvelle classe, Fred. Dans ce cas, Fred appelle les non-virtuoses dans Wilma, et Wilma appelle (généralement des virtuoses pures) en soi, qui sont dépassé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
répondu Bill the Lizard 2009-03-18 00:15:01

que je trouve très utile pour les interfaces (viz. abstract classes) que j'hérite là où je ne veux pas que d'autres codes touchent l'interface (seulement la classe hériter).

[édité dans un exemple]

prendre le exemple lié à ci-dessus. Disant que

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

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

l'héritage privé n'est pas mauvais; il est juste plus cher à maintenir, car il augmente la probabilité que quelqu'un va changer quelque chose qui va briser votre code.

commente sur l'effet désiré des programmeurs ayant besoin pour répondre à nos exigences d'interface, ou de briser 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éritière peut toucher (et les amis).

[modifié dans un autre exemple]

Cette page traite brièvement des interfaces privées (à partir d'un autre angle).

4
répondu bias 2009-03-19 21:32:44

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

class BigClass;

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

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

alors si Quelqu'un a besoin d'accéder à BigClass, il peut static_cast<BigClass *>(this) . Il n'est pas nécessaire qu'un membre supplémentaire occupe de la place.

2
répondu 2009-03-25 00:41:55

si la classe dérivée - doit réutiliser le code et - tu ne peux pas changer de classe de base et - protège ses méthodes en utilisant les membres de la base sous une serrure.

alors vous devez utiliser l'héritage privé, sinon vous avez le danger de méthodes de base déverrouillées exportées via cette classe dérivée.

1
répondu sportswithin.com 2011-04-18 23:47:11

parfois, il pourrait être une alternative à agrégation , par exemple si vous voulez agrégation, mais avec un comportement modifié de l'entité agrégable (dépassant les fonctions virtuelles).

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

1
répondu bayda 2017-11-05 20:25:48

j'ai trouvé une belle application pour l'héritage privé, bien qu'elle ait un usage limité.

problème à résoudre

supposons qu'on vous donne 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 est d'implémenter cette API en utilisant C++.

C-ish approche

bien sûr, nous pourrions choisir un style de mise en œuvre c-ish comme cela:

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;
}

But il y a plusieurs inconvénients:

  • gestion manuelle des ressources (p.ex. de la mémoire)
  • il est facile de mettre en place le struct faux
  • il est facile d'oublier la libération des ressources lors de la libération du struct
  • c'est C-ish

C++ Approche

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

introduction à la gestion automatisée des ressources

les problèmes ci-dessus sont essentiellement tous 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 à ce qui 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 ceci nous avons remédié à tout ce qui précède problème. Mais un client peut encore oublier les setters de WidgetImpl et attribuer 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 à mouler 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);
}

This solution résout tous les problèmes. Aucune gestion manuelle de la mémoire et Widget est joliment encapsulé de sorte que WidgetImpl n'a plus aucun membre de données publiques. Il rend l'application facile à utiliser correctement et dur (impossible?) pour une utilisation incorrecte.

les extraits de code forment un compilant exemple sur Coliru .

1
répondu Matthäus Brandl 2018-04-30 06:47:14

Privé de l'Héritage à être utilisé lorsque la relation n'est pas "un", Mais la Nouvelle de la classe peuvent être "mis en œuvre en terme de classe existante", ou la nouvelle classe "travail" comme de la classe existante.

exemple de "C++ normes de codage par Andrei Alexandrescu, Herb Sutter" :- Considérez que deux classes carré et Rectangle ont chacune des fonctions virtuelles pour définir leur hauteur et la largeur. Alors le carré ne peut pas hériter correctement du Rectangle, parce que le code qui utilise un Rectangle modifiable supposons que SetWidth ne change pas la hauteur (que Rectangle documente explicitement ce Contrat ou non), alors que Square::SetWidth ne peut pas préserver ce contrat et sa propre squareness invariante en même temps. Mais le Rectangle ne peut pas hériter correctement du Carré non plus, si les clients du Carré supposent par exemple que la superficie D'un carré est sa largeur au carré, ou s'ils s'appuient sur une autre propriété qui ne tient pas pour des Rectangles.

Un carré "est-un" rectangle (mathématiquement) mais un Carré n'est pas un Rectangle (comportemental). Par conséquent, au lieu de" is-A", Nous préférons dire" works-like-a "(ou, si vous préférez," usable-as-a") pour rendre la description moins sujette à des malentendus.

0
répondu Rahul_cs12 2015-11-24 11:07:05

une classe possède 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 sauvegarder dans un fichier - DTO si vous préférez). Le reste est mieux fait en termes de type agrégat. Cela est particulièrement vrai si vous êtes const correcte. Considérons:

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 pourriez juste stocker des collections de cache dans des conteneurs et regardez sur 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, il vaut la peine de le rechercher.

l'héritage privé peut presque toujours être modelé par un membre (en stockant une référence à la base si nécessaire). C'est juste pas toujours la peine de modèle; parfois, l'héritage est la plus efficace et la représentation.

0
répondu lorro 2017-04-12 22:05:06

ce n'est pas parce que C++ a une fonctionnalité qu'elle doit être utilisée.

, je dirais que vous ne devriez pas l'utiliser du tout.

si vous l'utilisez de toute façon, vous violez l'encapsulation et réduisez la cohésion. Vous mettez des données dans une classe, et ajoutez des méthodes qui manipulent les données dans une autre.

comme les autres fonctionnalités de C++, il peut être utilisé pour obtenir des effets secondaires tels que l'étanchéité d'une classe (comme mentionné dans la réponse de dribeas), mais cela ne fait pas une bonne caractéristique.

-2
répondu hasen 2009-03-24 16:42:08