Kiedy należy używać dziedziczenia prywatnego w C ++?


116

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?

c++  oop 

Odpowiedzi:


60

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 finalkontekstowe słowo kluczowe dokładnie w tym celu:

class Sealed final // ...

To świetna technika. Napiszę o tym wpis na blogu.

1
Pytanie: gdybyśmy nie używali dziedziczenia wirtualnego, skompilowałoby się FailsToDerive. Poprawny?

4
+1. @Sasha: Prawidłowo, dziedziczenie wirtualne jest potrzebne, ponieważ najbardziej pochodna klasa zawsze bezpośrednio wywołuje konstruktory wszystkich dziedziczonych wirtualnie klas, co nie ma miejsca w przypadku zwykłego dziedziczenia.
j_random_hacker

5
Można to uczynić ogólnymi, bez tworzenia niestandardowego ClassSealera dla każdej klasy, którą chcesz zapieczętować! Sprawdź to: class ClassSealer {protected: ClassSealer () {}}; to wszystko.

+1 Iraimbilanja, bardzo fajnie! BTW Widziałem twój wcześniejszy komentarz (teraz usunięty) o używaniu CRTP: Myślę, że to powinno działać, po prostu trudno jest uzyskać właściwą składnię dla przyjaciół szablonów. Ale w każdym razie twoje rozwiązanie nie oparte na szablonie jest o wiele bardziej niesamowite :)
j_random_hacker

138

Używam go cały czas. Kilka przykładów z mojej głowy:

  • Gdy chcę odsłonić niektóre, ale nie cały interfejs klasy bazowej. Dziedziczenie publiczne byłoby kłamstwem, ponieważ substytucyjność Liskova jest zepsuta, podczas gdy kompozycja oznaczałaby napisanie zestawu funkcji przekazujących.
  • Kiedy chcę wyprowadzić z konkretnej klasy bez wirtualnego destruktora. Dziedziczenie publiczne zaprosiłoby klientów do usunięcia za pomocą wskaźnika do bazy, wywołując niezdefiniowane zachowanie.

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...  
};
  • Podczas implementowania wzorca adaptera prywatne dziedziczenie z klasy Adapted pozwala uniknąć konieczności przekazywania dalej do zamkniętej instancji.
  • Aby zaimplementować prywatny interfejs. To pojawia się często w przypadku wzorca obserwatora. Zazwyczaj moja klasa Observer, mówi MyClass, subskrybuje się z jakimś Subject. Wtedy tylko MyClass musi wykonać konwersję MyClass -> Observer. Reszta systemu nie musi o tym wiedzieć, dlatego wskazane jest dziedziczenie prywatne.

4
@Krsna: Właściwie nie sądzę. Powód jest tylko jeden: lenistwo, poza ostatnim, które byłoby trudniejsze do obejścia.
Matthieu M.,

11
Nie tyle lenistwa (chyba że masz to na myśli w dobry sposób). Pozwala to na tworzenie nowych przeciążeń funkcji, które zostały ujawnione bez dodatkowej pracy. Jeśli w C ++ 1x dodadzą 3 nowe przeciążenia push_back, MyVectorotrzymają je za darmo.
David Stone

@DavidStone, czy nie możesz tego zrobić metodą szablonu?
Julien__

5
@Julien__: Tak, możesz pisać 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 usingoświadczenie daje, trzeba tego potwora dla każdej funkcji (i nie zapomnieć o consti volatileprzeciążenia!).
David Stone

2
Mówię o większości funkcji, ponieważ nadal wywołujesz jeden dodatkowy konstruktor przenoszenia, którego nie ma w wersji instrukcji using. Ogólnie rzecz biorąc, można oczekiwać, że zostanie to zoptymalizowane, ale funkcja mogłaby teoretycznie zwracać nieprzenośny typ według wartości. Szablon funkcji przekazywania ma również dodatkową instancję szablonu i głębokość constexpr. Może to spowodować przekroczenie limitów implementacji programu.
David Stone

31

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.


6
Warto wspomnieć o jednym z powodów, dla których jest on używany w tym przypadku: Pozwala to na wykonanie optymalizacji pustej klasy bazowej, która nie wystąpi, jeśli klasa byłaby składową zamiast klasą bazową.
jalf

2
jego głównym zastosowaniem jest zmniejszenie zajmowanego miejsca tam, gdzie jest to naprawdę ważne, np. w klasach łańcuchów kontrolowanych przez zasady lub w skompresowanych parach. faktycznie, boost :: compressed_pair używał chronionego dziedziczenia.
Johannes Schaub - litb

jalf: Hej, nie zdawałem sobie z tego sprawy. Myślałem, że niepubliczne dziedziczenie było używane głównie jako hack, gdy potrzebujesz dostępu do chronionych członków klasy. Zastanawiam się jednak, dlaczego pusty obiekt zajmowałby jakąkolwiek przestrzeń przy użyciu kompozycji. Prawdopodobnie dla uniwersalnej adresowalności ...

3
Przydatne jest również uniemożliwienie kopiowania klasy - po prostu prywatnie dziedziczenie z pustej klasy, której nie można skopiować. Teraz nie musisz przechodzić przez zajętą ​​pracę związaną z deklarowaniem, ale nie definiowaniem prywatnego konstruktora kopiującego i operatora przypisania. Meyers też o tym mówi.
Michael Burr,

Nie zdawałem sobie sprawy, że to pytanie w rzeczywistości dotyczy prywatnego dziedziczenia zamiast chronionego dziedziczenia. tak, wydaje mi się, że jest na to sporo aplikacji. nie mogę wymyślić wielu przykładów chronionego dziedziczenia: / wygląda na to, że rzadko jest to przydatne.
Johannes Schaub - litb

23

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.


1
Rzeczywiście, prywatne dziedzictwo jest prywatnym IS-A.
curiousguy

18

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.


4

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).


To nie brzmi zbyt dobrze ... czy możesz opublikować przykład

Myślę, że widzę, dokąd zmierzasz ... Typowy przypadek użycia może być taki, że Wilma jest rodzajem klasy narzędziowej, która musi wywoływać funkcje wirtualne w Fred, ale inne klasy nie muszą wiedzieć, że Fred jest zaimplementowany w terminach Wilmy. Dobrze?
j_random_hacker

Tak. Powinienem zaznaczyć, że w moim rozumieniu termin „interfejs” jest częściej używany w Javie. Kiedy pierwszy raz o tym usłyszałem, pomyślałem, że można by było nadać mu lepszą nazwę. Ponieważ w tym przykładzie mamy interfejs, z którym nikt nie łączy się w sposób, w jaki normalnie myślimy o tym słowie.
odchylenie

@Noos: Tak, myślę, że twoje stwierdzenie „Wilma jest interfejsem” jest nieco niejednoznaczne, ponieważ większość ludzi uznałaby to za interfejs, który Fred zamierza dostarczyć światu , a nie tylko kontrakt z Wilmą.
j_random_hacker,

@j_ Dlatego uważam, że interfejs to zła nazwa. Termin `` interfejs '' nie musi oznaczać dla świata, jak mogłoby się wydawać, ale raczej jest gwarancją funkcjonalności. Właściwie to byłem sporny co do terminu interfejs w mojej klasie projektowania programów. Ale korzystamy z tego, co otrzymaliśmy ...
odchylenie

2

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.


Nie ma potrzeby przedkładania deklaracji. Czy BigClassw tym przykładzie występuje? Uważam, że to interesujące, ale krzyczy mi w twarz.
Thomas Eding

2

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:

#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 ++.

Podejście C-ish

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:

  • Ręczne zarządzanie zasobami (np. Pamięcią)
  • Łatwo jest structźle skonfigurować
  • Łatwo jest zapomnieć o zwolnieniu zasobów podczas zwalniania struct
  • To jest C-ish

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 Widgeti dodawanie instancji zarządzającej zasobami do klasy pochodnej WidgetImpldla 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 WidgetImpli przypisać doWidget bezpośrednio członków.

Na scenę wkracza prywatne dziedzictwo

Aby hermetyzować Widgetczł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 Widgetjest ł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 .


1

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.


1

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.


0

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.


0

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ą.


0

Jeśli potrzebujesz std::ostreamdrobnych zmian (jak w tym pytaniu ), być może będziesz musiał

  1. Utwórz klasę, MyStreambufktóra wywodzi się z std::streambufi zaimplementuj tam zmiany
  2. Utwórz klasę, MyOStreamktóra pochodzi z std::ostreamtego, również inicjuje i zarządza wystąpieniem MyStreambufi przekazuje wskaźnik do tego wystąpienia do konstruktorastd::ostream

Pierwszym pomysłem może być dodanie MyStreaminstancji jako elementu członkowskiego danych do MyOStreamklasy:

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::streambufinstancji, do std::ostreamktó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::streambufinstancja, 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 }
    {}
};

-1

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.


czy jesteś sarkastyczny? wszystko co mam to -1! w każdym razie nie usunę tego, nawet jeśli dostanie -100 głosów
hasen

9
„w zasadzie naruszasz hermetyzację ” Czy możesz podać przykład?
curiousguy

1
dane w jednej klasie i zachowanie w innej brzmią jak wzrost elastyczności, ponieważ może istnieć więcej niż jedna klasa zachowań i klientów i wybrać, której z nich potrzebują, aby zaspokoić to, czego chcą
makar
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.