membro const e assegnazione. Come evitare il comportamento indefinito?
-
30-09-2019 - |
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?
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
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.
- In presenza di sia un'istanza della classe derivata di
A
e un distruttore virtuale, questo porterà a solo parziale ricostruzione dell'istanza originale. - 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;
}