Perché abbiamo bisogno di un puro distruttore virtuale in C ++?


154

Capisco la necessità di un distruttore virtuale. Ma perché abbiamo bisogno di un puro distruttore virtuale? In uno degli articoli del C ++, l'autore ha menzionato il fatto che usiamo un puro distruttore virtuale quando vogliamo fare un abstract di classe.

Ma possiamo fare un abstract di classe rendendo virtuale qualsiasi delle funzioni membro.

Quindi le mie domande sono

  1. Quando rendiamo davvero un distruttore puro virtuale? Qualcuno può fare un buon esempio in tempo reale?

  2. Quando stiamo creando classi astratte è una buona pratica rendere il distruttore anche puro virtuale? Se sì ... allora perché?



14
@ Daniel- I link citati non rispondono alla mia domanda. Risponde al motivo per cui un puro distruttore virtuale dovrebbe avere una definizione. La mia domanda è: perché abbiamo bisogno di un puro distruttore virtuale.
Segna il

Stavo cercando di scoprire il motivo, ma hai già fatto la domanda qui.
nsivakr,

Risposte:


119
  1. Probabilmente il vero motivo per cui sono permessi puri distruttori virtuali è che proibirli significherebbe aggiungere un'altra regola al linguaggio e non è necessario per questa regola poiché nessun effetto negativo può derivare dal consentire un puro distruttore virtuale.

  2. No, il semplice vecchio virtuale è abbastanza.

Se si crea un oggetto con implementazioni predefinite per i suoi metodi virtuali e si desidera renderlo astratto senza forzare nessuno a sovrascrivere alcun metodo specifico , è possibile rendere il distruttore puro virtuale. Non ci vedo molto, ma è possibile.

Si noti che poiché il compilatore genererà un distruttore implicito per le classi derivate, se l'autore della classe non lo fa, le classi derivate non saranno astratte. Pertanto, avere il puro distruttore virtuale nella classe base non farà alcuna differenza per le classi derivate. Renderà solo la classe base astratta (grazie per il commento di @kappa ).

Si può anche supporre che ogni classe derivante avrebbe probabilmente bisogno di avere un codice di pulizia specifico e usare il puro distruttore virtuale come promemoria per scriverne uno, ma questo sembra forzato (e non rinforzato).

Nota: il distruttore è l'unico metodo che anche se è puro virtuale deve avere un'implementazione per istanziare le classi derivate (sì, le funzioni virtuali pure possono avere implementazioni).

struct foo {
    virtual void bar() = 0;
};

void foo::bar() { /* default implementation */ }

class foof : public foo {
    void bar() { foo::bar(); } // have to explicitly call default implementation.
};

13
"sì, le funzioni virtuali pure possono avere implementazioni" Quindi non è virtual puro.
GManNickG,

2
Se vuoi rendere un abstract di classe, non sarebbe più semplice proteggere tutti i costruttori?
bdonlan,

78
@GMan, ti sbagli, essendo puro virtuale significa che le classi derivate devono sovrascrivere questo metodo, questo è ortogonale ad avere un'implementazione. Controlla il mio codice e commenta foof::barse vuoi vedere di persona.
Motti,

15
@GMan: la FAQ di C ++ dice "Nota che è possibile fornire una definizione per una pura funzione virtuale, ma questo di solito confonde i principianti ed è meglio evitarlo fino a dopo." parashift.com/c++-faq-lite/abcs.html#faq-22.4 Anche Wikipedia (quel bastione della correttezza) dice lo stesso. Credo che lo standard ISO / IEC usi una terminologia simile (sfortunatamente la mia copia è al momento funzionante) ... Sono d'accordo che è confuso, e generalmente non uso il termine senza chiarimenti quando fornisco una definizione, specialmente intorno ai programmatori più recenti ...
magro il

9
@Motti: Ciò che è interessante qui e fornisce più confusione è che il puro distruttore virtuale NON ha bisogno di essere esplicitamente scavalcato nella classe derivata (e istanziata). In tal caso viene utilizzata la definizione implicita :)
kappa,

33

Tutto ciò che serve per una classe astratta è almeno una pura funzione virtuale. Qualsiasi funzione farà; ma come succede, il distruttore è qualcosa che qualsiasi classe avrà - quindi è sempre lì come candidato. Inoltre, rendere il distruttore puro virtuale (anziché solo virtuale) non ha effetti collaterali comportamentali se non quello di rendere astratta la classe. Di conseguenza, molte guide di stile raccomandano che il puro distruttore virtuale sia usato in modo coerente per indicare che una classe è astratta, se non altro per fornire un posto coerente in cui qualcuno che legge il codice può vedere se la classe è astratta.


1
ma ancora perché fornire l'implementazione del puro distruttore virtaul. Che cosa potrebbe andare storto se faccio un distruttore puro virtuale e non fornisce la sua implementazione. Suppongo che vengano dichiarati solo i puntatori delle classi base e quindi il distruttore per la classe astratta non viene mai chiamato.
Krishna Oza,

4
@Surfing: perché un distruttore di una classe derivata chiama implicitamente il distruttore della sua classe base, anche se quel distruttore è puro virtuale. Quindi, se non vi è alcuna implementazione per esso, si verificherà un comportamento indefinito.
a.peganz,

19

Se si desidera creare una classe base astratta:

  • che non può essere istanziato (sì, questo è ridondante con il termine "astratto"!)
  • ma necessita di un comportamento di distruttore virtuale (si intende portare con sé puntatori all'ABC piuttosto che puntatori ai tipi derivati ​​ed eliminarli attraverso di essi)
  • ma non ha bisogno di alcun altro comportamento di spedizione virtuale per altri metodi (forse non ci sono altri metodi? si consideri un semplice contenitore "risorsa" protetto che necessita di costruttori / distruttori / assegnazione ma non molto altro)

... è più semplice rendere astratta la classe rendendo il distruttore puro virtuale e fornendo una definizione (metodo body) per esso.

Per la nostra ipotetica ABC:

Garantisci che non può essere istanziato (anche interno alla classe stessa, ecco perché i costruttori privati ​​potrebbero non essere sufficienti), ottieni il comportamento virtuale che desideri per il distruttore e non devi trovare e taggare un altro metodo che non è necessario un invio virtuale come "virtuale".


8

Dalle risposte che ho letto alla tua domanda, non sono riuscito a dedurre una buona ragione per usare effettivamente un puro distruttore virtuale. Ad esempio, il seguente motivo non mi convince affatto:

Probabilmente il vero motivo per cui sono permessi puri distruttori virtuali è che proibirli significherebbe aggiungere un'altra regola al linguaggio e non è necessario per questa regola poiché nessun effetto negativo può derivare dal consentire un puro distruttore virtuale.

Secondo me, i puri distruttori virtuali possono essere utili. Ad esempio, supponi di avere due classi myClassA e myClassB nel tuo codice e che myClassB erediti da myClassA. Per le ragioni menzionate da Scott Meyers nel suo libro "Più efficace C ++", voce 33 "Rendere astratte le classi non foglia", è buona pratica creare effettivamente una classe astratta myAbstractClass da cui ereditano myClassA e myClassB. Ciò fornisce una migliore astrazione e impedisce alcuni problemi derivanti, ad esempio, dalle copie degli oggetti.

Nel processo di astrazione (della creazione della classe myAbstractClass), può essere che nessun metodo di myClassA o myClassB sia un buon candidato per essere un metodo virtuale puro (che è un prerequisito per myAbstractClass per essere astratto). In questo caso, definisci il distruttore della classe astratta puro virtuale.

Di seguito un esempio concreto di un codice che ho scritto io stesso. Ho due classi, Numerics / PhysicsParams che condividono proprietà comuni. Li lascio quindi ereditare dalla classe astratta IParams. In questo caso, non avevo assolutamente nessun metodo a portata di mano che potesse essere puramente virtuale. Il metodo setParameter, ad esempio, deve avere lo stesso corpo per ogni sottoclasse. L'unica scelta che ho avuto è stata quella di rendere virtuale il distruttore di IParams.

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};

1
Mi piace questo uso, ma un altro modo per "far rispettare" l'eredità è dichiarare il costruttore IParamprotetto, come è stato notato in qualche altro commento.
rwols

4

Se si desidera interrompere l'istanza della classe base senza apportare modifiche alla classe derivata già implementata e testata, si implementa un distruttore virtuale puro nella classe base.


3

Qui voglio dire quando abbiamo bisogno di un distruttore virtuale e quando abbiamo bisogno di un puro distruttore virtuale

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. Quando vuoi che nessuno dovrebbe essere in grado di creare direttamente l'oggetto della classe Base, usa il puro distruttore virtuale virtual ~Base() = 0. Di solito è necessaria almeno una funzione virtuale pura, prendiamo virtual ~Base() = 0, come questa funzione.

  2. Quando non hai bisogno della cosa sopra, solo tu hai bisogno della distruzione sicura dell'oggetto classe Derivato

    Base * pBase = new Derived (); elimina pBase; non è richiesto il puro distruttore virtuale, solo il distruttore virtuale farà il lavoro.


2

Stai entrando in ipotesi con queste risposte, quindi cercherò di fare una spiegazione più semplice, più concreta per amor di chiarezza.

Le relazioni di base del design orientato agli oggetti sono due: IS-A e HAS-A. Non li ho inventati. Questo è come si chiamano.

IS-A indica che un particolare oggetto si identifica come appartenente alla classe che si trova sopra di esso in una gerarchia di classi. Un oggetto banana è un oggetto frutta se è una sottoclasse della classe frutta. Ciò significa che ovunque una classe di frutta può essere utilizzata, una banana può essere utilizzata. Tuttavia, non è riflessivo. Non è possibile sostituire una classe base per una classe specifica se tale classe specifica è richiesta.

Has-a ha indicato che un oggetto fa parte di una classe composita e che esiste una relazione di proprietà. Significa in C ++ che è un oggetto membro e in quanto tale spetta alla classe proprietaria disporne o consegnarne la proprietà prima di distruggersi.

Questi due concetti sono più facili da realizzare in linguaggi a eredità singola che in un modello di ereditarietà multipla come c ++, ma le regole sono essenzialmente le stesse. La complicazione arriva quando l'identità di classe è ambigua, come passare un puntatore di classe Banana in una funzione che accetta un puntatore di classe Fruit.

Le funzioni virtuali sono, in primo luogo, una cosa di runtime. Fa parte del polimorfismo in quanto viene utilizzato per decidere quale funzione eseguire nel momento in cui viene chiamata nel programma in esecuzione.

La parola chiave virtuale è una direttiva del compilatore per associare le funzioni in un determinato ordine in caso di ambiguità sull'identità della classe. Le funzioni virtuali sono sempre nelle classi principali (per quanto ne so) e indicano al compilatore che l'associazione delle funzioni membro ai loro nomi dovrebbe avvenire prima con la funzione della sottoclasse e successivamente con la funzione della classe genitore.

Una classe Fruit potrebbe avere una funzione virtuale color () che restituisce "NONE" per impostazione predefinita. La funzione color () della classe Banana restituisce "GIALLO" o "MARRONE".

Ma se la funzione che assume un puntatore Fruit chiama color () sulla classe Banana che gli viene inviata - quale funzione color () viene invocata? La funzione normalmente chiamerebbe Fruit :: color () per un oggetto Fruit.

Sarebbe il 99% delle volte non quello che era previsto. Ma se Fruit :: color () fosse dichiarato virtuale, allora Banana: color () verrebbe chiamato per l'oggetto perché la funzione color () corretta sarebbe legata al puntatore Fruit al momento della chiamata. Il runtime controlla l'oggetto a cui punta il puntatore perché è stato contrassegnato come virtuale nella definizione della classe Fruit.

Ciò è diverso dall'override di una funzione in una sottoclasse. In tal caso, il puntatore Fruit chiamerà Fruit :: color () se tutto ciò che sa è che è un puntatore IS-A a Fruit.

Quindi ora arriva l'idea di una "pura funzione virtuale". È una frase piuttosto sfortunata poiché la purezza non ha nulla a che fare con essa. Significa che è inteso che il metodo della classe base non deve mai essere chiamato. In effetti una funzione virtuale pura non può essere chiamata. Deve comunque essere definito. Deve esistere una firma di funzione. Molti programmatori eseguono un'implementazione vuota {} per completezza, ma il compilatore ne genererà uno internamente in caso contrario. In quel caso quando la funzione viene chiamata anche se il puntatore è su Fruit, verrà chiamato Banana :: color () in quanto è l'unica implementazione di color () che esiste.

Ora l'ultimo pezzo del puzzle: costruttori e distruttori.

I costruttori virtuali puri sono illegali, completamente. Questo è appena uscito.

Ma i puri distruttori virtuali funzionano nel caso in cui si desideri vietare la creazione di un'istanza di classe base. Solo le sottoclassi possono essere istanziate se il distruttore della classe base è virtuale puro. la convenzione è assegnarla a 0.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

In questo caso devi creare un'implementazione. Il compilatore sa che è ciò che stai facendo e si assicura che lo faccia nel modo giusto, oppure si lamenta con forza che non può collegarsi a tutte le funzioni che deve compilare. Gli errori possono essere fonte di confusione se non sei sulla buona strada per modellare la gerarchia di classi.

Quindi in questo caso è vietato creare istanze di Fruit, ma è consentito creare istanze di Banana.

Una chiamata per eliminare il puntatore Fruit che punta a un'istanza di Banana chiamerà prima Banana :: ~ Banana () e poi chiamerà Fuit :: ~ Fruit (), sempre. Perché, qualunque cosa accada, quando chiami un distruttore di sottoclasse, il distruttore della classe base deve seguire.

È un cattivo modello? È più complicato in fase di progettazione, sì, ma può garantire che venga eseguito il collegamento corretto in fase di esecuzione e che venga eseguita una funzione di sottoclasse in cui vi sia ambiguità su quale sottoclasse si acceda esattamente.

Se si scrive C ++ in modo da passare solo intorno a puntatori di classe esatti senza puntatori generici né ambigui, le funzioni virtuali non sono realmente necessarie. Ma se si richiedono flessibilità di runtime dei tipi (come in Apple Banana Orange ==> Fruit) le funzioni diventano più facili e più versatili con un codice meno ridondante. Non devi più scrivere una funzione per ogni tipo di frutto e sai che ogni frutto risponderà a color () con la sua funzione corretta.

Spero che questa spiegazione prolissa consolidi il concetto piuttosto che confondere le cose. Ci sono molti buoni esempi là fuori da guardare e guardare abbastanza e in realtà eseguirli e pasticciare con loro e lo otterrai.


1

Questo è un argomento vecchio di dieci anni :) Leggi gli ultimi 5 paragrafi dell'articolo 7 sul libro "C ++ efficace" per i dettagli, inizia da "Occasionalmente può essere conveniente dare a una classe un puro distruttore virtuale ...."


0

Hai chiesto un esempio e credo che quanto segue fornisca una ragione per un puro distruttore virtuale. Non vedo l'ora di rispondere se questo è un bene ragione ...

Non voglio che nessuno sia in grado di lanciare il error_basetipo, ma i tipi di eccezione error_oh_shuckse error_oh_blasthanno funzionalità identiche e non voglio scriverlo due volte. La complessità pImpl è necessaria per evitare l'esposizione std::stringai miei clienti e l'uso distd::auto_ptr richiede il costruttore di copie.

L'intestazione pubblica contiene le specifiche di eccezione che saranno disponibili per il client per distinguere i diversi tipi di eccezione generati dalla mia libreria:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

Ed ecco l'implementazione condivisa:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

La classe exception_string, mantenuta privata, nasconde std :: string dalla mia interfaccia pubblica:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

Il mio codice quindi genera un errore come:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

L'uso di un modello per errorè un po 'gratuito. Risparmia un po 'di codice a spese di richiedere ai clienti di rilevare errori come:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}

0

Forse c'è un altro REAL-CASE di puro distruttore virtuale che in realtà non riesco a vedere in altre risposte :)

Inizialmente, sono completamente d'accordo con la risposta marcata: è perché proibire il puro distruttore virtuale avrebbe bisogno di una regola aggiuntiva nelle specifiche del linguaggio. Ma non è ancora il caso d'uso che Mark richiede :)

Prima immagina questo:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

e qualcosa del genere:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

Semplicemente: abbiamo un'interfaccia Printablee alcuni "container" che contengono qualcosa con questa interfaccia. Penso che qui sia abbastanza chiaro perché il print()metodo sia puro virtuale. Potrebbe avere qualche corpo ma nel caso in cui non ci sia un'implementazione predefinita, il puro virtuale è una "implementazione" ideale (= "deve essere fornito da una classe discendente").

E ora immagina esattamente lo stesso tranne che non è per la stampa ma per la distruzione:

class Destroyable {
  virtual ~Destroyable() = 0;
};

E inoltre potrebbe esserci un contenitore simile:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

È un caso d'uso semplificato dalla mia vera applicazione. L'unica differenza qui è che è stato usato il metodo "speciale" (distruttore) anziché "normale"print() . Ma il motivo per cui è puro virtuale è sempre lo stesso: non esiste un codice predefinito per il metodo. Un po 'di confusione potrebbe essere il fatto che DEVE esserci un distruttore efficacemente e il compilatore in realtà genera un codice vuoto per esso. Ma dal punto di vista di un programmatore la pura virtualità significa ancora: "Non ho alcun codice predefinito, deve essere fornito da classi derivate".

Penso che non sia una grande idea qui, solo una maggiore spiegazione che la pura virtualità funziona davvero in modo uniforme - anche per i distruttori.


-2

1) Quando si desidera richiedere la pulizia delle classi derivate. Questo è raro

2) No, ma vuoi che sia virtuale, comunque.


-2

dobbiamo rendere virtuale il distruttore perché, se non rendiamo virtuale il distruttore, il compilatore distruggerà solo i contenuti della classe base, n tutte le classi derivate rimarranno invariate, il compilatore bacuse non chiamerà il distruttore di nessun altro classe tranne la classe base.


-1: la domanda non riguarda il motivo per cui un distruttore dovrebbe essere virtuale.
Troubadour,

Inoltre, in determinate situazioni i distruttori non devono essere virtuali per ottenere la corretta distruzione. I distruttori virtuali sono necessari solo quando finisci per chiamare deleteun puntatore alla classe base quando in realtà punta alla sua derivata.
CygnusX1

Hai ragione al 100%. Questa è ed è stata in passato una delle fonti numero uno di perdite e arresti anomali nei programmi C ++, in terzo luogo solo per cercare di fare cose con puntatori nulli e superare i limiti delle matrici. Un distruttore di classe base non virtuale verrà chiamato su un puntatore generico, ignorando del tutto il distruttore della sottoclasse se non è contrassegnato come virtuale. Se ci sono oggetti creati dinamicamente che appartengono alla sottoclasse, non verranno recuperati dal distruttore di base in una chiamata da eliminare. Stai andando bene, allora BLUURRK! (difficile da trovare anche dove.)
Chris Reid,
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.