Quali sono le conseguenze di nessun distruttore virtuale per questa classe base?

2

Ho trovato questo stesso codice qui: link

struct Base { virtual Base& operator+=(int) = 0; };

struct X : Base
{
    X(int n) : n_(n) { }
    X& operator+=(int n) { n_ += n; return *this; }
    int n_;
};

struct Y : Base
{
    Y(double n) : n_(n) { }
    Y& operator+=(int n) { n_ += n; return *this; }
    double n_;
};

void f(Base& x) { x += 2; } // run-time polymorphic dispatch

Ho letto molti Q & Riguardo a quando (e quando no) usare un distruttore virtuale, ma sono perplesso da questo codice di esempio. La mia comprensione da manuale del C ++ dice: "Aha! Una classe base senza un distruttore virtuale è cattiva . Può verificarsi una perdita di memoria o un comportamento indefinito." Penso anche che questo stile appaia nella libreria standard del C ++, ma non conosco un esempio in cima alla mia testa.

Che cosa accade se uso X (e Y ) come tale ...?

X* x = new X(5);
// Scenario 1
delete x;  // undefined behaviour...?  Does default dtor for 'Base' get called?
// Scenario 2
Base* b = x;
delete x;  // undefined behaviour...?

Forse sono confuso riguardo a (a) usare una classe in modo sicuro senza un distruttore virtuale contro (b) sicurezza per progetto / Non riesco a sbagliarmi.

Nel caso (a), per usare in modo sicuro, allocare solo lo stack. Ciò richiede disciplina!

Nel caso (b), aggiungere un distruttore virtuale in Base o forzare l'allocazione dello stack tramite un costruttore privato e un metodo factory statico pubblico. Questo non richiede disciplina. (Sebbene questo commento di David Rodríguez confuso me di più! "Perché dovresti farlo [prevenire l'allocazione dell'heap]?")

    
posta kevinarpe 16.08.2016 - 16:52
fonte

1 risposta

5

Nel tuo primo scenario, il comportamento è definito. Hai un puntatore a X e distruggi una X tramite quel puntatore. Il fatto che X sia derivato da B non causa problemi.

Il secondo scenario è dove sorge il grosso problema. Qui hai un comportamento indefinito, perché stai distruggendo un oggetto derivato tramite un puntatore all'oggetto base. In tal caso, deve avere un agente virtuale per ottenere un comportamento definito.

Il risultato esatto di ciò varia. In alcuni casi, il programma può bloccarsi. In altri, il dtor di base è invocato ma il dtor derivato non lo è, quindi l'oggetto è parzialmente (ma non completamente) distrutto. Ho fornito alcuni esempi concreti per un esempio, in una risposta su SO . Questo è solo un caso particolare: cambiare il compilatore o il codice potrebbe cambiare completamente il risultato.

Almeno secondo me, tentare di eliminare l'allocazione dinamica non è davvero una cura. Ad esempio, considera un codice come questo:

#include <iostream>

struct base {
    virtual void show() { std::cout << "type: base\n"; }
};


struct derived : public base {
    derived operator+(int) { return derived(); }
    virtual void show() { std::cout << "type: derived\n"; }
};

void f(derived &d) {
    base &bb{ d+1 };
    bb.show();
    // boom!
}

int main() {
    derived d;
    f(d);
}

Quando invochiamo bb.show() , esso (come previsto) mostra che il tipo è derived . Cioè, abbiamo un riferimento a base associato a un oggetto temporaneo di tipo derived . Alla fine della funzione (contrassegnato come "boom!") Quell'oggetto temporaneo di tipo derived viene distrutto tramite un riferimento a base . Non abbiamo usato alcuna allocazione dinamica (o statica), ma abbiamo ancora un comportamento indefinito.

Questo è probabilmente il motivo per cui David Rodriguez (che è un ragazzo intelligente e conosce bene il C ++ - continua a prestare attenzione a quello che dice) chiede cosa ha fatto - almeno per la situazione in discussione, prevenire l'allocazione dinamica non previene problemi .

    
risposta data 16.08.2016 - 18:05
fonte

Leggi altre domande sui tag