Sottoclass / eredita contenitori standard?

Ho letto spesso queste dichiarazioni su Stack Overflow. Personalmente, non trovo alcun problema con questo, a meno che non lo stia usando in modo polimorfico; cioè dove devo usare virtual distruttore virtual .

Se voglio estendere / aggiungere la funzionalità di un contenitore standard, qual è il modo migliore di ereditarne uno? Disporre quei contenitori all’interno di una class personalizzata richiede molto più impegno ed è ancora impuro.

Ci sono una serie di ragioni per cui questa è una ctriggers idea.

Innanzitutto, questa è una ctriggers idea perché i container standard non hanno distruttori virtuali . Non dovresti mai usare qualcosa in modo polimorfico che non abbia distruttori virtuali, perché non puoi garantire la pulizia nella tua class derivata.

Regole di base per i dvd virtuali

In secondo luogo, è davvero un pessimo design. E in realtà ci sono diversi motivi per cui il design è pessimo. In primo luogo, si dovrebbe sempre estendere la funzionalità dei contenitori standard attraverso algoritmi che operano genericamente. Questo è un motivo di complessità semplice – se devi scrivere un algoritmo per ogni contenitore a cui si applica e hai contenitori M e N algoritmi, cioè i metodi M x N devi scrivere. Se scrivi genericamente i tuoi algoritmi, hai solo N algoritmi. Quindi ottieni molto più riutilizzo.

È anche un design davvero pessimo perché interrompi una buona incapsulamento ereditando dal contenitore. Una buona regola è: se puoi eseguire ciò che ti serve usando l’interfaccia pubblica di un tipo, rendi quel nuovo comportamento esterno al tipo. Questo migliora l’incapsulamento. Se si tratta di un nuovo comportamento che si desidera implementare, trasformarlo in una funzione per lo spazio dei nomi (come gli algoritmi). Se hai una nuova invariante da imporre, usa il contenimento in una class.

Una descrizione classica dell’incapsulamento

Infine, in generale, non si dovrebbe mai pensare all’eredità come mezzo per estendere il comportamento di una class. Questa è una delle grandi bugie della precedente teoria dell’OOP, dovuta al pensiero non chiaro sul riuso, e continua ad essere insegnata e promossa fino ad oggi anche se c’è una chiara teoria del perché è ctriggers. Quando si utilizza l’ereditarietà per estendere il comportamento, si sta legando tale comportamento esteso al contratto di interfaccia in modo da bind le mani degli utenti alle modifiche future. Ad esempio, supponiamo di avere una class di tipo Socket che comunica usando il protocollo TCP ed estendi il suo comportamento derivando una class SSLSocket da Socket e implementando il comportamento del protocollo di stack SSL superiore su Socket. Ora, diciamo che hai un nuovo requisito per avere lo stesso protocollo di comunicazione, ma su una linea USB o per la telefonia. Dovresti tagliare e incollare tutto ciò che funziona su una nuova class che deriva da una class USB o una class Telefonia. E ora, se trovi un bug, devi aggiustarlo in tutti e tre i posti, cosa che non sempre accadrà, il che significa che i bug impiegheranno più tempo e non verranno sempre sistemati …

Questo è generale a qualsiasi gerarchia di ereditarietà A-> B-> C -> … Quando si desidera utilizzare i comportamenti estesi nelle classi derivate, come B, C, .. su oggetti non della class base A, devi ridisegnare o stai duplicando l’implementazione. Ciò porta a progettazioni molto monolitiche che sono molto difficili da modificare (si pensi a Microsoft MFC, o loro .NET, o – beh, fanno questo errore molto). Invece, dovresti quasi sempre pensare all’estensione attraverso la composizione quando è ansible. L’ereditarietà deve essere utilizzata quando si pensa “Principio Aperto / Chiuso”. Dovresti avere le classi di base astratte e il runtime del polymorphism dinamico attraverso la class ereditata, ognuna con implementazioni complete. Le gerarchie non dovrebbero essere profonde, quasi sempre a due livelli. Usa solo più di due quando hai diverse categorie dinamiche che vanno a una varietà di funzioni che richiedono tale distinzione per la sicurezza del tipo. In questi casi, usa basi astratte fino alle classi foglia, che hanno l’implementazione.

Può essere che molte persone qui non gradiranno questa risposta, ma è ora che venga detto qualche eresia e sì … venga detto anche che “il re è nudo!”

Tutte le motivazioni contro la derivazione sono deboli. La derivazione non è diversa dalla composizione. È solo un modo per “mettere insieme le cose”. La composizione mette le cose insieme dando loro dei nomi, l’ereditarietà lo fa senza dare nomi espliciti.

Se hai bisogno di un vettore che abbia la stessa interfaccia e implementazione di std :: vect più qualcosa in più, puoi:

utilizzare la composizione e riscrivere tutte le funzioni di implementazione dei prototipi di funzione object incorporato che le delegano (e se sono 10000 … sì: essere pronti a riscrivere tutte quelle 10000) o …

ereditalo e aggiungi solo quello che ti serve (e … riscrivi i costruttori, fino a quando gli avvocati del C ++ decideranno di renderli ereditabili: ricordo ancora 10 anni fa una discussione zelota su “perché i medici non possono chiamarsi” e perché è una “ctriggers brutta cosa” … fino a quando il C ++ 11 lo ha permesso e improvvisamente tutti quei fanatici zitti zitti!) e lasciare il nuovo distruttore non virtuale come era l’originale.

Proprio come per ogni class che ha qualche metodo virtuale e altri no, sai che non puoi fingere di invocare il metodo non virtuale derivato dall’indirizzamento alla base, lo stesso vale per l’eliminazione. Non vi è alcun motivo per cui eliminare per fare finta di prestare particolare attenzione. Un programmatore che sa che ciò che non è virtuale non è richiamabile richiamando la base sa anche di non utilizzare l’eliminazione sulla base dopo aver assegnato il derivato.

Tutto “evitare questo” “non farlo” sembra sempre “moralizzazione” di qualcosa che è nativamente agnostico. Esistono tutte le caratteristiche di un linguaggio per risolvere qualche problema. Il fatto che un determinato modo per risolvere il problema sia buono o cattivo dipende dal contesto, non dalla funzionalità stessa. Se quello che stai facendo deve servire molti contenitori, l’ereditarietà probabilmente non è la via (devi rifare per tutti). Se è per un caso specifico … l’ereditarietà è un modo di comporre. Dimentica i purismi OOP: il C ++ non è un “puro OOP” e il contenitore non è affatto OOP.

Si dovrebbe astenersi dal derivare pubblicamente da contianer standard. Puoi scegliere tra ereditarietà privata e composizione e mi sembra che tutte le linee guida generali indichino che la composizione è migliore qui dato che non hai la precedenza su nessuna funzione. Non ricavare contenitori STL di forma pubblica : non ce n’è davvero bisogno.

A proposito, se vuoi aggiungere una serie di algoritmi al contenitore, considera di aggiungerli come funzioni indipendenti eseguendo un intervallo di iteratori.

Il problema è che tu, o qualcun altro, potresti passare casualmente la tua class estesa a una funzione che si aspetta un riferimento alla class base. Ciò efficacemente (e silenziosamente!) Troncerà le estensioni e creerà alcuni bug difficili da trovare.

Dover scrivere alcune funzioni di inoltro sembra un piccolo prezzo da pagare in confronto.

L’ereditare pubblicamente è un problema per tutte le ragioni che altri hanno affermato, ovvero che il tuo contenitore può essere upcasted alla class base che non ha un distruttore virtuale o un operatore di assegnazione virtuale, il che può portare a problemi di slicing .

L’ereditarietà privata, d’altra parte, è meno di un problema. Considera il seguente esempio:

 #include  #include  // private inheritance, nobody else knows about the inheritance, so nobody is upcasting my // container to a std::vector template  class MyVector : private std::vector { private: // in case I changed to boost or something later, I don't have to update everything below typedef std::vector base_vector; public: typedef typename base_vector::size_type size_type; typedef typename base_vector::iterator iterator; typedef typename base_vector::const_iterator const_iterator; using base_vector::operator[]; using base_vector::begin; using base_vector::clear; using base_vector::end; using base_vector::erase; using base_vector::push_back; using base_vector::reserve; using base_vector::resize; using base_vector::size; // custom extension void reverse() { std::reverse(this->begin(), this->end()); } void print_to_console() { for (auto it = this->begin(); it != this->end(); ++it) { std::cout < < *it << '\n'; } } }; int main(int argc, char** argv) { MyVector intArray; intArray.resize(10); for (int i = 0; i < 10; ++i) { intArray[i] = i + 1; } intArray.print_to_console(); intArray.reverse(); intArray.print_to_console(); for (auto it = intArray.begin(); it != intArray.end();) { it = intArray.erase(it); } intArray.print_to_console(); return 0; } 

PRODUZIONE:

 1 2 3 4 5 6 7 8 9 10 10 9 8 7 6 5 4 3 2 1 

Pulito e semplice, ti dà la libertà di estendere i contenitori std senza troppi sforzi.

E se pensi di fare qualcosa di sciocco, come questo:

 std::vector* stdVector = &intArray; 

Ottieni questo:

 error C2243: 'type cast': conversion from 'MyVector *' to 'std::vector> *' exists, but is inaccessible 

Perché non puoi mai garantire di non averli usati in modo polimorfico. Stai elemosinando per problemi. Prendere lo sforzo di scrivere poche funzioni non è un grosso problema e, beh, anche il solo voler farlo è dubbio al meglio. Cosa è successo all’incapsulamento?

La ragione più comune per cui si desidera ereditare dai contenitori è perché si desidera aggiungere una funzione membro alla class. Poiché stdlib stesso non è modificabile, si pensa che l’ereditarietà sia il sostituto. Questo non funziona comunque. È meglio fare una funzione libera che prende un vettore come parametro:

 void f(std::vector &v) { ... } 

Di tanto in tanto eredito dai tipi di raccolta semplicemente come un modo migliore per nominare i tipi.
Non mi piace il typedef come una questione di preferenze personali. Quindi farò qualcosa come:

 class GizmoList : public std::vector { /* No Body & no changes. Just a more descriptive name */ }; 

Quindi è molto più semplice e chiaro scrivere:

 GizmoList aList = GetGizmos(); 

Se invece inizi ad aggiungere metodi a GizmoList, potresti incontrare dei problemi.

IMHO, non trovo alcun danno nell’assegnazione di contenitori STL se sono usati come estensioni di funzionalità . (Ecco perché ho fatto questa domanda :))

Il potenziale problema si può verificare quando si tenta di passare il puntatore / riferimento del contenitore personalizzato a un contenitore standard.

 template struct MyVector : std::vector {}; std::vector* p = new MyVector; //.... delete p; // oops "Undefined Behavior"; as vector::~vector() is not 'virtual' 

Tali problemi possono essere evitati consapevolmente , a condizione che venga seguita una buona pratica di programmazione.

Se voglio fare estrema attenzione, allora posso andare fino a questo:

 #include template struct MyVector : std::vector {}; #define vector DONT_USE 

Che non consentirà l’utilizzo del vector interamente.