Domanda

Comprendo 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ò dare un buon esempio in tempo reale?

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

È stato utile?

Soluzione

  1. Probabilmente la vera ragione per cui sono consentiti 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, basta semplicemente il vecchio virtuale.

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

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

Si potrebbe 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 inventato (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.
};

Altri suggerimenti

Tutto ciò che serve per una classe astratta è almeno una funzione virtuale pura. Qualsiasi funzione farà; ma come accade, il distruttore è qualcosa che qualsiasi classe avrà & # 8212; 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. Come tale, molte guide di stile raccomandano che il destiator virtuale puro sia usato coerentemente per indicare che una classe è astratta & # 8212; se per nessun altro motivo che fornisce un posto coerente qualcuno che legge il codice può vedere per vedere se la classe è astratta.

Se vuoi creare una classe base astratta:

  • che non può essere istanziato (sì, questo è ridondante con il termine "astratto"))
  • ma richiede 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 altri comportamenti di invio virtuale per altri metodi (forse non ci sono nessun altro metodo? considera un semplice contenitore protetto "risorsa" che necessita di costruttori / destructor / assegnazione ma non molto altro)

... è più semplice rendere astratta la classe rendendo il distruttore puro virtuale e fornendo una definizione (corpo del metodo) 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 necessita dell'invio virtuale come "virtuale".

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 la vera ragione per cui sono permessi puri distruttori virtuali è che proibirli significherebbe aggiungere un'altra regola al linguaggio e non c'è bisogno di 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 i motivi menzionati da Scott Meyers nel suo libro "Più efficace C ++", voce 33 "Rendere astratte le classi non foglia", è consigliabile creare effettivamente una classe astratta myAbstractClass da cui ereditano myClassA e myClassB. Ciò fornisce una migliore astrazione e previene 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 il distruttore di IParams puro virtuale.

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; 
};

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.

Qui voglio dire quando abbiamo bisogno di distruttore virtuale e quando abbiamo bisogno di 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. Se vuoi che nessuno possa essere in grado di creare direttamente l'oggetto della classe Base, usa il distruttore virtuale puro 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.

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 è ciò che vengono chiamati.

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 come tale l'onere è nella classe proprietaria per smaltirlo o passarne la proprietà prima di distruggere se stesso.

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, innanzitutto, 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 prende 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.

Che il 99% delle volte non sarebbe 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 quel caso il puntatore Fruit chiamerà Fruit :: color () se tutto ciò che sa è che è un puntatore IS-A a Fruit.

Quindi ora l'idea di una "pura funzione virtuale" viene fuori. È 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 completamente illegali. 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 è quello che stai facendo e fa sur

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

Non voglio che nessuno sia in grado di lanciare il tipo error_base , ma i tipi di eccezione error_oh_shucks e error_oh_blast hanno funzionalità identiche e I non voglio scriverlo due volte. La complessità pImpl è necessaria per evitare di esporre std :: string ai miei clienti e l'uso di std :: 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 errore è un po 'gratuito. Salva 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>&) {
}

Forse c'è un altro CASO DI UTILIZZO REALE 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 l'interfaccia Printable e alcuni "container" tenendo qualcosa con questa interfaccia. Penso che qui sia abbastanza chiaro perché il metodo print () sia puro virtuale. Potrebbe avere un po 'di corpo ma nel caso in cui non ci sia un'implementazione predefinita, il puro virtuale è un ideale "implementazione" (= " 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 potrebbe esserci anche 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 "speciale" è stato usato il metodo (distruttore) invece di "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. & Quot;

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.

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

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

2) No, ma vuoi che sia virtuale, però.

abbiamo bisogno di rendere il distruttore virtuale a causa del fatto che, se non rendiamo il distruttore virtuale, il compilatore distruggerà solo i contenuti della classe base, n tutte le classi derivate rimarranno invariate, il compilatore bacuse non chiamerà il distruttore di qualsiasi altra classe tranne la classe base.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top