Quando dovrei usare l'ereditarietà privata C ++?


116

A differenza dell'ereditarietà protetta, l'ereditarietà privata C ++ ha trovato la sua strada nello sviluppo C ++ tradizionale. Tuttavia, non ne ho ancora trovato un buon uso.

Quando lo usate ragazzi?

c++  oop 

Risposte:


60

Nota dopo l'accettazione della risposta: questa NON è una risposta completa. Leggi altre risposte come qui (concettualmente) e qui (sia teoriche che pratiche) se sei interessato alla domanda. Questo è solo un trucco di fantasia che può essere ottenuto con l'eredità privata. Sebbene sia di fantasia, non è la risposta alla domanda.

Oltre all'uso di base della sola ereditarietà privata mostrato nelle FAQ C ++ (collegate nei commenti di altri) puoi usare una combinazione di eredità privata e virtuale per sigillare una classe (nella terminologia .NET) o per rendere finale una classe (nella terminologia Java) . Questo non è un utilizzo comune, ma comunque l'ho trovato interessante:

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

Sealed può essere istanziato. Deriva da ClassSealer e può chiamare direttamente il costruttore privato in quanto è un amico.

FailsToDerive non si compila in quanto deve chiamare direttamente il costruttore ClassSealer (requisito di ereditarietà virtuale), ma non può in quanto è privato nella classe Sealed e in questo caso FailsToDerive non è amico di ClassSealer .


MODIFICARE

È stato menzionato nei commenti che questo non poteva essere reso generico al momento utilizzando CRTP. Lo standard C ++ 11 rimuove tale limitazione fornendo una sintassi diversa per fare amicizia con gli argomenti del modello:

template <typename T>
class Seal {
   friend T;          // not: friend class T!!!
   Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...

Ovviamente questo è tutto discutibile, poiché C ++ 11 fornisce una finalparola chiave contestuale esattamente per questo scopo:

class Sealed final // ...

È un'ottima tecnica. Ci scriverò un post sul blog.

1
Domanda: se non usassimo l'ereditarietà virtuale, FailsToDerive verrebbe compilato. Corretta?

4
+1. @Sasha: è necessaria l'ereditarietà virtuale corretta poiché la classe più derivata chiama sempre i costruttori di tutte le classi virtualmente ereditate direttamente, il che non è il caso dell'ereditarietà semplice.
j_random_hacker

5
Questo può essere reso generico, senza creare un ClassSealer personalizzato per ogni classe che desideri sigillare! Dai un'occhiata: class ClassSealer {protected: ClassSealer () {}}; È tutto.

+1 Iraimbilanja, molto bello! A proposito, ho visto il tuo precedente commento (ora cancellato) sull'uso del CRTP: penso che dovrebbe funzionare, è solo difficile ottenere la sintassi giusta per gli amici dei modelli. Ma in ogni caso la tua soluzione senza modello è molto più fantastica :)
j_random_hacker

138

Io lo uso per tutto il tempo. Alcuni esempi fuori dalla mia testa:

  • Quando voglio esporre alcune ma non tutte le interfacce di una classe base. L'eredità pubblica sarebbe una bugia, poiché la sostituibilità di Liskov è rotta, mentre la composizione significherebbe scrivere un mucchio di funzioni di inoltro.
  • Quando voglio derivare da una classe concreta senza un distruttore virtuale. L'ereditarietà pubblica inviterà i client a eliminare tramite un puntatore alla base, invocando un comportamento indefinito.

Un tipico esempio è derivato privatamente da un contenitore 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...  
};
  • Quando si implementa il modello di adattatore, l'ereditarietà privata dalla classe Adapted evita di dover inoltrare a un'istanza racchiusa.
  • Per implementare un'interfaccia privata. Questo accade spesso con il modello Observer. Tipicamente la mia classe Observer, dice MyClass, si iscrive con qualche Subject. Quindi, solo MyClass deve eseguire la conversione MyClass -> Observer. Il resto del sistema non ha bisogno di saperlo, quindi è indicata l'eredità privata.

4
@Krsna: In realtà, non credo. C'è solo una ragione qui: la pigrizia, a parte l'ultima, che sarebbe più complicata da aggirare.
Matthieu M.

11
Non così tanta pigrizia (a meno che tu non la intenda nel senso buono). Ciò consente la creazione di nuovi sovraccarichi di funzioni che sono state esposte senza alcun lavoro aggiuntivo. Se in C ++ 1x aggiungono 3 nuovi overload push_back, MyVectorli ottiene gratuitamente.
David Stone

@DavidStone, non puoi farlo con un metodo modello?
Julien__

5
@ Julien__: Sì, potresti scrivere 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)...); }o potresti scrivere usando Base::f;. Se si desidera che la maggior parte delle funzionalità e flessibilità che l'ereditarietà privata e una usingdichiarazione che dà, si dispone di quel mostro per ogni funzione (e non dimenticare conste volatilesovraccarichi!).
David Stone

2
Dico la maggior parte delle funzionalità perché stai ancora invocando un costruttore di mosse extra che non è presente nella versione dell'istruzione using. In generale, ti aspetteresti che questo venga ottimizzato, ma la funzione potrebbe teoricamente restituire un tipo non mobile in base al valore. Il modello della funzione di inoltro ha anche un'istanza di modello extra e una profondità constexpr. Ciò potrebbe causare l'esecuzione del programma nei limiti di implementazione.
David Stone

31

L'utilizzo canonico dell'ereditarietà privata è la relazione "implementata in termini di" (grazie all '"Effective C ++" di Scott Meyers per questa formulazione). In altre parole, l'interfaccia esterna della classe che eredita non ha alcuna relazione (visibile) con la classe ereditata, ma la utilizza internamente per implementare la sua funzionalità.


6
Può valere la pena menzionare uno dei motivi per cui viene utilizzato in questo caso: ciò consente di eseguire l'ottimizzazione della classe di base vuota, che non si verificherà se la classe fosse stata un membro anziché una classe di base.
jalf

2
il suo utilizzo principale è quello di ridurre il consumo di spazio dove è veramente importante, ad esempio nelle classi di stringhe controllate da policy o in coppie compresse. in realtà, boost :: compressed_pair utilizzava l'ereditarietà protetta.
Johannes Schaub - litb

jalf: Ehi, non me ne rendevo conto. Ho pensato che l'ereditarietà non pubblica fosse utilizzata principalmente come hack quando è necessario accedere ai membri protetti di una classe. Mi chiedo perché un oggetto vuoto occuperebbe spazio quando si usa la composizione. Probabilmente per indirizzabilità universale ...

3
È anche utile rendere una classe non copiabile: è sufficiente ereditarla privatamente da una classe vuota che non è copiabile. Ora non devi passare attraverso il lavoro impegnativo di dichiarare ma non definire un costruttore di copia privata e un operatore di assegnazione. Anche Meyers parla di questo.
Michael Burr

Non mi rendevo conto che questa domanda in realtà riguarda l'eredità privata anziché l'eredità protetta. sì, immagino che ci siano un bel po 'di applicazioni per questo. non riesco a pensare a molti esempi di ereditarietà protetta: / sembra che sia utile solo raramente.
Johannes Schaub - litb

23

Un utile utilizzo dell'ereditarietà privata è quando si dispone di una classe che implementa un'interfaccia, che viene quindi registrata con qualche altro oggetto. Rendi privata quell'interfaccia in modo che la classe stessa debba registrarsi e solo l'oggetto specifico con cui è registrata può utilizzare quelle funzioni.

Per esempio:

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() { ... }
};

Pertanto la classe FooUser può chiamare i metodi privati ​​di FooImplementer tramite l'interfaccia FooInterface, mentre altre classi esterne no. Questo è un ottimo modello per la gestione di callback specifici definiti come interfacce.


1
In effetti, l'eredità privata è IS-A privata.
curioso

18

Penso che la sezione critica delle domande frequenti su C ++ Lite sia:

Un utilizzo legittimo ea lungo termine per l'ereditarietà privata è quando si desidera creare una classe Fred che utilizza il codice in una classe Wilma e il codice della classe Wilma deve richiamare le funzioni membro dalla nuova classe, Fred. In questo caso, Fred chiama non virtuali in Wilma e Wilma chiama (di solito puri virtuali) in sé, che vengono sovrascritti da Fred. Questo sarebbe molto più difficile da fare con la composizione.

In caso di dubbio, dovresti preferire la composizione all'eredità privata.


4

Lo trovo utile per le interfacce (vale a dire classi astratte) che sto ereditando dove non voglio che altro codice tocchi l'interfaccia (solo la classe che eredita).

[modificato in un esempio]

Prendi l' esempio collegato a sopra. Dicendo che

[...] classe Wilma ha bisogno di richiamare le funzioni membro dalla tua nuova classe, Fred.

significa che Wilma richiede a Fred di poter invocare determinate funzioni membro, o, piuttosto, sta dicendo che Wilma è un'interfaccia . Quindi, come menzionato nell'esempio

l'eredità privata non è malvagia; è solo più costoso da mantenere, poiché aumenta la probabilità che qualcuno cambi qualcosa che rompa il tuo codice.

commenti sull'effetto desiderato dei programmatori che necessitano di soddisfare i nostri requisiti di interfaccia o di rompere il codice. E, poiché fredCallsWilma () è protetto solo gli amici e le classi derivate possono toccarlo, cioè un'interfaccia ereditata (classe astratta) che solo la classe ereditaria può toccare (e gli amici).

[modificato in un altro esempio]

Questa pagina discute brevemente delle interfacce private (da un'altra angolazione).


Non sembra davvero utile ... puoi pubblicare un esempio

Penso di capire dove stai andando ... Un tipico caso d'uso potrebbe essere che Wilma è un tipo di classe di utilità che ha bisogno di chiamare funzioni virtuali in Fred, ma altre classi non devono sapere che Fred è implementato-in-termini- di Wilma. Destra?
j_random_hacker

Sì. Vorrei sottolineare che, a quanto mi risulta, il termine "interfaccia" è più comunemente usato in Java. Quando ne ho sentito parlare per la prima volta ho pensato che avrebbe potuto essere dato un nome migliore. Poiché, in questo esempio, abbiamo un'interfaccia con cui nessuno si interfaccia nel modo in cui normalmente pensiamo alla parola.
bias

@Noos: Sì, penso che la tua affermazione "Wilma è un'interfaccia" sia un po 'ambigua, poiché la maggior parte delle persone vorrebbe che questo significhi che Wilma è un'interfaccia che Fred intende fornire al mondo , piuttosto che un contratto solo con Wilma.
j_random_hacker

@j_ Ecco perché penso che l'interfaccia sia un brutto nome. Interfaccia, il termine, non significa necessariamente per il mondo come si potrebbe pensare, ma piuttosto è una garanzia di funzionalità. In realtà, ero controverso sul termine interfaccia nel mio corso di progettazione del programma. Ma usiamo quello che ci viene dato ...
pregiudizio

2

A volte trovo utile utilizzare l'ereditarietà privata quando voglio esporre un'interfaccia più piccola (ad esempio una raccolta) nell'interfaccia di un'altra, dove l'implementazione della raccolta richiede l'accesso allo stato della classe di esposizione, in modo simile alle classi interne in Giava.

class BigClass;

struct SomeCollection
{
    iterator begin();
    iterator end();
};

class BigClass : private SomeCollection
{
    friend struct SomeCollection;
    SomeCollection &GetThings() { return *this; }
};

Quindi, se SomeCollection deve accedere a BigClass, può farlo static_cast<BigClass *>(this). Non è necessario disporre di un membro dati aggiuntivo che occupa spazio.


Non è necessaria la dichiarazione anticipata di BigClassc'è in questo esempio? Lo trovo interessante, ma mi urla in faccia.
Thomas Eding

2

Ho trovato una bella applicazione per l'eredità privata, sebbene abbia un utilizzo limitato.

Problema da risolvere

Supponiamo che ti venga fornita la seguente API 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

Ora il tuo compito è implementare questa API utilizzando C ++.

Approccio C-ish

Ovviamente potremmo scegliere uno stile di implementazione C-ish in questo modo:

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;
}

Ma ci sono diversi svantaggi:

  • Gestione manuale delle risorse (es. Memoria)
  • È facile impostare il structsbagliato
  • È facile dimenticare di liberare le risorse quando si libera il file struct
  • È C-ish

Approccio C ++

Ci è permesso usare C ++, quindi perché non usare i suoi pieni poteri?

Presentazione della gestione automatizzata delle risorse

I problemi di cui sopra sono sostanzialmente tutti legati alla gestione manuale delle risorse. La soluzione che viene in mente è ereditare Widgete aggiungere un'istanza di gestione delle risorse alla classe derivata WidgetImplper ogni variabile:

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;
};

Ciò semplifica l'implementazione come segue:

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);
}

In questo modo abbiamo risolto tutti i problemi di cui sopra. Ma un cliente può comunque dimenticare i setter WidgetImple assegnarli Widgetdirettamente ai membri.

Entra in scena l'eredità privata

Per incapsulare i Widgetmembri utilizziamo l'ereditarietà privata. Purtroppo ora abbiamo bisogno di due funzioni extra per eseguire il cast tra entrambe le classi:

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;
};

Ciò rende necessari i seguenti adattamenti:

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);
}

Questa soluzione risolve tutti i problemi. Nessuna gestione manuale della memoria ed Widgetè ben incapsulato in modo che WidgetImplnon abbia più membri di dati pubblici. Rende l'implementazione facile da usare correttamente e difficile (impossibile?) Da usare in modo sbagliato.

I frammenti di codice costituiscono un esempio di compilazione su Coliru .


1

Se la classe derivata - deve riutilizzare il codice e - non è possibile modificare la classe di base e - protegge i suoi metodi utilizzando i membri di base sotto un blocco.

allora dovresti usare l'ereditarietà privata, altrimenti corri il rischio di metodi di base sbloccati esportati tramite questa classe derivata.


1

A volte potrebbe essere un'alternativa all'aggregazione , ad esempio se si desidera l'aggregazione ma con un comportamento modificato dell'entità aggregabile (sovrascrivendo le funzioni virtuali).

Ma hai ragione, non ha molti esempi dal mondo reale.


0

Ereditarietà privata da utilizzare quando la relazione non è "è una", ma la nuova classe può essere "implementata in termini di classe esistente" o la nuova classe "funziona come" classe esistente.

esempio da "Standard di codifica C ++ di Andrei Alexandrescu, Herb Sutter": - Considera che due classi Square e Rectangle hanno ciascuna funzioni virtuali per l'impostazione della loro altezza e larghezza. Quindi Square non può ereditare correttamente da Rectangle, perché il codice che utilizza un Rectangle modificabile presumerà che SetWidth non cambi l'altezza (indipendentemente dal fatto che Rectangle documenti esplicitamente tale contratto o meno), mentre Square :: SetWidth non può preservare quel contratto e la propria ortogonalità invariante a lo stesso tempo. Ma Rectangle non può ereditare correttamente nemmeno da Square, se i clienti di Square presumono, ad esempio, che l'area di Square sia la sua larghezza al quadrato, o se si affidano a qualche altra proprietà che non vale per Rectangles.

Un quadrato "è-un" rettangolo (matematicamente) ma un quadrato non è un rettangolo (comportamentale). Di conseguenza, invece di "è-un", preferiamo dire "funziona-come-un" (o, se preferite, "utilizzabile-come-un") per rendere la descrizione meno incline a fraintendimenti.


0

Una classe contiene un invariante. L'invariante è stabilito dal costruttore. Tuttavia, in molte situazioni è utile avere una visione dello stato di rappresentazione dell'oggetto (che puoi trasmettere in rete o salvare in un file - DTO se preferisci). REST è fatto meglio in termini di AggregateType. Questo è particolarmente vero se sei corretto. Tener conto di:

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
}

A questo punto, potresti semplicemente archiviare raccolte di cache in contenitori e cercarle durante la costruzione. Comodo se c'è una vera elaborazione. Si noti che la cache fa parte del QE: le operazioni definite sul QE potrebbero significare che la cache è parzialmente riutilizzabile (ad esempio, c non influenza la somma); tuttavia, quando non c'è cache, vale la pena cercarla.

L'eredità privata può quasi sempre essere modellata da un membro (memorizzando il riferimento alla base se necessario). Non vale sempre la pena modellare in questo modo; a volte l'ereditarietà è la rappresentazione più efficiente.


0

Se hai bisogno di un std::ostreamcon alcune piccole modifiche (come in questa domanda ) potresti aver bisogno di

  1. Crea una classe MyStreambufche deriva da std::streambufe implementa le modifiche
  2. Crea una classe MyOStreamche derivi da std::ostreamche inizializza e gestisce anche un'istanza di MyStreambufe passa il puntatore a tale istanza al costruttore distd::ostream

La prima idea potrebbe essere quella di aggiungere l' MyStreamistanza come membro dati alla MyOStreamclasse:

class MyOStream : public std::ostream
{
public:
    MyOStream()
        : std::basic_ostream{ &m_buf }
        , m_buf{}
    {}

private:
    MyStreambuf m_buf;
};

Ma le classi di base vengono costruite prima di qualsiasi membro di dati, quindi stai passando un puntatore a std::streambufun'istanza non ancora costruita a std::ostreamcui è un comportamento non definito.

La soluzione è proposta nella risposta di Ben alla suddetta domanda , eredita semplicemente prima dal buffer dello stream, poi dallo stream e quindi inizializza lo stream con this:

class MyOStream : public MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

Tuttavia, la classe risultante potrebbe anche essere utilizzata come std::streambufistanza che di solito è indesiderata. Il passaggio all'eredità privata risolve questo problema:

class MyOStream : private MyStreamBuf, public std::ostream
{
public:
    MyOStream()
        : MyStreamBuf{}
        , basic_ostream{ this }
    {}
};

-1

Solo perché C ++ ha una caratteristica, non significa che sia utile o che debba essere usato.

Direi che non dovresti usarlo affatto.

Se lo stai usando comunque, beh, stai fondamentalmente violando l'incapsulamento e abbassando la coesione. Stai inserendo i dati in una classe e aggiungendo metodi che manipolano i dati in un'altra.

Come altre funzionalità C ++, può essere utilizzato per ottenere effetti collaterali come il sigillamento di una classe (come menzionato nella risposta di dribeas), ma questo non lo rende una buona funzionalità.


sei sarcastico? tutto quello che ho è un -1! comunque non lo cancellerò anche se otterrà -100 voti
hasen

9
" stai praticamente violando l'incapsulamento " Puoi fare un esempio?
curioso

1
i dati in una classe e il comportamento in un'altra suonano come un aumento della flessibilità, dal momento che possono esserci più classi di comportamento e clienti e scegliere quale di cui hanno bisogno per soddisfare ciò che vogliono
makar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.