Domanda

I risposto alla domanda su std::vector di oggetti e const correttezza , e ha ricevuto un commento su un comportamento indefinito. Non sono d'accordo, e quindi ho una domanda.

Si consideri la classe con membro const:

class A { 
public: 
    const int c; // must not be modified! 
    A(int c) : c(c) {} 
    A(const A& copy) : c(copy.c) { }     
    // No assignment operator
}; 

Voglio avere un operatore di assegnazione, ma io non voglio usare const_cast come nel codice seguente da una delle risposte:

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is undefined behavior
    return *this; 
} 

La mia soluzione è

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}  

Devo comportamento non definito (UB)?

Che cosa sarebbe una soluzione senza UB?

È stato utile?

Soluzione

Il tuo codice provoca un comportamento indefinito.

Non solo "undefined se A è utilizzato come una classe di base e questo, quello o l'altro". In realtà non definito, sempre. return *this è già UB, perché this non è garantito per fare riferimento al nuovo oggetto.

In particolare, in considerazione 3.8 / 7:

  

Se, dopo il ciclo di vita di un oggetto   è finita e prima della memorizzazione che   l'oggetto occupata viene riutilizzato o   rilasciato, un nuovo oggetto viene creato   la posizione di memoria, che la   oggetto originale occupato, un puntatore   che indicò l'oggetto originale, un   riferimento a quella di cui alla   oggetto originale, oppure il nome del   oggetto originale verrà automaticamente   fare riferimento al nuovo oggetto e, una volta che il   vita del nuovo oggetto ha   iniziato, può essere utilizzato per manipolare il   nuovo oggetto, se:

     

...

     

- il tipo di oggetto originale è   Non const-qualificato, e, se una classe   tipo, non contiene non statico   membro dati il ??cui tipo sia   const qualificato o un tipo di riferimento,

Ora, "dopo la vita di un oggetto è finita e prima della memorizzazione che l'oggetto occupata è riutilizzato o rilasciato, un nuovo oggetto viene creato il percorso di archiviazione, che l'oggetto originale occupato" è esattamente quello che stai facendo.

Il tuo oggetto è di tipo di classe, e non contiene un membro di dati non statico il cui tipo è const-qualificato. Pertanto, dopo il vostro operatore di assegnazione è stato eseguito, puntatori, riferimenti e nomi che si riferiscono alla vecchia oggetto sono non garantito per fare riferimento al nuovo oggetto e di essere utilizzabile per manipolarla.

Come esempio concreto di quello che potrebbe andare storto, considerare:

A x(1);
B y(2);
std::cout << x.c << "\n";
x = y;
std::cout << x.c << "\n";

Si aspettano questa uscita?

1
2

Sbagliato! E 'plausibile che si potrebbe ottenere in uscita, ma i membri ragione const sono un'eccezione alla norma contenuta nel 3,8 / 7, è così che il compilatore può trattare x.c come l'oggetto const che pretende di essere. In altre parole, il compilatore è consentito per il trattamento di questo codice come se fosse:

A x(1);
B y(2);
int tmp = x.c
std::cout << tmp << "\n";
x = y;
std::cout << tmp << "\n";

A causa (informalmente) oggetti const non cambiano i loro valori . Il valore potenziale di tale garanzia quando ottimizzare codice coinvolgono oggetti const dovrebbe essere ovvio. Perché vi sia alcun modo per modificare x.c senza invocando UB, questa garanzia avrebbe dovuto essere rimosso. Quindi, fintanto che gli scrittori standard sono fatto il loro lavoro senza errori, non c'è modo di fare ciò che si vuole.

[*] In realtà ho i miei dubbi sull'utilizzo this come argomento di nuova collocazione - forse si dovrebbe avere copiato a un void* prima, e usato quella. Ma io non sono preoccupato se questo particolare è UB, dal momento che non sarebbe salvare la funzione nel suo complesso.

Altri suggerimenti

In primo luogo: Quando si effettua una const membro di dati, stai dicendo al compilatore e tutto il mondo che questo membro di dati non cambia mai . Naturalmente poi non è possibile assegnare ad esso e certamente non deve ingannare il compilatore ad accettare il codice che fa così , non importa quanto intelligente il trucco.
O si può avere un membro dati const o un operatore di assegnazione assegnando a tutti i membri di dati. non si può avere entrambe le cose.

Per quanto riguarda la vostra "soluzione" al problema:
Suppongo che chiamare il distruttore su un oggetto all'interno di una funzione membro invocata per che gli oggetti potrebbe invocare UB subito. Invocare un costruttore sui dati grezzi non inizializzati per creare un oggetto all'interno di una funzione di membro che è stato richiamato per un oggetto che risiedeva dove ora il costruttore viene richiamato su dati grezzi ... anche molto molto suona come UB per me. (Inferno, appena ortografia questo fuori rende le unghie dei piedi curl.) E, no, non ho capitolo e versetto dello standard per questo. Odio leggere lo standard. Penso che non sopporto il suo metro.

Tuttavia, tecnicismi a parte, ammetto che si potrebbe ottenere via con il vostro "soluzione" su quasi ogni piattaforma fino a quando i soggiorni di codice semplici come nel tuo esempio . Eppure, questo non lo rende un bene soluzione. In realtà, direi che non è nemmeno un accettabili soluzione, perché il codice IME non rimane mai così semplice come sembra. Nel corso degli anni si otterrà esteso, cambiato, mutato, e contorto e poi sarà fallire in modo silenzioso e richiedono un noiose 36hrs spostamento di debug al fine di trovare il problema. Io non so voi, ma ogni volta che trovo un pezzo di codice come questo responsabile per 36 ore di debug divertimento voglio strangolare il miserabile stupido idiota che ha fatto questo a me.

Herb Sutter, nel suo GotW # 23 , sviscera questo pezzo idea per pezzo e, infine, conclude che "è pieno di insidie ?? , è spesso sbagliate , e rende la vita un inferno per delle classi derivate ... non usare mai il trucco di attuare assegnamento per copia in termini di copia costruzione utilizzando un distruttore esplicito seguito da nuova collocazione , anche se questo trucco affiora ogni tre mesi sui newsgroup"(sottolineare il mio).

Come si può eventualmente assegnando ad una A se ha un membro const? Si sta cercando di realizzare qualcosa che è fondamentalmente impossibile. La soluzione non ha alcun nuovo comportamento sopra l'originale, che non è necessariamente UB ma la tua è sicuramente.

Il fatto è semplice, si modifica un membro const. Si sia bisogno di non-const vostro membro, o fosso l'operatore di assegnazione. Non v'è alcuna soluzione al problema-si tratta di una contraddizione totale.

Modifica per maggiore chiarezza:

Const fusione non sempre introdurre un comportamento indefinito. Tu, però, certamente ha fatto. A parte tutto, non è definito, non chiamare tutto destructors- e non hai nemmeno chiamare il diritto one prima di voi ha in esso a meno che non si sapeva per certo che T è una classe POD. Inoltre, non c'è-time Owch undefined comportamenti connessi con le varie forme di eredità.

Lo fai invoke comportamento non definito, e si può evitare questo non cercando di assegnare ad un oggetto const.

Se sicuramente voglia di avere un immutabili (ma assegnabile) membro, quindi senza di UB si può porre le cose in questo modo:

#include <iostream>

class ConstC
{
    int c;
protected:
    ConstC(int n): c(n) {}
    int get() const { return c; }
};

class A: private ConstC
{
public:
    A(int n): ConstC(n) {}
    friend std::ostream& operator<< (std::ostream& os, const A& a)
    {
        return os << a.get();
    }
};

int main()
{
    A first(10);
    A second(20);
    std::cout << first << ' ' << second << '\n';
    first = second;
    std::cout << first << ' ' << second << '\n';
}

Avere una lettura di questo link:

http://www.informit.com/guides/content. aspx? g = cplusplus & seqNum = 368

In particolare ...

  

Questo trucco presumibilmente impedisce codice   reduplicazione. Tuttavia, ha un po '   gravi difetti. Per il lavoro, è C   destructor deve assegnare annullare ogni   puntatore che ha cancellato a causa   la successiva chiamata costruttore di copia   potrebbe eliminare nuovamente gli stessi puntatori   quando si riassegna un nuovo valore a char   array.

In assenza di altri membri (non const), questo non ha alcun senso, indipendentemente dal comportamento indefinito o meno.

A& operator=(const A& assign) 
{ 
    *const_cast<int*> (&c)= assign.c;  // very very bad, IMHO, it is UB
    return *this; 
}

Per quanto ne so, questo non è un comportamento indefinito accadendo qui perché c non è un'istanza static const, altrimenti non potrebbe invocare l'operatore di copia-assegnazione. Tuttavia, const_cast dovrebbe suonare un campanello e dirvi qualcosa non va. const_cast è stato progettato principalmente per aggirare le API non const-corretta, e non sembra essere il caso qui.

Inoltre, nel seguente frammento di codice:

A& operator=(const A& right)  
{  
    if (this == &right) return *this;  
    this->~A() 
    new (this) A(right); 
    return *this;  
}

Devi due rischi principali , il primo dei quali è già stato sottolineato.

  1. In presenza di sia un'istanza della classe derivata di A e un distruttore virtuale, questo porterà a solo parziale ricostruzione dell'istanza originale.
  2. Se la chiamata al costruttore in new(this) A(right); genera un'eccezione, l'oggetto verrà distrutto due volte. In questo caso particolare, non sarà un problema, ma se vi capita di avere un notevole pulizia, si sta andando a pentirsene.

Modifica : se la classe ha a questo utente const che non è considerato "stato" in oggetto (vale a dire che è una sorta di ID utilizzato per il monitoraggio dei casi e non fa parte di confronti in operator== e simili), allora quanto segue potrebbe avere senso:

A& operator=(const A& assign) 
{ 
    // Copy all but `const` member `c`.
    // ...

    return *this;
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top