W przeciwieństwie do dziedziczenia chronionego, dziedziczenie prywatne w C ++ znalazło zastosowanie w głównym nurcie programowania w C ++. Jednak nadal nie znalazłem dla niego dobrego zastosowania.
Kiedy go używacie?
W przeciwieństwie do dziedziczenia chronionego, dziedziczenie prywatne w C ++ znalazło zastosowanie w głównym nurcie programowania w C ++. Jednak nadal nie znalazłem dla niego dobrego zastosowania.
Kiedy go używacie?
Odpowiedzi:
Uwaga po przyjęciu odpowiedzi: NIE jest to pełna odpowiedź. Przeczytaj inne odpowiedzi, takie jak tutaj (koncepcyjne) i tutaj (zarówno teoretyczne, jak i praktyczne), jeśli jesteś zainteresowany tym pytaniem. To tylko wymyślna sztuczka, którą można osiągnąć dzięki dziedziczeniu prywatnemu. Chociaż jest to wymyślne, nie jest to odpowiedź na pytanie.
Oprócz podstawowego użycia tylko prywatnego dziedziczenia pokazanego w C ++ FAQ (połączonych w komentarzach innych osób) możesz użyć kombinacji dziedziczenia prywatnego i wirtualnego do zapieczętowania klasy (w terminologii .NET) lub do ostatecznego zakończenia klasy (w terminologii Java) . Nie jest to powszechne zastosowanie, ale i tak uznałem to za interesujące:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Można utworzyć instancję Sealed . Pochodzi z ClassSealer i może bezpośrednio wywołać prywatnego konstruktora, ponieważ jest przyjacielem.
FailsToDerive nie skompiluje się, ponieważ musi bezpośrednio wywołać konstruktor ClassSealer (wymóg dziedziczenia wirtualnego), ale nie może, ponieważ jest prywatny w klasie Sealed iw tym przypadku FailsToDerive nie jest przyjacielem ClassSealer .
EDYTOWAĆ
W komentarzach wspomniano, że w tamtym czasie nie można było uczynić tego standardowym przy użyciu CRTP. Standard C ++ 11 usuwa to ograniczenie, udostępniając inną składnię, aby zaprzyjaźnić się z argumentami szablonu:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Oczywiście to wszystko jest dyskusyjne, ponieważ C ++ 11 dostarcza final
kontekstowe słowo kluczowe dokładnie w tym celu:
class Sealed final // ...
Używam go cały czas. Kilka przykładów z mojej głowy:
Typowy przykład pochodzi prywatnie z kontenera 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
, MyVector
otrzymają je za darmo.
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)...); }
lub możesz pisać używając Base::f;
. Jeśli chcesz większość funkcji i elastyczność tej prywatnej dziedziczenia oraz using
oświadczenie daje, trzeba tego potwora dla każdej funkcji (i nie zapomnieć o const
i volatile
przeciążenia!).
Kanoniczne użycie dziedziczenia prywatnego to relacja „realizowana w kategoriach” (dzięki „Effective C ++” Scotta Meyersa za to sformułowanie). Innymi słowy, interfejs zewnętrzny klasy dziedziczącej nie ma (widocznego) związku z klasą dziedziczoną, ale używa go wewnętrznie do implementacji swojej funkcjonalności.
Jednym z przydatnych zastosowań dziedziczenia prywatnego jest posiadanie klasy, która implementuje interfejs, a następnie jest rejestrowana z innym obiektem. Ustawiasz ten interfejs jako prywatny, tak że sama klasa musi się zarejestrować i tylko określony obiekt, w którym została zarejestrowana, może korzystać z tych funkcji.
Na przykład:
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() { ... }
};
Dlatego klasa FooUser może wywoływać metody prywatne FooImplementer za pośrednictwem interfejsu FooInterface, podczas gdy inne klasy zewnętrzne nie mogą. To świetny wzorzec do obsługi określonych wywołań zwrotnych, które są zdefiniowane jako interfejsy.
Myślę, że krytyczna sekcja z C ++ FAQ Lite to:
Prawidłowe, długoterminowe użycie do dziedziczenia prywatnego ma miejsce wtedy, gdy chcesz zbudować klasę Fred, która używa kodu z klasy Wilma, a kod z klasy Wilma musi wywoływać funkcje składowe z nowej klasy, Fred. W tym przypadku Fred wywołuje funkcje nie-wirtualne w Wilmie, a wywołania Wilmy (zwykle czyste wirtualne) same w sobie, które są nadpisywane przez Freda. Byłoby to znacznie trudniejsze w przypadku kompozycji.
Jeśli masz wątpliwości, powinieneś przedkładać kompozycję nad prywatne dziedzictwo.
Uważam, że jest to przydatne w przypadku interfejsów (a mianowicie klas abstrakcyjnych), które dziedziczę, gdy nie chcę, aby inny kod dotykał interfejsu (tylko klasa dziedzicząca).
[zredagowano w przykładzie]
Weź przykład podany powyżej. Mówiąc, że
[...] klasa Wilma musi wywołać funkcje składowe z twojej nowej klasy, Fred.
to powiedzieć, że Wilma wymaga od Freda możliwości wywołania pewnych funkcji składowych, a raczej mówi, że Wilma jest interfejsem . Stąd, jak wspomniano w przykładzie
prywatne dziedzictwo nie jest złe; jest po prostu droższy w utrzymaniu, ponieważ zwiększa prawdopodobieństwo, że ktoś zmieni coś, co złamie twój kod.
komentarze na temat oczekiwanego efektu konieczności spełnienia przez programistów wymagań naszego interfejsu lub złamania kodu. A ponieważ fredCallsWilma () jest chroniona, tylko przyjaciele i klasy pochodne mogą jej dotykać, tj. Dziedziczony interfejs (klasa abstrakcyjna), którego może dotykać tylko klasa dziedzicząca (i przyjaciele).
[zredagowano w innym przykładzie]
Ta strona pokrótce omawia prywatne interfejsy (z jeszcze innej perspektywy).
Czasami uważam, że przydatne jest dziedziczenie prywatne, gdy chcę wyeksponować mniejszy interfejs (np. Kolekcję) w interfejsie innego, gdzie implementacja kolekcji wymaga dostępu do stanu klasy eksponującej, podobnie jak klasy wewnętrzne w Jawa.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Jeśli SomeCollection potrzebuje dostępu do BigClass, będzie to możliwe static_cast<BigClass *>(this)
. Nie ma potrzeby, aby dodatkowy element danych zajmował miejsce.
BigClass
w tym przykładzie występuje? Uważam, że to interesujące, ale krzyczy mi w twarz.
Znalazłem fajną aplikację do dziedziczenia prywatnego, chociaż ma ograniczone zastosowanie.
Załóżmy, że masz następujący interfejs API w języku C:
#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
Teraz Twoim zadaniem jest zaimplementowanie tego API przy użyciu C ++.
Oczywiście mogliśmy wybrać styl implementacji C-ish, na przykład:
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;
}
Ale jest kilka wad:
struct
źle skonfigurowaćstruct
Możemy używać C ++, więc dlaczego nie wykorzystać jego pełnych możliwości?
Wszystkie powyższe problemy są w zasadzie związane z ręcznym zarządzaniem zasobami. Rozwiązanie, które przychodzi na myśl, to dziedziczenie Widget
i dodawanie instancji zarządzającej zasobami do klasy pochodnej WidgetImpl
dla każdej zmiennej:
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;
};
Upraszcza to implementację do następujących:
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);
}
W ten sposób rozwiązaliśmy wszystkie powyższe problemy. Ale klient nadal może zapomnieć o ustawieniach WidgetImpl
i przypisać doWidget
bezpośrednio członków.
Aby hermetyzować Widget
członków, używamy dziedziczenia prywatnego. Niestety, potrzebujemy teraz dwóch dodatkowych funkcji do rzutowania między obiema klasami:
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;
};
To sprawia, że konieczne są następujące dostosowania:
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);
}
To rozwiązanie rozwiązuje wszystkie problemy. Brak ręcznego zarządzania pamięcią i Widget
jest ładnie zamknięty, dzięki czemuWidgetImpl
nie ma już żadnych publicznych członków danych. To sprawia, że implementacja jest łatwa w obsłudze i trudna (niemożliwa?) Do niewłaściwego użycia.
Fragmenty kodu stanowią przykład kompilacji w Coliru .
Jeśli klasa pochodna - musi ponownie użyć kodu i - nie możesz zmienić klasy bazowej i - chroni swoje metody za pomocą elementów członkowskich bazy pod blokadą.
wtedy powinieneś użyć dziedziczenia prywatnego, w przeciwnym razie istnieje ryzyko odblokowania metod bazowych wyeksportowanych za pośrednictwem tej klasy pochodnej.
Dziedziczenie prywatne, które ma być używane, gdy relacja nie jest „jest”, ale nowa klasa może być „zaimplementowana w ramach istniejącej klasy” lub nowa klasa „działa jak” istniejąca klasa.
przykład ze „standardów kodowania C ++ autorstwa Andrei Alexandrescu, Herb Sutter”: - Weź pod uwagę, że dwie klasy Square i Rectangle mają funkcje wirtualne do ustawiania ich wysokości i szerokości. Następnie Square nie może poprawnie dziedziczyć po Rectangle, ponieważ kod, który używa modyfikowalnego Rectangle zakłada, że SetWidth nie zmienia wysokości (niezależnie od tego, czy Rectangle jawnie dokumentuje ten kontrakt, czy nie), podczas gdy Square :: SetWidth nie może zachować tego kontraktu i jego własnej niezmiennej prostokątności na o tym samym czasie. Ale Rectangle nie może również poprawnie dziedziczyć po Square, jeśli klienci Square zakładają na przykład, że pole Square jest jego szerokością do kwadratu, lub jeśli opierają się na jakiejś innej właściwości, która nie dotyczy Rectangles.
Kwadrat „jest” prostokątem (matematycznie), ale kwadrat nie jest prostokątem (z punktu widzenia zachowania). W związku z tym zamiast „is-a” wolimy mówić „działa jak a” (lub, jeśli wolisz, „użyteczny-jako-a”), aby opis był mniej podatny na nieporozumienia.
Klasa zawiera niezmiennik. Niezmiennik jest ustalany przez konstruktora. Jednak w wielu sytuacjach warto mieć wgląd w stan reprezentacji obiektu (który można przesłać przez sieć lub zapisać do pliku - jeśli wolisz - DTO). REST najlepiej jest wykonywać w kategoriach AggregateType. Jest to szczególnie ważne, jeśli masz stałą rację. Rozważać:
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
}
W tym momencie możesz po prostu przechowywać kolekcje pamięci podręcznej w kontenerach i sprawdzić je na budowie. Przydatne, jeśli istnieje prawdziwe przetwarzanie. Należy zauważyć, że pamięć podręczna jest częścią QE: operacje zdefiniowane w QE mogą oznaczać, że pamięć podręczna jest częściowo wielokrotnego użytku (np. C nie wpływa na sumę); ale gdy nie ma pamięci podręcznej, warto to sprawdzić.
Dziedziczenie prywatne prawie zawsze może być modelowane przez członka (w razie potrzeby przechowywanie odniesienia do bazy). Po prostu nie zawsze warto modelować w ten sposób; czasami dziedziczenie jest najbardziej efektywną reprezentacją.
Jeśli potrzebujesz std::ostream
drobnych zmian (jak w tym pytaniu ), być może będziesz musiał
MyStreambuf
która wywodzi się z std::streambuf
i zaimplementuj tam zmianyMyOStream
która pochodzi z std::ostream
tego, również inicjuje i zarządza wystąpieniem MyStreambuf
i przekazuje wskaźnik do tego wystąpienia do konstruktorastd::ostream
Pierwszym pomysłem może być dodanie MyStream
instancji jako elementu członkowskiego danych do MyOStream
klasy:
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Ale klasy bazowe są konstruowane przed jakimikolwiek składowymi danych, więc przekazujesz wskaźnik do jeszcze nieskonstruowanej std::streambuf
instancji, do std::ostream
której jest niezdefiniowane zachowanie.
Rozwiązanie jest proponowane w odpowiedzi Bena na powyższe pytanie , po prostu dziedzicz najpierw z bufora strumienia, potem ze strumienia, a następnie zainicjuj strumień za pomocą this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Jednak wynikowa klasa może być również używana jako std::streambuf
instancja, co jest zwykle niepożądane. Przejście na dziedziczenie prywatne rozwiązuje ten problem:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
To, że C ++ ma jakąś funkcję, nie oznacza, że jest ona użyteczna lub że powinna być używana.
Powiedziałbym, że w ogóle nie powinieneś go używać.
Jeśli i tak go używasz, cóż, w zasadzie naruszasz hermetyzację i obniżasz spójność. Umieszczasz dane w jednej klasie i dodajesz metody, które manipulują danymi w innej.
Podobnie jak inne funkcje C ++, można go użyć do uzyskania efektów ubocznych, takich jak zapieczętowanie klasy (jak wspomniano w odpowiedzi dribeas), ale to nie czyni z niej dobrej funkcji.