Domanda

Considera questo classico esempio usato per spiegare cosa non a che fare con le dichiarazioni anticipate:

//in Handle.h file
class Body;

class Handle
{
   public:
      Handle();
      ~Handle() {delete impl_;}
   //....
   private:
      Body *impl_;
};

//---------------------------------------
//in Handle.cpp file

#include "Handle.h"

class Body 
{
  //Non-trivial destructor here
    public:
       ~Body () {//Do a lot of things...}
};

Handle::Handle () : impl_(new Body) {}

//---------------------------------------
//in Handle_user.cpp client code:

#include "Handle.h"

//... in some function... 
{
    Handle handleObj;

    //Do smtg with handleObj...

    //handleObj now reaches end-of-life, and BUM: Undefined behaviour
} 

Capisco dallo standard che questo caso è diretto verso UB poiché il distruttore di Body non è banale.Quello che sto cercando di capire è davvero la causa principale di tutto ciò.

Voglio dire, il problema sembra essere "innescato" dal fatto che il dtor di Handle è inline, e quindi il compilatore fa qualcosa di simile alla seguente "espansione inline" (quasi pseudo-codice qui).

inline Handle::~Handle()
{
     impl_->~Body();
     operator delete (impl_);
}

In tutte le unità di traduzione (solo Handle_user.cpp in questo caso) dove un'istanza di Handle viene distrutta, giusto?proprio non riesco a capire questo:ok, quando si genera l'espansione in linea di cui sopra, il compilatore non ha una definizione completa della classe Body, ma perché non può semplicemente avere la risoluzione del linker per impl_->~Body() cosa e quindi chiamare la funzione di distruzione del corpo che è effettivamente definita nel suo file di implementazione?

In altre parole:Capisco che al momento della distruzione di Handle il compilatore non sa nemmeno se esiste o meno un distruttore (non banale) per Body, ma perché non può fare come sempre, cioè lasciare un "segnaposto" per il linker da compilare, ed eventualmente avere un linker "esterno non risolto" se quella funzione non è davvero disponibile?

Mi sto perdendo qualcosa di grosso qui (e in tal caso mi scuso per la domanda stupida)?Se così non fosse, sono solo curioso di capire la logica alla base di tutto ciò.

È stato utile?

Soluzione

Per combinare più risposte e aggiungere il mio, senza una definizione di classe il codice chiamante non conosce:

  • se la classe ha un distruttore dichiarata, o se il distruttore di default è da utilizzare, e in tal caso se il distruttore di default è banale,
  • se il distruttore è accessibile al codice chiamante,
  • quello di base esistono classi e hanno distruttori,
  • se il distruttore è virtuale. funzione virtuale chiama in uso effetti una convenzione di chiamata diversa da quelle non-virtuali. Il compilatore non può solo "emettere il codice per chiamare ~ Body", e lasciare il linker per elaborare i dettagli in seguito,
  • (questo solo in, grazie Gman) se delete è sovraccarico per la classe.

Non è possibile chiamare qualsiasi funzione membro su un tipo incompleto per alcune o tutte queste ragioni (più un altro che non si applica ai distruttori - non si sa i parametri o il tipo di ritorno). Un distruttore non è diverso. Quindi io non sono sicuro di cosa si intende quando si dice "perché non può fare come fa sempre?".

Come già sapete, la soluzione è quella di definire il distruttore del Handle nel TU che ha la definizione di Body, stesso luogo, come si definisce ogni altra funzione di membro della Handle che chiama le funzioni o usa membri di dati di Body. Poi nel punto in cui è stato compilato delete impl_;, tutte le informazioni sono disponibili per emettere il codice per quella chiamata.

Si noti che la norma in realtà dice: 5.3.5 / 5:

  

se l'oggetto viene eliminato ha   tipo di classe incompleta al punto di   la cancellazione e la classe completa ha un   non banale distruttore o un   funzione deallocazione, il comportamento è   non definito.

Presumo che questo è il modo che è possibile eliminare un tipo di POD incompleta, proprio come si potrebbe free in C. g ++ ti dà un avvertimento abbastanza severo se lo provate, però.

Altri suggerimenti

E non sa se il distruttore sarà pubblica o meno.

Chiamare un metodo virtuale o un metodo non virtuale sono due cose totalmente diverse.

Se chiami un metodo non virtuale, il compilatore deve generare codice che faccia questo:

  • mettere tutti gli argomenti in pila
  • chiama la funzione e comunica al linker che dovrebbe risolvere la chiamata

Dato che stiamo parlando del distruttore, non ci sono argomenti da mettere nello stack, quindi sembra che possiamo semplicemente eseguire la chiamata e dire al linker di risolvere la chiamata.Nessun prototipo necessario.

Tuttavia, chiamare un metodo virtuale è completamente diverso:

  • mettere tutti gli argomenti in pila
  • ottieni il vptr dell'istanza
  • ottieni l'ennesima voce dalla vtable
  • chiamare la funzione a cui punta questa n-esima voce

Questo è completamente diverso, quindi il compilatore deve davvero sapere se stai chiamando un metodo virtuale o non virtuale.

La seconda cosa importante è che il compilatore deve sapere in quale posizione si trova il metodo virtuale nella vtable.Per questo è necessario avere anche la definizione completa della classe.

Senza una corretta dichiarazione di Body il codice Handle.h non sa se distruttore è virtual o anche accessibile (cioè pubblica).

Sono solo ipotesi, ma forse ha a che fare con la capacità degli operatori di allocazione per classe.

Cioè:

struct foo
{
    void* operator new(size_t);
    void operator delete(void*);
};

// in another header, like your example

struct foo;

struct bar
{
    bar();
    ~bar() { delete myFoo; }

    foo* myFoo;
};

// in translation unit

#include "bar.h"
#include "foo.h"

bar::bar() :
myFoo(new foo) // uses foo::operator new
{}

// but destructor uses global...!!

E ora abbiamo non corrispondenti agli operatori di allocazione, ed è entrato un comportamento indefinito. L'unico modo per garantire che non può accadere è quello di dire "fare il tipo completo". In caso contrario, è impossibile da garantire.

Questo è in realtà solo un caso speciale di chiamare un metodo (il distruttore, indirettamente). eliminare impl_ efficacemente solo chiama il distruttore e poi l'operatore appropriato delete (globale o classe). Non si può chiamare qualsiasi altro funzione su un tipo incompleto, quindi perché dovrebbe eliminare chiameremo al distruttore essere dato un trattamento speciale?

La parte io sono sicuro di quello che è complicazione provoca lo standard per renderlo undefined invece di proibire come nella chiamata di metodo.

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