Quando dovrei usare l’ereditarietà privata del C ++?

A differenza dell’ereditarietà protetta, l’ereditarietà privata del C ++ ha trovato la sua strada nello sviluppo del C ++ mainstream. Tuttavia, non ho ancora trovato un buon uso per questo.

Quando lo usate ragazzi?

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 elegante che può essere raggiunto con l’ereditarietà privata. Mentre è fantasia, non è la risposta alla domanda.

Oltre all’utilizzo di base dell’ereditarietà privata mostrata nelle FAQ C ++ (linkate nei commenti di altri) è ansible utilizzare una combinazione di ereditarietà privata e virtuale per sigillare una class (in terminologia .NET) o per creare una finale di class (nella terminologia Java) . Questo non è un uso 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 }; 

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

FailsToDerive non verrà compilato poiché deve chiamare direttamente il costruttore ClassSealer (requisito di ereditarietà virtuale), ma non può essere privato nella class Sealed e in questo caso FailsToDerive non è un amico di ClassSealer .


MODIFICARE

È stato menzionato nei commenti che questo non poteva essere reso generico al momento utilizzando CRTP. Lo standard C ++ 11 rimuove questa limitazione fornendo una syntax diversa per gli argomenti del template di amicizia:

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

Ovviamente tutto ciò è discutibile, dal momento che C ++ 11 fornisce una parola chiave contestuale final esattamente per questo scopo:

 class Sealed final // ... 

Io lo uso per tutto il tempo. Alcuni esempi in cima alla mia testa:

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

Un esempio tipico è derivato privatamente da un contenitore STL:

 class MyVector : private vector { public: // Using declarations expose the few functions my clients need // without a load of forwarding functions. using vector::push_back; // etc... }; 
  • Quando si implementa il Pattern adattatore, ereditando privatamente dalla class adattata si salva dover inoltrare a un’istanza chiusa.
  • Per implementare un’interfaccia privata. Questo succede spesso con il pattern Observer. Tipicamente la mia class Observer, dice MyClass, si sottoscrive con alcuni Subject. Quindi, solo MyClass deve eseguire la conversione MyClass -> Observer. Il resto del sistema non ha bisogno di saperlo, quindi è indicata l’ereditarietà privata.

L’uso canonico dell’ereditarietà privata è la relazione “implementata in termini di” (grazie a “Effective C ++” di Scott Meyers per questa formulazione). In altre parole, l’interfaccia esterna della class ereditaria non ha alcuna relazione (visibile) con la class ereditata, ma la utilizza internamente per implementarne la funzionalità.

Un utile utilizzo dell’ereditarietà privata è quando si ha una class che implementa un’interfaccia, che viene quindi registrata con qualche altro object. Rendi l’interfaccia privata in modo che la class stessa debba registrarsi e solo l’object specifico con cui è registrata può utilizzare tali 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 class FooUser può chiamare i metodi privati ​​di FooImplementer tramite l’interfaccia FooInterface, mentre altre classi esterne non possono. Questo è un ottimo modello per la gestione di callback specifici definiti come interfacce.

Penso che la sezione critica del C ++ FAQ Lite sia:

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

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

Trovo utile per le interfacce (cioè le classi astratte) che sto ereditando dove non voglio che l’altro codice tocchi l’interfaccia (solo la class ereditaria).

[modificato in un esempio]

Prendi l’ esempio collegato a sopra. Dicendo che

[…] class Wilma ha bisogno di invocare funzioni membro dalla tua nuova class, Fred.

è per dire che Wilma richiede a Fred di poter richiamare determinate funzioni membro, o, piuttosto, sta dicendo che Wilma è un’interfaccia . Quindi, come menzionato nell’esempio

l’eredità privata non è ctriggers; è solo più costoso da mantenere, poiché aumenta la probabilità che qualcuno cambi qualcosa che infrange il codice.

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

[modificato in un altro esempio]

Questa pagina tratta brevemente le interfacce private (da un’altra angolazione).

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 class espositrice, 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ò static_cast(this) . Non è necessario che un membro dati aggiuntivo occupi spazio.

Se la class derivata – ha bisogno di riutilizzare il codice e – non è ansible cambiare la class base e – sta proteggendo i suoi metodi usando i membri della base sotto un lucchetto.

quindi si dovrebbe usare l’ereditarietà privata, altrimenti si corre il rischio di metodi di base sbloccati esportati tramite questa class derivata.

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

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

Ho trovato una bella applicazione per l’ereditarietà privata, sebbene abbia un uso limitato.

Problema da risolvere

Supponiamo di avere 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 usando C ++.

Approccio C-ish

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

 Widget const * loadWidget() { auto result = std::make_unique(); 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 (ad es. Memoria)
  • È facile configurare la struct errata
  • È facile dimenticare di liberare le risorse quando si libera la struct
  • È C-ish

Approccio C ++

Siamo autorizzati ad usare il C ++, quindi perché non usare tutti i suoi poteri?

Presentazione della gestione automatizzata delle risorse

I problemi di cui sopra sono fondamentalmente tutti legati alla gestione manuale delle risorse. La soluzione che viene in mente è quella di ereditare da Widget e aggiungere un’istanza di gestione delle risorse alla class derivata WidgetImpl per 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; }; 

Questo semplifica l’implementazione di quanto segue:

 Widget const * loadWidget() { auto result = std::make_unique(); 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(widget); } 

In questo modo abbiamo rimediato a tutti i problemi di cui sopra. Ma un client può ancora dimenticare i setter di WidgetImpl e assegnarlo direttamente ai membri del Widget .

L’ereditarietà privata entra in scena

Per incapsulare i membri del Widget usiamo 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(this); } static void deleteWidget(Widget const * const widget) { delete static_cast(widget); } private: std::string m_nameResource; }; 

Ciò rende necessari i seguenti adattamenti:

 Widget const * loadWidget() { auto widgetImpl = std::make_unique(); 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 e Widget sono ben incapsulati in modo che WidgetImpl non abbia più alcun membro di dati pubblici. Rende l’implementazione facile da usare correttamente e difficile (imansible?) Da usare sbagliato.

Gli snippet di codice formano un esempio di compilazione su Coliru .

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

esempio da “Standard di codifica C ++ di Andrei Alexandrescu, Herb Sutter”: – Considerare che due classi Square e Rectangle hanno ciascuna funzioni virtuali per impostarne l’altezza e la larghezza. Quindi Square non può ereditare correttamente da Rectangle, poiché il codice che utilizza un rettangolo modificabile presuppone che SetWidth non modifichi l’altezza (indipendentemente dal fatto che Rectangle documenti esplicitamente quel contratto o meno), mentre Square :: SetWidth non può conservare quel contratto e la propria invarianza di quadratura a lo stesso tempo. Ma Rettangolo non può ereditare correttamente da Square neanche, se i clienti di Square assumono per esempio che l’area di un Quadrato è la sua larghezza al quadrato, o se si basano su qualche altra proprietà che non regge per Rettangoli.

Un quadrato “è-a” rettangolo (matematicamente) ma un quadrato non è un rettangolo (comportamentale). Di conseguenza, anziché “is-a”, preferiamo dire “funziona-come-a” (o, se preferisci, “utilizzabile come documento”) per rendere la descrizione meno incline all’incomprensione.

Una class ha un valore invariante. L’invariante è stabilito dal costruttore. Tuttavia, in molte situazioni è utile avere una vista dello stato di rappresentazione dell’object (che puoi trasmettere via rete o salvare in un file – DTO se preferisci). REST è fatto meglio in termini di un AggregateType. Questo è particolarmente vero se sei corretto. Prendere in considerazione:

 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 try_read(std::istream& is); template static std::common_type< decltype(std::declval()(std::declval()), decltype(std::declval()())>::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 std::istream& operator>>(std::istream& is, std::optional); struct QuadraticEquationCache { mutable std::optional determinant_cache; mutable std::optional x1_cache; mutable std::optional x2_cache; mutable std::optional 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, è ansible archiviare solo raccolte di cache in contenitori e cercarle nella costruzione. Pratico 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 sum); eppure, quando non c’è cache, vale la pena di cercarlo.

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

Solo perché C ++ ha una funzione, non significa che sia utile o che dovrebbe essere usata.

Direi che non dovresti usarlo affatto.

Se lo stai usando comunque, beh, in pratica stai violando l’incapsulamento e abbassando la coesione. Stai mettendo i dati in una class e aggiungendo metodi che manipolano i dati in un’altra.

Come altre caratteristiche del C ++, può essere usato per ottenere effetti collaterali come sigillare una class (come menzionato nella risposta di dribeas), ma questo non lo rende una buona caratteristica.