Riutilizzare lo storage avvia la durata di un nuovo object?

#include  struct B { virtual void f(); void mutate(); virtual ~B(); }; struct D1 : B { void f(); }; struct D2 : B { void f(); }; void B::mutate() { new (this) D2; // reuses storage — ends the lifetime of *this f(); // undefined behavior - WHY???? ... = this; // OK, this points to valid memory } 

Devo essere spiegato perché f() invokation ha UB? new (this) D2; riutilizza lo storage, ma chiama anche un costruttore per D2 e da quando inizia la durata di un nuovo object. In tal caso, f() equivale a this -> f() . Cioè chiamiamo semplicemente la funzione membro f() di D2 . Chissà perché è UB?

Lo standard mostra questo esempio § 3.8 67 N3690:

 struct C { int i; void f(); const C& operator=( const C& ); }; const C& C::operator=( const C& other) { if ( this != &other ) { this->~C(); // lifetime of *this ends new (this) C(other); // new object of type C created f(); // well-defined } return *this; } C c1; C c2; c1 = c2; // well-defined c1.f(); // well-defined; c1 refers to a new object of type C 

Si noti che questo esempio sta terminando la durata dell’object prima di build il nuovo object sul posto (confrontare con il proprio codice, che non chiama il distruttore).

Ma anche se lo facessi, lo standard dice anche:

Se, al termine della vita di un object e prima che la memoria occupata dall’object sia riutilizzata o rilasciata, viene creato un nuovo object nella posizione di memoria occupata dall’object originale, un puntatore che punta all’object originale, un riferimento che riferito all’object originale, oppure il nome dell’object originale farà automaticamente riferimento al nuovo object e, una volta avviata la durata del nuovo object, potrà essere utilizzato per manipolare il nuovo object, se:

– la memoria per il nuovo object si sovrappone esattamente alla posizione di memorizzazione occupata dall’object originale e – il nuovo object è dello stesso tipo dell’object originale (ignorando i qualificatori di cv di primo livello), e

– il tipo dell’object originale non è const-qualificato e, se un tipo di class, non contiene alcun membro dati non statico il cui tipo è const-qualificato o un tipo di riferimento, e

– l’object originale era un object più derivato (1.8) di tipo T e il nuovo object è un object di derivazione più di tipo T (ovvero non sono sottooggetti di class base).

notare le parole “e”, le condizioni di cui sopra devono essere tutte soddisfatte.

Poiché non si soddisfano tutte le condizioni (si dispone di un object derivato inserito nello spazio di memoria di un object della class base), si ha un comportamento non definito quando si fa riferimento a cose con un uso implicito o esplicito di questo puntatore.

A seconda dell’implementazione del compilatore, questo potrebbe o potrebbe esplodere perché un object virtuale di class base riserva spazio per il vtable , costruendo sul posto un object di un tipo derivato che sovrascrive alcune delle funzioni virtuali significa che il vtable potrebbe essere diverso , mettere allineamento problemi e altri interni di basso livello e avrai una semplice dimensione non sufficiente per determinare se il tuo codice è giusto o meno.

Questo costrutto è molto interessante:

  • Il posizionamento-nuovo non è garantito per chiamare il distruttore dell’object. Quindi questo codice non garantirà correttamente la fine della vita dell’object.

  • Quindi in linea di principio dovresti chiamare il distruttore prima di riutilizzare l’object. Ma poi continueresti ad eseguire una funzione membro di un object che è morto. Secondo la sezione standard.9.3.1 / 2 Se una funzione membro non statica di una class X viene chiamata per un object che non è di tipo X, o di un tipo derivato da X, il comportamento non è definito.

  • Se non elimini in modo esplicito il tuo object, come fai nel tuo codice, allora ricrea un nuovo object (costruendo un secondo B senza disfare il primo, poi il secondo D2 di questo nuovo B).

Al termine della creazione del tuo nuovo object, l’id quadro dell’object corrente è stata effettivamente modificata durante l’esecuzione della funzione. Non si può essere sicuri che il puntatore alla funzione virtuale che verrà chiamata sia stato letto prima del posizionamento-nuovo (quindi il vecchio puntatore a D1 :: f) o dopo (quindi D2 :: f).

A proposito, è proprio per questo motivo che ci sono alcuni vincoli su ciò che puoi o non puoi fare in un’unione, dove una stessa memoria è condivisa per diversi oggetti attivi (vedi punto 9.5 / 2 e perticolarmente punto 9.5 / 4 nello standard).