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?
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 final
mot clé contextuel exactement dans ce but:
class Sealed final // ...
Je l'utilise tout le temps. Quelques exemples du haut de ma tête:
Un exemple typique est la dérivation privée d'un conteneur STL:
la source
push_back
, lesMyVector
obtient gratuitement.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 utilisantBase::f;
. Si vous voulez la plupart des fonctionnalités et de flexibilité que l' héritage privé et uneusing
déclaration que vous donne, vous avez ce monstre pour chaque fonction (et ne pas oublierconst
etvolatile
surcharges!).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.
la source
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:
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.
la source
Je pense que la section critique de la FAQ C ++ Lite est:
En cas de doute, préférez la composition à l'héritage privé.
la source
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
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
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).
la source
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.
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
BigClass
est-il dans cet exemple? Je trouve cela intéressant, mais ça me crie hackish.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:
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:
Mais il y a plusieurs inconvénients:
struct
mauvaisstruct
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
Widget
et d'ajouter une instance de gestion des ressources à la classe dérivéeWidgetImpl
pour chaque variable:Cela simplifie la mise en œuvre comme suit:
Ainsi, nous avons résolu tous les problèmes ci-dessus. Mais un client peut toujours oublier les setters
WidgetImpl
et les assignerWidget
directement membres.L'héritage privé entre en scène
Pour encapsuler les
Widget
membres, nous utilisons l'héritage privé. Malheureusement, nous avons maintenant besoin de deux fonctions supplémentaires à convertir entre les deux classes:Cela rend les adaptations suivantes nécessaires:
Cette solution résout tous les problèmes. Pas de gestion manuelle de la mémoire et
Widget
est 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 .
la source
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.
la source
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.
la source
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.
la source
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:
À 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.
la source
Si vous avez besoin d'un
std::ostream
avec quelques petits changements (comme dans cette question ), vous devrez peut-êtreMyStreambuf
qui dérive destd::streambuf
et implémente les changements là-basMyOStream
qui en dérivestd::ostream
également initialise et gère une instance deMyStreambuf
et passe le pointeur vers cette instance au constructeur destd::ostream
La première idée pourrait être d'ajouter l'
MyStream
instance en tant que membre de données à laMyOStream
classe:Mais les classes de base sont construites avant tout membre de données, vous passez donc un pointeur vers une
std::streambuf
instance 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
:Cependant, la classe résultante peut également être utilisée comme
std::streambuf
instance, ce qui est généralement indésirable. Le passage à l'héritage privé résout ce problème:la source
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é.
la source