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?
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:
źródło
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ącBase::f;
. Jeśli chcesz większość funkcji i elastyczność tej prywatnej dziedziczenia orazusing
oświadczenie daje, trzeba tego potwora dla każdej funkcji (i nie zapomnieć oconst
ivolatile
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.
źródło
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:
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.
źródło
Myślę, że krytyczna sekcja z C ++ FAQ Lite to:
Jeśli masz wątpliwości, powinieneś przedkładać kompozycję nad prywatne dziedzictwo.
źródło
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
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
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).
źródło
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.
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.źródło
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.
Problem do rozwiązania
Załóżmy, że masz następujący interfejs API w języku C:
Teraz Twoim zadaniem jest zaimplementowanie tego API przy użyciu C ++.
Podejście C-ish
Oczywiście mogliśmy wybrać styl implementacji C-ish, na przykład:
Ale jest kilka wad:
struct
źle skonfigurowaćstruct
Podejście C ++
Możemy używać C ++, więc dlaczego nie wykorzystać jego pełnych możliwości?
Wprowadzenie zautomatyzowanego zarządzania zasobami
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 pochodnejWidgetImpl
dla każdej zmiennej:Upraszcza to implementację do następujących:
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.Na scenę wkracza prywatne dziedzictwo
Aby hermetyzować
Widget
członków, używamy dziedziczenia prywatnego. Niestety, potrzebujemy teraz dwóch dodatkowych funkcji do rzutowania między obiema klasami:To sprawia, że konieczne są następujące dostosowania:
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 .
źródło
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.
źródło
Czasami może to być alternatywa dla agregacji , na przykład jeśli chcesz agregacji, ale ze zmienionym zachowaniem agregowanej jednostki (nadpisanie funkcji wirtualnych).
Ale masz rację, nie ma wielu przykładów z prawdziwego świata.
źródło
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.
źródło
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ć:
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ą.
źródło
Jeśli potrzebujesz
std::ostream
drobnych zmian (jak w tym pytaniu ), być może będziesz musiałMyStreambuf
która wywodzi się zstd::streambuf
i zaimplementuj tam zmianyMyOStream
która pochodzi zstd::ostream
tego, również inicjuje i zarządza wystąpieniemMyStreambuf
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 doMyOStream
klasy:Ale klasy bazowe są konstruowane przed jakimikolwiek składowymi danych, więc przekazujesz wskaźnik do jeszcze nieskonstruowanej
std::streambuf
instancji, dostd::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
: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:źródło
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.
źródło