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?
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?
Risposte:
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 final
parola chiave contestuale esattamente per questo scopo:
class Sealed final // ...
Io lo uso per tutto il tempo. Alcuni esempi fuori dalla mia testa:
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...
};
push_back
, MyVector
li ottiene gratuitamente.
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 using
dichiarazione che dà, si dispone di quel mostro per ogni funzione (e non dimenticare const
e volatile
sovraccarichi!).
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à.
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.
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.
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).
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.
BigClass
c'è in questo esempio? Lo trovo interessante, ma mi urla in faccia.
Ho trovato una bella applicazione per l'eredità privata, sebbene abbia un utilizzo limitato.
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 ++.
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:
struct
sbagliatostruct
Ci è permesso usare C ++, quindi perché non usare i suoi pieni poteri?
I problemi di cui sopra sono sostanzialmente tutti legati alla gestione manuale delle risorse. La soluzione che viene in mente è ereditare Widget
e aggiungere un'istanza di gestione delle risorse alla classe 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;
};
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 WidgetImpl
e assegnarli Widget
direttamente ai membri.
Per incapsulare i Widget
membri 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 WidgetImpl
non 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 .
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.
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.
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.
Se hai bisogno di un std::ostream
con alcune piccole modifiche (come in questa domanda ) potresti aver bisogno di
MyStreambuf
che deriva da std::streambuf
e implementa le modificheMyOStream
che derivi da std::ostream
che inizializza e gestisce anche un'istanza di MyStreambuf
e passa il puntatore a tale istanza al costruttore distd::ostream
La prima idea potrebbe essere quella di aggiungere l' MyStream
istanza come membro dati alla MyOStream
classe:
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::streambuf
un'istanza non ancora costruita a std::ostream
cui è 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::streambuf
istanza 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 }
{}
};
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à.