Im Gegensatz zur geschützten Vererbung fand die private C ++ - Vererbung Eingang in die Mainstream-C ++ - Entwicklung. Ich habe jedoch immer noch keine gute Verwendung dafür gefunden.
Wann benutzt ihr es?
Im Gegensatz zur geschützten Vererbung fand die private C ++ - Vererbung Eingang in die Mainstream-C ++ - Entwicklung. Ich habe jedoch immer noch keine gute Verwendung dafür gefunden.
Wann benutzt ihr es?
Antworten:
Hinweis nach Annahme der Antwort: Dies ist KEINE vollständige Antwort. Lesen Sie andere Antworten wie hier (konzeptionell) und hier (sowohl theoretisch als auch praktisch), wenn Sie an der Frage interessiert sind. Dies ist nur ein ausgefallener Trick, der mit privater Vererbung erreicht werden kann. Es ist zwar schick , aber nicht die Antwort auf die Frage.
Neben der grundlegenden Verwendung der privaten Vererbung, die in den häufig gestellten Fragen zu C ++ (in den Kommentaren anderer verknüpft) aufgeführt ist, können Sie eine Kombination aus privater und virtueller Vererbung verwenden, um eine Klasse zu versiegeln (in der .NET-Terminologie) oder eine Klasse endgültig zu machen (in der Java-Terminologie). . Dies ist keine übliche Verwendung, aber ich fand es trotzdem interessant:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Versiegelt kann instanziiert werden. Es stammt von ClassSealer und kann den privaten Konstruktor direkt aufrufen, da er ein Freund ist.
FailsToDerive wird nicht kompiliert, da es den ClassSealer- Konstruktor direkt aufrufen muss (virtuelle Vererbungsanforderung), kann dies jedoch nicht, da es in der Sealed- Klasse privat ist und in diesem Fall FailsToDerive kein Freund von ClassSealer ist .
BEARBEITEN
In den Kommentaren wurde erwähnt, dass dies zu diesem Zeitpunkt mit CRTP nicht generisch gemacht werden konnte. Der C ++ 11-Standard hebt diese Einschränkung auf, indem eine andere Syntax bereitgestellt wird, um sich mit Vorlagenargumenten anzufreunden:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Dies ist natürlich alles umstritten, da C ++ 11 final
genau zu diesem Zweck ein kontextbezogenes Schlüsselwort bereitstellt :
class Sealed final // ...
Ich benutze es die ganze Zeit. Ein paar Beispiele aus meinem Kopf:
Ein typisches Beispiel ist die private Ableitung von einem STL-Container:
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
, erhalten MyVector
sie diese kostenlos.
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)...); }
oder du könntest mit schreiben Base::f;
. Wenn Sie den größten Teil der Funktionalität und Flexibilität wünschen, die Ihnen die private Vererbung und eine using
Anweisung bieten, haben Sie dieses Monster für jede Funktion (und vergessen Sie es nicht const
und volatile
überladen Sie es!).
Die kanonische Verwendung der privaten Vererbung ist die Beziehung "implementiert in Bezug auf" (dank Scott Meyers '' Effective C ++ 'für diesen Wortlaut). Mit anderen Worten, die externe Schnittstelle der ererbenden Klasse hat keine (sichtbare) Beziehung zur geerbten Klasse, sondern verwendet sie intern, um ihre Funktionalität zu implementieren.
Eine nützliche Verwendung der privaten Vererbung besteht darin, dass Sie eine Klasse haben, die eine Schnittstelle implementiert, die dann bei einem anderen Objekt registriert wird. Sie machen diese Schnittstelle privat, sodass sich die Klasse selbst registrieren muss und nur das spezifische Objekt, bei dem sie registriert ist, diese Funktionen verwenden kann.
Beispielsweise:
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() { ... }
};
Daher kann die FooUser-Klasse die privaten Methoden von FooImplementer über die FooInterface-Schnittstelle aufrufen, während andere externe Klassen dies nicht können. Dies ist ein großartiges Muster für die Behandlung bestimmter Rückrufe, die als Schnittstellen definiert sind.
Ich denke, der kritische Abschnitt aus dem C ++ FAQ Lite ist:
Eine legitime, langfristige Verwendung für die private Vererbung ist, wenn Sie eine Klasse Fred erstellen möchten, die Code in einer Klasse Wilma verwendet, und der Code aus der Klasse Wilma Mitgliedsfunktionen aus Ihrer neuen Klasse Fred aufrufen muss. In diesem Fall ruft Fred Nicht-Virtuals in Wilma auf, und Wilma ruft (normalerweise reine Virtuals) an sich auf, die von Fred überschrieben werden. Dies wäre mit der Komposition viel schwieriger zu tun.
Im Zweifelsfall sollten Sie die Komposition der privaten Vererbung vorziehen.
Ich finde es nützlich für Schnittstellen (dh abstrakte Klassen), die ich erbe, wenn ich nicht möchte, dass anderer Code die Schnittstelle berührt (nur die erbende Klasse).
[in einem Beispiel bearbeitet]
Nehmen Sie das oben verlinkte Beispiel . Sagt, dass
[...] Klasse Wilma muss Mitgliedsfunktionen Ihrer neuen Klasse Fred aufrufen.
Das heißt, Wilma verlangt von Fred, dass er bestimmte Mitgliedsfunktionen aufrufen kann, oder vielmehr, dass Wilma eine Schnittstelle ist . Daher, wie im Beispiel erwähnt
privates Erbe ist nicht böse; Die Wartung ist nur teurer, da dadurch die Wahrscheinlichkeit erhöht wird, dass jemand etwas ändert, das Ihren Code beschädigt.
Kommentare zum gewünschten Effekt von Programmierern, die unsere Schnittstellenanforderungen erfüllen oder den Code brechen müssen. Und da fredCallsWilma () geschützt ist, können nur Freunde und abgeleitete Klassen es berühren, dh eine geerbte Schnittstelle (abstrakte Klasse), die nur die erbende Klasse berühren kann (und Freunde).
[in einem anderen Beispiel bearbeitet]
Diese Seite beschreibt kurz private Schnittstellen (aus einem anderen Blickwinkel).
Manchmal finde ich es nützlich, die private Vererbung zu verwenden, wenn ich eine kleinere Schnittstelle (z. B. eine Sammlung) in der Schnittstelle einer anderen verfügbar machen möchte, wobei für die Implementierung der Sammlung der Zugriff auf den Status der verfügbar machenden Klasse erforderlich ist, ähnlich wie bei inneren Klassen in Java.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Wenn SomeCollection dann auf BigClass zugreifen muss, ist dies möglich static_cast<BigClass *>(this)
. Es ist nicht erforderlich, dass ein zusätzliches Datenelement Speicherplatz beansprucht.
BigClass
In diesem Beispiel ist keine Vorwärtsdeklaration erforderlich . Ich finde das interessant, aber es schreit hackisch in mein Gesicht.
Ich habe eine nette Anwendung für die private Vererbung gefunden, obwohl sie nur begrenzt verwendet werden kann.
Angenommen, Sie erhalten die folgende C-API:
#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
Jetzt müssen Sie diese API mit C ++ implementieren.
Natürlich könnten wir einen C-ischen Implementierungsstil wie folgt wählen:
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;
}
Es gibt jedoch mehrere Nachteile:
struct
Falschestruct
Wir dürfen C ++ verwenden. Warum also nicht die volle Leistung nutzen?
Die oben genannten Probleme hängen im Wesentlichen alle mit der manuellen Ressourcenverwaltung zusammen. Die Lösung besteht darin, für jede Variable Widget
eine Ressourcenverwaltungsinstanz zu erben und der abgeleiteten Klasse hinzuzufügen WidgetImpl
:
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;
};
Dies vereinfacht die Implementierung auf Folgendes:
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);
}
Auf diese Weise haben wir alle oben genannten Probleme behoben. Ein Kunde kann jedoch immer noch die Setter vergessen WidgetImpl
und den Widget
Mitgliedern direkt zuweisen .
Um die Widget
Mitglieder zu kapseln, verwenden wir die private Vererbung. Leider benötigen wir jetzt zwei zusätzliche Funktionen, um zwischen beiden Klassen zu wechseln:
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;
};
Dies macht folgende Anpassungen erforderlich:
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);
}
Diese Lösung löst alle Probleme. Keine manuelle Speicherverwaltung und Widget
ist damit schön gekapseltWidgetImpl
keine öffentlichen Datenelemente mehr vorhanden sind. Es macht die Implementierung einfach korrekt und schwer (unmöglich?) Falsch zu verwenden.
Die Codefragmente bilden ein Kompilierungsbeispiel für Coliru .
Wenn abgeleitete Klasse - Code wiederverwenden muss und - Sie können die Basisklasse nicht ändern und - ihre Methoden mithilfe der Mitglieder der Basis unter einer Sperre schützen.
Dann sollten Sie die private Vererbung verwenden, da sonst die Gefahr besteht, dass entsperrte Basismethoden über diese abgeleitete Klasse exportiert werden.
Private Vererbung, die verwendet wird, wenn die Beziehung nicht "ist eine" ist, sondern eine neue Klasse "in Bezug auf eine vorhandene Klasse implementiert" werden kann oder eine neue Klasse "wie eine vorhandene Klasse" funktioniert.
Beispiel aus "C ++ - Codierungsstandards von Andrei Alexandrescu, Herb Sutter": - Beachten Sie, dass zwei Klassen Square und Rectangle jeweils virtuelle Funktionen zum Einstellen ihrer Höhe und Breite haben. Dann kann Square nicht korrekt von Rectangle erben, da Code, der ein modifizierbares Rectangle verwendet, davon ausgeht, dass SetWidth die Höhe nicht ändert (unabhängig davon, ob Rectangle diesen Vertrag explizit dokumentiert oder nicht), während Square :: SetWidth diesen Vertrag und seine eigene Rechteckigkeit nicht beibehalten kann die selbe Zeit. Aber Rectangle kann auch nicht korrekt von Square erben, wenn Clients von Square beispielsweise annehmen, dass die Fläche eines Quadrats seine quadratische Breite ist, oder wenn sie sich auf eine andere Eigenschaft verlassen, die für Rectangles nicht gilt.
Ein Quadrat "ist-ein" Rechteck (mathematisch), aber ein Quadrat ist kein Rechteck (verhaltensmäßig). Folglich sagen wir anstelle von "is-a" lieber "works-like-a" (oder, wenn Sie es vorziehen, "us-as-a"), um die Beschreibung weniger anfällig für Missverständnisse zu machen.
Eine Klasse enthält eine Invariante. Die Invariante wird vom Konstruktor festgelegt. In vielen Situationen ist es jedoch hilfreich, den Darstellungsstatus des Objekts anzuzeigen (den Sie über das Netzwerk übertragen oder in einer Datei speichern können - DTO, wenn Sie dies bevorzugen). REST wird am besten in Bezug auf einen AggregateType durchgeführt. Dies gilt insbesondere dann, wenn Sie const korrekt sind. Erwägen:
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
}
Zu diesem Zeitpunkt können Sie Cachesammlungen einfach in Containern speichern und bei der Erstellung nachschlagen. Praktisch, wenn es eine echte Verarbeitung gibt. Beachten Sie, dass der Cache Teil der QE ist: In der QE definierte Operationen können bedeuten, dass der Cache teilweise wiederverwendbar ist (z. B. hat c keinen Einfluss auf die Summe). Wenn es jedoch keinen Cache gibt, lohnt es sich, ihn nachzuschlagen.
Private Vererbung kann fast immer von einem Mitglied modelliert werden (bei Bedarf wird auf die Basis verwiesen). Es lohnt sich einfach nicht immer, so zu modellieren. Manchmal ist die Vererbung die effizienteste Darstellung.
Wenn Sie eine std::ostream
mit einigen kleinen Änderungen (wie in dieser Frage ) benötigen, müssen Sie möglicherweise
MyStreambuf
die von abgeleitet iststd::streambuf
Änderungen und implementieren Sie sie dortMyOStream
die std::ostream
auch eine Instanz von initialisiert und verwaltet MyStreambuf
und den Zeiger auf diese Instanz an den Konstruktor von übergibtstd::ostream
Die erste Idee könnte darin bestehen, die MyStream
Instanz als Datenelement zur MyOStream
Klasse hinzuzufügen :
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Basisklassen werden jedoch vor Datenelementen erstellt, sodass Sie einen Zeiger auf eine noch nicht erstellte std::streambuf
Instanz übergeben, für std::ostream
die ein undefiniertes Verhalten vorliegt.
Die Lösung wird in Bens Antwort auf die oben genannte Frage vorgeschlagen . Erben Sie einfach zuerst vom Stream-Puffer, dann vom Stream und initialisieren Sie den Stream dann mit this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Die resultierende Klasse kann jedoch auch als std::streambuf
Instanz verwendet werden, was normalerweise unerwünscht ist. Der Wechsel zur privaten Vererbung löst dieses Problem:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Nur weil C ++ eine Funktion hat, heißt das nicht, dass sie nützlich ist oder verwendet werden sollte.
Ich würde sagen, du solltest es überhaupt nicht benutzen.
Wenn Sie es trotzdem verwenden, verletzen Sie im Grunde die Kapselung und verringern den Zusammenhalt. Sie ordnen Daten einer Klasse zu und fügen Methoden hinzu, mit denen die Daten in einer anderen Klasse bearbeitet werden.
Wie andere C ++ - Funktionen kann es verwendet werden, um Nebenwirkungen wie das Versiegeln einer Klasse zu erzielen (wie in der Antwort von dribeas erwähnt), aber dies macht es nicht zu einer guten Funktion.