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

116

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 l'utilisez vous?


la source

Réponses:

60

Note 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éoriques et pratiques) si vous êtes intéressé par la question. C'est juste une astuce sophistiquée 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 la FAQ C ++ (liée dans les commentaires d'autres personnes), vous pouvez utiliser une combinaison d'héritage privé et virtuel pour sceller une classe (dans la terminologie .NET) ou pour rendre une classe finale (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
};

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

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


ÉDITER

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 sans objet, car C ++ 11 fournit un finalmot clé contextuel exactement dans ce but:

class Sealed final // ...
David Rodríguez - Dribeas
la source
C'est une excellente technique. J'écrirai une entrée de blog là-dessus.
1
Question: si nous n'utilisions pas l'héritage virtuel, alors FailsToDerive compilerait. Correct?
4
+1. @Sasha: Correct, l'héritage virtuel est nécessaire car la classe la plus dérivée appelle toujours directement les constructeurs de toutes les classes virtuellement héritées, ce qui n'est pas le cas avec l'héritage simple.
j_random_hacker
5
Cela peut être rendu générique, sans créer de ClassSealer personnalisé pour chaque classe que vous souhaitez sceller! Vérifiez-le: class ClassSealer {protected: ClassSealer () {}}; c'est tout.
+1 Iraimbilanja, très cool! BTW J'ai vu votre commentaire précédent (maintenant supprimé) sur l'utilisation du CRTP: je pense que cela devrait en fait fonctionner, il est juste difficile d'obtenir la bonne syntaxe pour les amis modèles. Mais dans tous les cas, votre solution non-modèle est beaucoup plus géniale :)
j_random_hacker
138

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é de Liskov est brisée, alors que la composition signifierait écrire un tas de fonctions de transmission.
  • 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, en invoquant un comportement non défini.

Un exemple typique est la dérivation privée 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 arrive souvent avec le modèle d'observateur. Typiquement, ma classe Observer, dit MyClass, s'abonne avec un sujet. Ensuite, seul 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é.
pétillant
la source
4
@Krsna: En fait, je ne pense pas. Il n'y a qu'une seule raison ici: la paresse, à part la dernière, qui serait plus délicate à contourner.
Matthieu M.
11
Pas tellement de paresse (sauf si vous le pensez dans le bon sens). Cela permet la création de nouvelles surcharges de fonctions qui ont été exposées sans aucun travail supplémentaire. Si en C ++ 1x, ils ajoutent 3 nouvelles surcharges push_back, les MyVectorobtient gratuitement.
David Stone
@DavidStone, ne pouvez-vous pas le faire avec une méthode de modèle?
Julien__
5
@Julien__: Oui, vous pouvez écrire template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }ou vous pouvez écrire en utilisant Base::f;. Si vous voulez la plupart des fonctionnalités et de flexibilité que l' héritage privé et une usingdéclaration que vous donne, vous avez ce monstre pour chaque fonction (et ne pas oublier constet volatilesurcharges!).
David Stone
2
Je dis la plupart des fonctionnalités parce que vous invoquez toujours un constructeur de mouvement supplémentaire qui n'est pas présent dans la version de l'instruction using. En général, vous vous attendez à ce que cela soit optimisé, mais la fonction pourrait théoriquement renvoyer un type non mobile par valeur. Le modèle de fonction de transfert a également une instanciation de modèle supplémentaire et une profondeur constexpr. Cela peut entraîner l'exécution de votre programme dans les limites d'implémentation.
David Stone
31

L'usage canonique de l'héritage privé est la relation «implémentée en termes de» (merci à «Effective C ++» de Scott Meyers pour cette formulation). En d'autres termes, l'interface externe de la classe héritière n'a aucune relation (visible) avec la classe héritée, mais elle l'utilise en interne pour implémenter ses fonctionnalités.

Harper Shelby
la source
6
Cela peut valoir la peine de mentionner l'une des raisons pour lesquelles il est utilisé dans ce cas: Cela permet d'effectuer l'optimisation de la classe de base vide, ce qui ne se produira pas si la classe avait été membre au lieu d'une classe de base.
jalf
2
son utilisation principale est de réduire la consommation d'espace là où cela compte vraiment, par exemple dans les classes de chaînes contrôlées par politique ou dans les paires compressées. en fait, boost :: compress_pair utilisait l'héritage protégé.
Johannes Schaub - litb
jalf: Hé, je ne m'en suis pas rendu compte. Je pensais que l'héritage non public était principalement utilisé comme hack lorsque vous avez besoin d'accéder aux membres protégés d'une classe. Je me demande pourquoi un objet vide prendrait de la place lors de l'utilisation de la composition. Probablement pour une adressabilité universelle ...
3
Il est également pratique de rendre une classe non copiable - hériter simplement en privé d'une classe vide qui n'est pas copiable. Vous n'avez plus à passer par le travail chargé de déclarer mais pas de définir un constructeur de copie privée et un opérateur d'affectation. Meyers en parle aussi.
Michael Burr
Je ne savais pas que cette question concernait en fait l'héritage privé au lieu de l'héritage protégé. ouais je suppose qu'il y a pas mal d'applications pour cela. Je ne peux pas penser à de nombreux exemples d'héritage protégé: / semble que ce n'est que rarement utile.
Johannes Schaub - allumé
23

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 que seul l'objet spécifique avec lequel 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. C'est un excellent modèle pour gérer des rappels spécifiques définis comme des interfaces.

Daemin
la source
1
En effet, l'héritage privé est un IS-A privé.
curiousguy
18

Je pense que la section critique de la FAQ C ++ 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 des fonctions membres de votre nouvelle classe, Fred. Dans ce cas, Fred appelle des non-virtuels dans Wilma, et Wilma appelle (généralement des virtuels purs) en lui-même, qui sont remplacés par Fred. Ce serait beaucoup plus difficile à faire avec la composition.

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

Bill le lézard
la source
4

Je trouve cela utile pour les interfaces (c'est-à-dire les classes abstraites) dont j'hérite où je ne veux pas que d'autres codes touchent l'interface (uniquement la classe héritière).

[édité dans un exemple]

Prenons l' exemple lié à ci-dessus. Dire que

[...] classe Wilma a besoin d'appeler les fonctions membres de votre nouvelle classe, Fred.

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

l'héritage privé n'est pas un mal; c'est juste plus cher à maintenir, 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 comme 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).

[édité dans un autre exemple]

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

biais
la source
Cela ne semble pas vraiment utile ... pouvez-vous poster un exemple
Je pense que je vois où vous allez ... Un cas d'utilisation typique pourrait être que Wilma est un type de classe utilitaire qui a besoin d'appeler des fonctions virtuelles dans Fred, mais les autres classes n'ont pas besoin de savoir que Fred est implémenté en termes - de Wilma. Droite?
j_random_hacker
Oui. Je dois souligner que, à ma connaissance, le terme «interface» est plus couramment utilisé en Java. Quand j'en ai entendu parler pour la première fois, j'ai pensé qu'on aurait pu lui donner un meilleur nom. Puisque, dans cet exemple, nous avons une interface avec laquelle personne ne s'interface de la façon dont nous pensons normalement au mot.
biais
@Noos: Oui, je pense que votre déclaration "Wilma est une interface" est un peu ambiguë, car la plupart des gens penseraient que Wilma est une interface que Fred a l'intention de fournir au monde , plutôt qu'un contrat avec Wilma uniquement.
j_random_hacker
@j_ C'est pourquoi je pense que l'interface est un mauvais nom. Interface, le terme, n'a pas besoin de signifier pour le monde comme on pourrait le penser, mais est plutôt une garantie de fonctionnalité. En fait, j'étais content du terme interface dans ma classe de conception de programme. Mais, nous utilisons ce que l'on nous donne ...
biais
2

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

class BigClass;

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

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

Ensuite, si SomeCollection a besoin d'accéder à BigClass, il le peut static_cast<BigClass *>(this). Pas besoin d'avoir un membre de données supplémentaire occupant de l'espace.


la source
Il n'y a pas besoin de la déclaration anticipée de BigClassest-il dans cet exemple? Je trouve cela intéressant, mais ça me crie hackish.
Thomas Eding
2

J'ai trouvé une belle application pour l'héritage privé, même si son utilisation est limitée.

Problème à résoudre

Supposons que vous disposiez 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 en utilisant 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 (ex: mémoire)
  • Il est facile de configurer le structmauvais
  • Il est facile d'oublier de libérer les ressources lors de la libération du 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 tous liés à la gestion manuelle des ressources. La solution qui me vient à l'esprit est d'hériter Widgetet d'ajouter une instance de gestion des ressources à la classe dérivée WidgetImplpour 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);
}

Ainsi, nous avons résolu tous les problèmes ci-dessus. Mais un client peut toujours oublier les setters WidgetImplet les assignerWidget directement membres.

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

Pour encapsuler les Widgetmembres, nous utilisons l'héritage privé. Malheureusement, nous avons maintenant besoin de deux fonctions supplémentaires à convertir 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 rend les adaptations suivantes nécessaires:

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. Pas de gestion manuelle de la mémoire et Widgetest bien encapsulé pour queWidgetImpl ne plus avoir de membres de données publiques. Cela rend la mise en œuvre facile à utiliser correctement et difficile (impossible?) À mal utiliser.

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

Matthäus Brandl
la source
1

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 de voir des méthodes de base déverrouillées exportées via cette classe dérivée.

sportswithin.com
la source
1

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

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

bayda
la source
0

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

exemple tiré 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 change pas la hauteur (que Rectangle documente explicitement ce contrat ou non), alors que Square :: SetWidth ne peut pas conserver ce contrat et son propre invariant de carré à le même temps. Mais Rectangle ne peut pas non plus hériter correctement de Square, si les clients de Square supposent par exemple que l'aire d'un Square est sa largeur au carré, ou s'ils s'appuient sur une autre propriété qui ne vaut pas pour les Rectangles.

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

Rahul_cs12
la source
0

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 d'AggregateType. Cela est particulièrement vrai si vous avez raison. Considérer:

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 (stockage de référence à la base si nécessaire). Cela ne vaut pas toujours la peine de modéliser de cette façon; l'héritage est parfois la représentation la plus efficace.

Lorro
la source
0

Si vous avez besoin d'un std::ostreamavec quelques petits changements (comme dans cette question ), vous devrez peut-être

  1. Créer une classe MyStreambufqui dérive destd::streambuf et implémente les changements là-bas
  2. Créer une classe MyOStreamqui en dérive std::ostreamégalement initialise et gère une instance de MyStreambufet passe le pointeur vers cette instance au constructeur destd::ostream

La première idée pourrait être d'ajouter l' MyStreaminstance en tant que membre de données à la MyOStreamclasse:

class MyOStream : public std::ostream
{
public:
    MyOStream()
        : std::basic_ostream{ &m_buf }
        , m_buf{}
    {}

private:
    MyStreambuf m_buf;
};

Mais les classes de base sont construites avant tout membre de données, vous passez donc un pointeur vers une std::streambufinstance non encore construite àstd::ostream laquelle un comportement n'est pas défini.

La solution est proposée dans la réponse de Ben à la question susmentionnée , héritez simplement du tampon de flux d'abord, puis du flux et ensuite initialisez le flux avec this:

class MyOStream : public MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

Cependant, la classe résultante peut également être utilisée comme std::streambufinstance, ce qui est généralement indésirable. Le passage à l'héritage privé résout ce problème:

class MyOStream : private MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};
Matthäus Brandl
la source
-1

Ce n'est pas parce que C ++ a une fonctionnalité qu'il est utile ou qu'il doit être utilisé.

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

Si vous l'utilisez quand même, eh bien, vous violez fondamentalement l'encapsulation et réduisez la cohésion. Vous placez des données dans une classe et ajoutez des méthodes qui manipulent les données dans une autre.

Comme d'autres fonctionnalités C ++, il peut être utilisé pour obtenir des effets secondaires tels que sceller une classe (comme mentionné dans la réponse de dribeas), mais cela n'en fait pas une bonne fonctionnalité.

hasen
la source
Es-tu sarcastique? tout ce que j'ai est un -1! de toute façon je ne vais pas supprimer ce même si elle obtient -100 votes
Hasen
9
" vous violez fondamentalement l'encapsulation " Pouvez-vous donner un exemple?
curiousguy
1
les données dans une classe et le comportement dans une autre semblent être une augmentation de la flexibilité, car il peut y avoir plus d'une classe de comportement et de clients et choisir celui dont ils ont besoin pour satisfaire ce qu'ils veulent
makar