2016-05-16 11 views
15

Solo un paio di settimane fa, ho appreso che lo standard C++ aveva una rigida regola di aliasing. Fondamentalmente, avevo fatto una domanda sullo spostamento dei bit - piuttosto che spostare ogni byte uno alla volta, per massimizzare le prestazioni volevo caricare i registri nativi del mio processore con (32 o 64 bit, rispettivamente) ed eseguire lo spostamento del 4/8 byte tutti in una singola istruzione.Rigida regola di aliasing del C++ - L'esonero di aliasing 'char' è una strada a doppio senso?

Questo è il codice che volevo evitare:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 }; 

for (int i = 0; i < 3; ++i) 
{ 
    buffer[i] <<= 4; 
    buffer[i] |= (buffer[i + 1] >> 4); 
} 
buffer[3] <<= 4; 

E invece, ho voluto usare qualcosa come:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 }; 
unsigned int *p = (unsigned int*)buffer; // unsigned int is 32 bit on my platform 
*p <<= 4; 

Qualcuno gridò in un commento che la mia soluzione proposta ha violato il C++ Regole di aliasing (perché p era di tipo int* e buffer era di tipo char* e stavo dereferenziando p per eseguire il turno. (Si prega di ignorare eventuali problemi di allineamento e ordine dei byte - gestisco quelli al di fuori di questo frammento) Sono rimasto piuttosto sorpreso t o impara a conoscere la regola di Aliasing severo dato che lavoro regolarmente su dati provenienti da buffer, lo lancio da un tipo all'altro e non ho mai avuto alcun problema. Ulteriori indagini hanno rivelato che il compilatore che uso (MSVC) non applica regole rigorose di aliasing e dal momento che sviluppo solo su gcc/g ++ nel mio tempo libero come hobby, probabilmente non avevo ancora riscontrato il problema.

Allora ho fatto una domanda sulle regole Aliasing rigorosa e C++ s 'Posizionamento nuovo operatore:

IsoCpp.org offre una FAQ per quanto riguarda il posizionamento nuovo e forniscono il seguente esempio di codice:

#include <new>  // Must #include this to use "placement new" 
#include "Fred.h"  // Declaration of class Fred 
void someCode() 
{ 
    char memory[sizeof(Fred)];  // Line #1 
    void* place = memory;   // Line #2 
    Fred* f = new(place) Fred(); // Line #3 (see "DANGER" below) 
    // The pointers f and place will be equal 
    // ... 
} 

L'esempio è abbastanza semplice, ma mi chiedo: "Che cosa succede se qualcuno chiama un metodo su f - ad es. f->talk()? A quel punto ci sarebbe il dereferenziamento f, che punta alla stessa posizione di memoria di memory (di tipo char*. Ho letto numerosi posti s che esiste un'esenzione per le variabili di tipo char* per l'alias di qualsiasi tipo, ma avevo l'impressione che non fosse una "strada a doppio senso" - ovvero, char* può alias (lettura/scrittura) qualsiasi tipo T, ma digitare T può essere utilizzato solo per alias un char* se T è di char*. Mentre sto scrivendo questo, non ha senso per me e quindi mi sto appoggiando alla convinzione che l'affermazione che il mio esempio iniziale (bit shifting) abbia violato la rigida regola dell'aliasing è falsa.

Qualcuno può spiegare, per favore, cosa è corretto? Vado dadi con il tentativo di capire ciò che è legale e ciò che non è (pur avendo letto numerosi siti web e così i messaggi sul tema)

Grazie

+3

Se chiamare funzioni membro di 'f' fosse un comportamento indefinito, ciò renderebbe il posizionamento di nuovo tipo di inutile no? – Barry

+0

"che punta alla stessa posizione di memoria della memoria (di tipo char *)" - il tipo di 'memoria' non è' char * '. Sulla linea 2 l'array decade in un puntatore, ma ciò non significa che 'memory' è un puntatore. E, anche se 'memoria' era un puntatore, il tipo di posizione della memoria a cui puntava sarebbe' char', non 'char *'. – davmac

+0

Sembra che la tua domanda riguardi se 'f-> talk()' è OK; Penso che migliorerebbe la domanda di cancellare tutto il preambolo (la roba prima di "Allora") –

risposta

6

La regola aliasing significa che il linguaggio promette soltanto i vostri dereferenziazioni di puntatori per essere valido (cioè non innescare un comportamento indefinito) se:

  • Si accede a un oggetto attraverso un puntatore di una classe compatibile: o la sua classe reale o una delle sue superclassi, cast correttamente. Ciò significa che se B è una superclasse di D e hai D* d che punta a una D valida, l'accesso al puntatore restituito da static_cast<B*>(d) è OK, ma l'accesso a quello restituito da reinterpret_cast<B*>(d) è non. Quest'ultimo può non ha dovuto tenere conto del layout dell'oggetto secondario B all'interno di D.
  • È possibile accedervi tramite un puntatore a char. Poiché char è di dimensioni in byte e allineato ai byte, non è possibile che tu possa leggere i dati da un char* mentre sei in grado di leggerlo da un D*.

Detto, altri regole nello standard (in particolare quelli descritti i componenti di matrice e tipi POD) possono essere letti come quella che si può utilizzare puntatori e reinterpret_cast<T*> di alias bidirezionale tra tipi POD e char array se si assicura di avere un array di caratteri della dimensione appropriata e l'allineamento.

In altre parole, questo è legale:

int* ia = new int[3]; 
char* pc = reinterpret_cast<char*>(ia); 
// Possibly in some other function 
int* pi = reinterpret_cast<int*>(pc); 

Mentre questo può invocare un comportamento indefinito:

char* some_buffer; size_t offset; // Possibly passed in as an argument 
int* pi = reinterpret_cast<int*>(some_buffer + offset); 
pi[2] = -5; 

Anche se siamo in grado di garantire che il buffer è abbastanza grande da contenere tre int s, l'allineamento potrebbe non essere corretto. Come con tutte le istanze di comportamento non definito, il compilatore può fare assolutamente qualsiasi cosa. Tre ricorrenze comuni potrebbero essere:

  • Il codice potrebbe essere Just Work (TM) perché nella piattaforma l'allineamento predefinito di tutte le allocazioni di memoria è uguale a quello di int.
  • Il cast puntatore potrebbe arrotondare l'indirizzo per l'allineamento di int (si parla di pi = pc & -4), rendendo potenzialmente leggere/scrittura alla memoria di sbagliato.
  • La stessa deviazione del puntatore può non riuscire in qualche modo: la CPU può rifiutare gli accessi disallineati, rendendo l'applicazione in crash.

Dal momento che si desidera sempre allontanare UB come il diavolo stesso, è necessario un array char con le dimensioni e l'allineamento corretti. Il modo più semplice per ottenerlo è semplicemente quello di iniziare con un array di tipo "giusto" (int in questo caso), quindi riempirlo con un puntatore char, che sarebbe permesso poiché int è un tipo POD.

Addendum: dopo aver utilizzato il posizionamento new, sarà possibile chiamare qualsiasi funzione sull'oggetto. Se la costruzione è corretta e non richiama UB a causa di quanto sopra, allora hai creato correttamente un oggetto nella posizione desiderata, quindi qualsiasi chiamata è OK, anche se l'oggetto era non POD (ad esempio perché aveva funzioni virtuali). Dopo tutto, qualsiasi classe allocatore will likely use placement new per creare gli oggetti nella memoria che ottengono.Si noti che questo è necessariamente vero solo se si utilizza il posizionamento new; altri usi di tipo punning (ad esempio serializzazione naïve con fread/fwrite) possono risultare in un oggetto che è incompleto o errato perché alcuni valori nell'oggetto devono essere trattati appositamente per mantenere invarianti di classe.

+0

Non '[basic.stc.dynamic.allocation]' garantisce che l'esempio * * abbia l'allineamento corretto? – Hurkyl

+0

@Hurkyl 'new' non è richiesto per restituire lo stesso indirizzo che è stato restituito dall'unità di allocazione (cioè potrebbe restituire un po 'di offset in esso). Quindi, no, non vedo il requisito di allocatore come garanzia che 'new char [3 * sizeof (int)]' restituirà necessariamente un puntatore con l'allineamento adatto per 'int', anche se l'allocatore standard è garantito per farlo . – davmac

+2

[\ [expr.new \]/11] (http://eel.is/c++draft/expr.new#11) garantisce l'allineamento corretto. Nondimeno, è UB, perché non c'è nessun oggetto 'int' vivente da nessuna parte, solo un gruppo di' char's. –

0

È un dato di fatto, la spiegazione della regola standard per quanto riguarda il puntatore, il punzonamento attraverso il rigoroso aliasing non è necessariamente corretto o facile da capire. Lo standard non menziona il "rigoroso aliasing" e trovo che la formulazione standard originale sia più facile da capire e ragionare.

In sostanza, si dice che è possibile accedere a un oggetto con un puntatore al tipo correlato adatto per accedere a questo oggetto (come lo stesso tipo o tipo di classe correlato) o tramite un puntatore a char*.

Come vedete, la questione della "strada a doppio senso" non è nemmeno applicabile.

+1

Non vedo come la domanda a due vie non sia applicabile. –

+0

"o tramite un puntatore a' char * '" - no, è attraverso un puntatore a 'char'. Hai introdotto un livello di riferimento indiretto. – davmac