2012-05-07 9 views
18

Voglio ereditare da std::map, ma per quanto ne so std::map non ha alcun distruttore virtuale.C++: Ereditato da std :: map

È quindi possibile chiamare esplicitamente il distruttore std::map nel mio distruttore per garantire la corretta distruzione degli oggetti?

risposta

28

Il distruttore viene chiamato, anche se non è virtuale, ma non è questo il problema.

Si ottiene un comportamento non definito se si tenta di eliminare un oggetto del proprio tipo tramite un puntatore a std::map.

Utilizzare la composizione anziché l'ereditarietà, i contenitori std non sono destinati ad essere ereditati e non si dovrebbe.

sto supponendo che si desidera estendere la funzionalità di std::map (diciamo che si desidera trovare il valore minimo), nel qual caso si hanno due gran lunga migliore, e legale, opzioni:

1) Come suggerito la composizione, è possibile utilizzare invece:

template<class K, class V> 
class MyMap 
{ 
    std::map<K,V> m; 
    //wrapper methods 
    V getMin(); 
}; 

2) funzioni gratis:

namespace MapFunctionality 
{ 
    template<class K, class V> 
    V getMin(const std::map<K,V> m); 
} 
+9

+1 Preferire sempre la composizione anziché l'ereditarietà. Vorrei ancora che ci fosse un modo per ridurre tutto il codice di caldaia necessario per il confezionamento. – daramarak

+0

@daramarak: anche io, se solo qualcosa come "using attribute.insert;" potrebbe funzionare! D'altra parte, è piuttosto raro che in realtà tu abbia bisogno di tutti i metodi, e il wrapping dà l'opportunità di dare un nome significativo e prendere tipi di livello superiore :) –

+3

@daramarak: * Ancora vorrei che ci fosse un modo per ridurre tutto il codice di codice necessario per il wrapping *: sì, c'è: eredità. Ma i programmatori sono convinti di non doverlo usare ... perché tendono sempre a interpretarlo come "è un". Ma questo non è un requisito, solo una pubblica convinzione. –

13

voglio ereditare da std::map [...]

Perché?

Ci sono due motivi tradizionali per ereditare:

  • di riutilizzare l'interfaccia (e quindi, metodi codificati contro di essa)
  • di riutilizzare il comportamento

Il precedente non ha senso qui come map non ha alcun metodo virtual in modo da non poter modificare il suo comportamento ereditando; e il secondo è una perversione dell'uso dell'eredità che alla fine complica solo il mantenimento.


Senza una chiara idea del vostro utilizzo previsto (mancanza di contesto nella sua domanda), mi immagino che cosa si vuole veramente è quello di fornire un contenitore carta simile, con alcune operazioni di bonus. Ci sono due modi per raggiungere questo:

  • composizione: si crea un nuovo oggetto, che contiene un std::map, e fornire l'interfaccia adeguata
  • estensione: si creano nuovi free-funzioni che operano su std::map

Quest'ultimo è più semplice, tuttavia è anche più aperto: l'interfaccia originale di std::map è ancora aperta; pertanto non è adatto per limitando le operazioni.

Il primo è più pesante, senza dubbio, ma offre più possibilità.

Sta a te decidere quale dei due approcci è più adatto.

12

C'è un equivoco: l'ereditarietà - al di fuori del concetto di puro OOP, che C++ non è - non è altro che una "composizione con un membro senza nome, con una capacità di decadimento".

L'assenza di funzioni virtuali (e il distruttore non è speciale, in questo senso) rende il tuo oggetto non polimorfico, ma se quello che stai facendo è solo "riusare il comportamento ed esporre l'interfaccia nativa" l'ereditarietà fa esattamente ciò che chiesto.

I distruttori non devono essere chiamati esplicitamente l'uno dall'altro, poiché la loro chiamata è sempre concatenata dalla specifica.

#include <iostream> 
unsing namespace std; 

class A 
{ 
public: 
    A() { cout << "A::A()" << endl; } 
    ~A() { cout << "A::~A()" << endl; } 
    void hello() { cout << "A::hello()" << endl; } 
}; 

class B: public A 
{ 
public: 
    B() { cout << "B::B()" << endl; } 
    ~B() { cout << "B::~B()" << endl; } 
    void hello() { cout << "B::hello()" << endl; } 
}; 

int main() 
{ 
    B b; 
    b.hello(); 
    return 0; 
} 

stamperà

A::A() 
B::B() 
B::hello() 
B::~B() 
A::~A() 

rendendo un incorporato in B con

class B 
{ 
public: 
    A a; 
    B() { cout << "B::B()" << endl; } 
    ~B() { cout << "B::~B()" << endl; } 
    void hello() { cout << "B::hello()" << endl; } 
}; 

che uscita sarà esattamente lo stesso.

Il "Non derivare se il distruttore non è virtuale" non è una conseguenza obbligatoria del C++, ma solo una regola comunemente accettata non scritta (non c'è nulla nella specifica a riguardo: a parte una regola di cancellazione chiamata su base UB) che sorge prima del C++ 99, quando l'OOP per ereditarietà dinamica e funzioni virtuali era l'unico paradigma di programmazione supportato da C++.

Naturalmente, molti programmatori di tutto il mondo fatto le loro ossa con quel tipo di scuola (lo stesso che insegnano iostreams come primitivi, poi si trasferisce a matrice e puntatori, e l'ultima lezione l'insegnante dice "oh. ..cheh è anche l'STL che ha vettoriale, stringhe e altre funzionalità avanzate ") e oggi, anche se il C++ è diventato multiparadigm, insiste ancora con questa regola OOP pura.

Nel mio esempio A :: ~ A() non è virtuale esattamente come A :: ciao. Cosa significa?

Semplice: per lo stesso motivo di intervento A::hello non si tradurrà in chiamare B::hello, chiamando A::~A() (da delete) non comporterà B::~B(). Se è possibile accettare -in stile di programmazione- la prima affermazione, non vi è alcun motivo per cui non è possibile accettare il secondo. Nel mio esempio non c'è lo A* p = new B che riceverà delete p poiché A :: ~ A non è virtuale e So cosa significa.

Esattamente la stessa ragione che non farà, utilizzando il secondo esempio per B, A* p = &((new B)->a); con delete p;, anche se questo secondo caso, perfettamente duale con il primo, sembra chiunque non interessante per ragioni apparenti.

L'unico problema è "manutenzione", nel senso che - se il codice yopur è visualizzato da un programmatore OOP - lo rifiuterà, non perché è sbagliato di per sé, ma perché gli è stato detto di farlo.

Infatti, il "non derivare se il distruttore non è virtuale" è perché la maggior parte dei programmatori crede che ci siano troppi programmatori che non sanno di non poter chiamare eliminare su un puntatore a una base. (Scusate se questo non è educato, ma dopo 30 + anni di esperienza di programmazione non riesco a vedere qualsiasi altra ragione!)

Ma la tua domanda è diversa:

Calling B :: ~ B() (dalla cancellazione o per fine finale) risulterà sempre in A :: ~ A() poiché A (sia esso incorporato o ereditato) è in ogni caso parte di B.


A seguito delle osservazioni Luchian: il comportamento non definito accennato sopra una nei suoi commenti è legato a una cancellazione su un puntatore-a-an-object's-base senza distruttore virtuale.

Secondo la scuola OOP, ciò comporta la regola "non derivare se non esiste un distruttore virtuale".

Quello che sto sottolineando, qui, è che le ragioni di questa scuola dipendono dal fatto che ogni oggetto orientato OOP deve essere polimorfico e tutto ciò che è polimorfico deve essere indirizzabile da un puntatore a una base, per consentire la sostituzione dell'oggetto . Facendo questa affermazione, quella scuola sta deliberatamente cercando di rendere nullo l'intersezione tra derivata e non sostituibile, in modo che un puro programma OOP non verifichi quell'UB.

La mia posizione, semplicemente, ammette che C++ non è solo OOP, e non tutti gli oggetti C++ DEVONO ESSERE OOP orientati di default e, ammettere che OOP non è sempre un bisogno necessario necessariamente a servizio di sostituzione OOP.

std :: map NON è polimorfico, quindi NON è sostituibile. MyMap è lo stesso: NON polimorfo e NON sostituibile.

È sufficiente riutilizzare std :: map ed esporre la stessa interfaccia std :: map. E l'ereditarietà è solo il modo di evitare una lunga serie di funzioni riscritte che chiamano solo quelle riutilizzate.

MyMap non avrà dtor virtuale come std :: map non ne ha uno. E questo -per me- è sufficiente per dire a un programmatore C++ che questi non sono oggetti polimorfi e che non devono essere usati uno nel posto dell'altro.

Devo ammettere che questa posizione non è oggi condivisa dalla maggior parte degli esperti di C++. Ma penso (la mia unica opinione personale) questo è solo per la loro storia, che si riferisce a OOP come un dogma da servire, non a causa di un bisogno di C++. Per me il C++ non è un puro linguaggio OOP e non deve necessariamente seguire sempre il paradigma OOP, in un contesto in cui OOP non è seguito o richiesto.

+3

Stai facendo alcune dichiarazioni pericolose lì. Non considerare la necessità di un distruttore virtuale obsoleto. Lo standard ** afferma chiaramente ** che il comportamento non definito si pone nella situazione che ho citato. L'astrazione è una parte importante di OOP. Ciò significa che non si ottiene solo il riutilizzo, ma anche il tipo effettivo. Se si utilizza l'ereditarietà, in una buona progettazione, si otterrà 'std :: map *' che punta effettivamente a 'MyMap'. E se lo elimini, può succedere di tutto, incluso un crash. –

+4

@LuchianGrigore: * Lo standard afferma chiaramente che il comportamento non definito si pone nella situazione che ho citato. *. Vero, ma questa non è la situazione che ho menzionato, e non quella in cui si trova l'OP. * Significato, in una buona progettazione, se usi l'ereditarietà, finirai con std :: map * che in realtà punta a MyMap *: in generale FALSE e vero solo con OOP basato su un puntatore puro. Questo è esattamente ciò che i miei campioni NON sono. Come spieghi l'esistenza dei miei campioni, che non usano affatto il polimorfismo e i puntatori? –

+2

@LuchianGrigore: In ogni caso, penso che tu sia * corretto *: quello che sto affermando è pericoloso, ma non per la correttezza del programma, ma per la cultura basata sulla programmazione OOP! Ma non preoccuparti: la tua reazione era prevista! –