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?
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?
Réponses:
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:
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...
};
push_back
, les MyVector
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 utilisant Base::f;
. Si vous voulez la plupart des fonctionnalités et de flexibilité que l' héritage privé et une using
déclaration que vous donne, vous avez ce monstre pour chaque fonction (et ne pas oublier const
et volatile
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.
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.
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é.
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).
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.
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.
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 ++.
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:
struct
mauvaisstruct
Nous sommes autorisés à utiliser C ++, alors pourquoi ne pas utiliser ses pleins pouvoirs?
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é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);
}
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.
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:
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 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 .
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.
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.
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.
Si vous avez besoin d'un std::ostream
avec quelques petits changements (comme dans cette question ), vous devrez peut-être
MyStreambuf
qui dérive destd::streambuf
et implémente les changements là-basMyOStream
qui en dérive std::ostream
également initialise et gère une instance de MyStreambuf
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 à la MyOStream
classe:
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::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
:
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::streambuf
instance, 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 }
{}
};
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é.