Qual è la regola rigorosa di aliasing?

Quando si fa una domanda sul comportamento non definito comune in C , le anime sono più illuminate di quanto facesse riferimento alla rigida regola di aliasing.
Di cosa stanno parlando?

Una tipica situazione in cui si verificano problemi di aliasing è quando si sovrappone una struct (come un device / network msg) su un buffer della dimensione della parola del proprio sistema (come un puntatore a uint32_t o uint16_t s). Quando sovrapponi una struttura su un tale buffer, o un buffer su una tale struttura attraverso il cast del puntatore, puoi facilmente violare le rigide regole di aliasing.

Quindi, in questo tipo di configurazione, se voglio inviare un messaggio a qualcosa, dovrei avere due puntatori incompatibili che puntano allo stesso blocco di memoria. Potrei quindi codificare in modo ingenuo qualcosa del genere:

 typedef struct Msg { unsigned int a; unsigned int b; } Msg; void SendWord(uint32_t); int main(void) { // Get a 32-bit buffer from the system uint32_t* buff = malloc(sizeof(Msg)); // Alias that buffer through message Msg* msg = (Msg*)(buff); // Send a bunch of messages for (int i =0; i < 10; ++i) { msg->a = i; msg->b = i+1; SendWord(buff[0]); SendWord(buff[1]); } } 

La rigorosa regola di aliasing rende questa impostazione illegale: il dereferenziamento di un puntatore che alias un object che non è di un tipo compatibile o uno degli altri tipi consentiti da C 2011 6.5 paragrafo 7 1 è un comportamento indefinito. Sfortunatamente, puoi ancora scrivere codice in questo modo, magari ricevere degli avvertimenti, farlo compilare bene, solo per avere strani comportamenti inaspettati quando esegui il codice.

(GCC appare piuttosto incoerente nella sua capacità di dare avvertimenti di aliasing, a volte dandoci un avvertimento amichevole e qualche volta no.)

Per capire perché questo comportamento è indefinito, dobbiamo pensare a cosa la rigida regola di aliasing compra il compilatore. Fondamentalmente, con questa regola, non deve pensare di inserire istruzioni per aggiornare il contenuto del buff ogni volta che si esegue il ciclo. Invece, quando si ottimizzano, con alcune assunzioni fastidiosamente non forzate sull’aliasing, possono omettere quelle istruzioni, caricare il buff[0] e buff[1 ] nei registri della CPU una volta prima dell’esecuzione del ciclo e accelerare il corpo del ciclo. Prima che fosse introdotto il rigoroso aliasing, il compilatore doveva vivere in uno stato di paranoia che il contenuto del buff poteva cambiare in qualsiasi momento da qualunque luogo. Quindi, per ottenere un vantaggio in termini di prestazioni extra e presumendo che la maggior parte delle persone non utilizzi puntatori di punteggiatura tipografica, è stata introdotta la rigorosa regola di aliasing.

Tenete a mente, se pensate che l’esempio sia inventato, questo potrebbe accadere anche se state passando un buffer ad un’altra funzione facendo l’invio per voi, se invece lo avete.

 void SendMessage(uint32_t* buff, size_t size32) { for (int i = 0; i < size32; ++i) { SendWord(buff[i]); } } 

E abbiamo riscritto il nostro ciclo precedente per sfruttare questa comoda funzione

 for (int i = 0; i < 10; ++i) { msg->a = i; msg->b = i+1; SendMessage(buff, 2); } 

Il compilatore potrebbe o potrebbe non essere abbastanza o abbastanza intelligente da provare a inviare SendMessage e potrebbe decidere di caricare o meno il buff di nuovo. Se SendMessage fa parte di un'altra API compilata separatamente, probabilmente contiene istruzioni per caricare i contenuti del buff. Poi di nuovo, forse sei in C ++ e questa è solo una versione di intestazione basata su modelli che il compilatore pensa possa essere in linea. O forse è solo qualcosa che hai scritto nel tuo file .c per tua comodità. In ogni caso, un comportamento indefinito potrebbe comunque verificarsi. Anche quando conosciamo qualcosa di ciò che sta accadendo sotto il cofano, è ancora una violazione della regola, quindi non è garantito un comportamento ben definito. Quindi, semplicemente avvolgendo una funzione che prende il nostro buffer delimitato dalla parola, non è necessariamente d'aiuto.

Quindi come faccio ad aggirare questo?

  • Usa un sindacato. La maggior parte dei compilatori supporta questo senza lamentarsi del rigoroso aliasing. Questo è permesso in C99 e permesso esplicitamente in C11.

     union { Msg msg; unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)]; }; 
  • Puoi disabilitare il rigoroso aliasing nel tuo compilatore ( f [no-] strict-aliasing in gcc))

  • Puoi usare char* per l'aliasing invece della parola del tuo sistema. Le regole consentono un'eccezione per char* (incluso signed char e unsigned char ). Si presume sempre che char* alias altri tipi. Tuttavia, questo non funzionerà nell'altro modo: non ci sono supposizioni che la tua strucse alias un buffer di caratteri.

Attenzione per principianti

Questo è solo un potenziale campo minato quando si sovrappongono due tipi l'uno sull'altro. Dovresti anche imparare l' endianness , l' allineamento delle parole e come affrontare i problemi di allineamento attraverso il packing delle strutture correttamente.

Nota

1 I tipi che C 2011 6.5 7 consente a un lvalue di accedere sono:

  • un tipo compatibile con il tipo effettivo dell'object,
  • una versione qualificata di un tipo compatibile con il tipo effettivo dell'object,
  • un tipo che è il tipo firmato o non firmato corrispondente al tipo effettivo dell'object,
  • un tipo che è il tipo firmato o senza segno corrispondente a una versione qualificata del tipo effettivo dell'object,
  • un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (incluso, in modo ricorsivo, un membro di un'unione subaggregata o contenuta), o
  • un tipo di personaggio.

La migliore spiegazione che ho trovato è di Mike Acton, Understanding Aliasing . Si concentra un pò sullo sviluppo di PS3, ma fondamentalmente è solo GCC.

Dall’articolo:

“Aliasing rigoroso è un’ipotesi, fatta dal compilatore C (o C ++), che i riferimenti di dereferenziazione a oggetti di tipi diversi non si riferiranno mai alla stessa locazione di memoria (cioè alias l’un l’altro).”

Quindi, in pratica se hai un int* punta a qualche memoria contenente un int e poi punti un float* a quella memoria e lo usi come float infrangi la regola. Se il tuo codice non rispetta questo, l’ottimizzatore del compilatore molto probabilmente romperà il tuo codice.

L’eccezione alla regola è un char* , che può indicare qualsiasi tipo.

Questa è la regola di aliasing rigida, trovata nella sezione 3.10 dello standard C ++ 03 (altre risposte forniscono una buona spiegazione, ma nessuna ha fornito la regola stessa):

Se un programma tenta di accedere al valore memorizzato di un object tramite un lvalue diverso da uno dei seguenti tipi, il comportamento non è definito:

  • il tipo dinamico dell’object,
  • una versione cv-qualificata del tipo dinamico dell’object,
  • un tipo che è il tipo firmato o senza segno corrispondente al tipo dinamico dell’object,
  • un tipo che è il tipo firmato o senza segno corrispondente a una versione qualificata CV del tipo dinamico dell’object,
  • un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (incluso, in modo ricorsivo, un membro di un sindacato subaggregato o contenuto),
  • un tipo che è un tipo di class base (possibilmente qualificato cv) del tipo dinamico dell’object,
  • un tipo di char o unsigned char .

Formulazione C ++ 11 e C ++ 14 (modifiche enfatizzate):

Se un programma tenta di accedere al valore memorizzato di un object tramite un glValue diverso da uno dei seguenti tipi, il comportamento non è definito:

  • il tipo dinamico dell’object,
  • una versione cv-qualificata del tipo dinamico dell’object,
  • un tipo simile (come definito in 4.4) al tipo dinamico dell’object,
  • un tipo che è il tipo firmato o senza segno corrispondente al tipo dinamico dell’object,
  • un tipo che è il tipo firmato o senza segno corrispondente a una versione qualificata CV del tipo dinamico dell’object,
  • un tipo aggregato o di unione che include uno dei tipi sopra menzionati tra i suoi elementi o membri di dati non statici (incluso, in modo ricorsivo, un elemento o un membro di dati non statici di una unione parziale o contenuta),
  • un tipo che è un tipo di class base (possibilmente qualificato cv) del tipo dinamico dell’object,
  • un tipo di char o unsigned char .

Due modifiche erano piccole: glvalue invece di lvalue e chiarimenti del caso aggregato / unione.

Il terzo cambiamento rende più forte la garanzia (rilassa la forte regola dell’aliasing): il nuovo concetto di tipi simili che ora sono alias sicuri.


Anche la dicitura C (C99; ISO / IEC 9899: 1999 6.5 / 7; la stessa identica formulazione è utilizzata in ISO / IEC 9899: 2011 §6.5 ¶7):

Un object deve avere il suo valore memorizzato accessibile solo da un’espressione lvalue che ha uno dei seguenti tipi 73) o 88) :

  • un tipo compatibile con il tipo effettivo dell’object,
  • una versione quali fi cata di un tipo compatibile con il tipo effettivo dell’object,
  • un tipo che è il tipo firmato o non firmato corrispondente al tipo effettivo dell’object,
  • un tipo che è il tipo firmato o senza segno corrispondente a una versione quali fi cata del tipo effettivo dell’object,
  • un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (incluso, in modo ricorsivo, un membro di un’unione subaggregata o contenuta), o
  • un tipo di personaggio.

73) o 88) L’intento di questa lista è di specificare quelle circostanze in cui un object può o non può essere aliasato.

Aliasing rigoroso non si riferisce solo ai puntatori, influisce anche sui riferimenti, ne ho scritto uno per il wiki dello sviluppatore boost ed è stato così ben accolto che l’ho trasformato in una pagina del mio sito di consulenza. Spiega completamente di cosa si tratta, perché confonde così tanto le persone e cosa fare al riguardo. Libro bianco aliasing rigoroso . In particolare, spiega perché i sindacati sono un comportamento rischioso per C ++ e perché l’utilizzo di memcpy è l’unica soluzione portatile su C e C ++. Spero che questo sia utile.

Come addendum a ciò che Doug T. ha già scritto, ecco un semplice caso di test che probabilmente lo innesca con gcc:

check.c

 #include  void check(short *h,long *k) { *h=5; *k=6; if (*h == 5) printf("strict aliasing problem\n"); } int main(void) { long k[1]; check((short *)k,k); return 0; } 

Compilare con gcc -O2 -o check check.c . Solitamente (con la maggior parte delle versioni di gcc che ho provato) questo produce “un rigoroso problema di aliasing”, perché il compilatore presuppone che “h” non possa essere lo stesso indirizzo di “k” nella funzione “check”. Per questo motivo il compilatore ottimizza il if (*h == 5) distanza e chiama sempre il printf.

Per chi fosse interessato ecco il codice assemblatore x64, prodotto da gcc 4.6.3, eseguito su ubuntu 12.04.2 per x64:

 movw $5, (%rdi) movq $6, (%rsi) movl $.LC0, %edi jmp puts 

Quindi la condizione if è completamente scomparsa dal codice assembler.

Digitare punire tramite cast di puntatore (al contrario di usare un’unione) è un importante esempio di rottura dell’aliasing rigoroso.

Nota

Questo è tratto dal mio “Qual è la regola di aliasing severo e perché ci importa?” Scrivilo.

Cos’è il rigoroso aliasing?

In alias C e C ++ ha a che fare con quali tipi di espressioni siamo autorizzati ad accedere ai valori memorizzati attraverso. In entrambi C e C ++ lo standard specifica quali tipi di espressioni sono autorizzati ad alias di quali tipi. Il compilatore e l’ottimizzatore possono assumere che seguiamo rigorosamente le regole di aliasing, quindi il termine regola di aliasing rigoroso . Se tentiamo di accedere a un valore utilizzando un tipo non consentito, è classificato come comportamento non definito ( UB ). Una volta che abbiamo un comportamento indefinito, tutte le scommesse sono state annullate, i risultati del nostro programma non sono più affidabili.

Sfortunatamente con violazioni rigorose di aliasing, otterremo spesso i risultati che ci aspettiamo, lasciando la possibilità che una versione futura di un compilatore con una nuova ottimizzazione rompa il codice che pensavamo fosse valido. Questo non è auspicabile ed è un objective utile per comprendere le rigide regole di aliasing e come evitare di violarle.

Per comprendere meglio il motivo per cui ci preoccupiamo, discuteremo i problemi che emergono quando violiamo le rigide regole di aliasing, il tipo puning, dal momento che le tecniche comuni usate nel puning di tipo spesso violano le rigide regole di aliasing e come digitare correttamente il pun.

Esempi preliminari

Diamo un’occhiata ad alcuni esempi, quindi possiamo parlare esattamente di ciò che dicono gli standard, esaminare alcuni ulteriori esempi e poi vedere come evitare il rigoroso aliasing e catturare le violazioni che abbiamo perso. Ecco un esempio che non dovrebbe sorprendere ( esempio dal vivo ):

 int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n"; 

Abbiamo un int * che punta alla memoria occupata da un int e questo è un aliasing valido. L'ottimizzatore deve presupporre che le assegnazioni tramite ip possano aggiornare il valore occupato da x .

Il prossimo esempio mostra l'aliasing che porta al comportamento non definito ( esempio dal vivo ):

 int foo( float *f, int *i ) { *i = 1; *f = 0.f; return *i; } int main() { int x = 0; std::cout << x << "\n"; // Expect 0 x = foo(reinterpret_cast(&x), &x); std::cout << x << "\n"; // Expect 0? } 

Nella funzione foo prendiamo un int * e un float * , in questo esempio chiamiamo foo e impostiamo entrambi i parametri in modo che puntino alla stessa locazione di memoria che in questo esempio contiene un int . Nota: reinterpret_cast dice al compilatore di trattare l'espressione come se avesse il tipo specificato dal suo parametro template. In questo caso, stiamo dicendo di trattare l'espressione & x come se fosse di tipo float * . Possiamo ingenuamente aspettarci che il risultato del secondo cout sia 0 ma con l'ottimizzazione abilitata usando -O2 sia gcc che clang producono il seguente risultato:

 0 1 

Quale non può essere previsto ma è perfettamente valido poiché abbiamo invocato il comportamento non definito. Un float non può validamente alias un object int . Pertanto, l'ottimizzatore può assumere la costante 1 memorizzata quando il dereferenziamento i sarà il valore di ritorno poiché un archivio attraverso f non potrebbe influenzare validamente un object int . Colbind il codice in Compiler Explorer mostra che questo è esattamente ciò che sta accadendo ( esempio dal vivo ):

 foo(float*, int*): # @foo(float*, int*) mov dword ptr [rsi], 1 mov dword ptr [rdi], 0 mov eax, 1 ret 

L'ottimizzatore che utilizza l' analisi alias basata sui tipi (TBAA) presuppone che venga restituito 1 e sposta direttamente il valore costante nel registro eax che trasporta il valore restituito. TBAA utilizza le regole delle lingue su quali tipi sono consentiti per l'alias per ottimizzare carichi e negozi. In questo caso, TBAA sa che un float non può alias e int e ottimizza il carico di i .

Ora, al libro delle regole

Cosa dice esattamente lo standard che siamo autorizzati e non autorizzati a fare? La lingua standard non è semplice, quindi per ciascun elemento cercherò di fornire esempi di codice che ne dimostrino il significato.

Cosa dice lo standard C11?

Lo standard C11 dice quanto segue nella sezione 6.5 Espressioni paragrafo 7 :

Un object deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi: 88) - un tipo compatibile con il tipo effettivo dell'object,

 int x = 1; int *p = &x; printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int 

- una versione qualificata di un tipo compatibile con il tipo effettivo dell'object,

 int x = 1; const int *p = &x; printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int 

- un tipo che è il tipo firmato o senza segno corrispondente al tipo effettivo dell'object,

 int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to // the effective type of the object 

gcc / clang ha un'estensione e inoltre permette di assegnare int * unsigned * a int * anche se non sono tipi compatibili.

- un tipo che è il tipo firmato o senza segno corrispondente a una versione qualificata del tipo effettivo dell'object,

 int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type // that corresponds with to a qualified verison of the effective type of the object 

- un tipo aggregato o sindacale che includa uno dei tipi sopra menzionati tra i suoi membri (incluso, in modo ricorsivo, un membro di un'unione subaggregata o contenuta), o

 struct foo { int x; }; void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can // can alias with *ip foo f; foobar( &f, &f.x ); 

- un tipo di carattere.

 int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type. // The results are not portable due to endianness issues. 

Cosa dice la bozza di C ++ 17

Lo standard di bozza C ++ 17 nella sezione [basic.lval] paragrafo 11 dice:

Se un programma tenta di accedere al valore memorizzato di un object tramite un glValue diverso da uno dei seguenti tipi, il comportamento non è definito: 63 (11.1) - il tipo dinamico dell'object,

 void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type // of the allocated object 

(11.2) - una versione qualificata CV del tipo dinamico dell'object,

 int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified // version of the dynamic type of x 

(11.3) - un tipo simile (come definito in 7.5) al tipo dinamico dell'object,

(11.4) - un tipo che è il tipo firmato o senza segno corrispondente al tipo dinamico dell'object,

 // Both si and ui are signed or unsigned types corresponding to each others dynamic types // We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing. signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; } 

(11.5) - un tipo che è il tipo firmato o senza segno corrispondente ad una versione qualificata cv del tipo dinamico dell'object,

 signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing 

(11.6) - un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi elementi o membri di dati non statici (incluso, in modo ricorsivo, un elemento o un membro di dati non statici di una unione parziale o contenuta),

 struct foo { int x; }; // Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx ); 

(11.7) - un tipo che è un tipo di class base (possibilmente qualificato cv) del tipo dinamico dell'object,

 struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; } 

(11.8) - un carattere char, unsigned char o std :: byte.

 int foo( std::byte &b, uint32_t &ui ) { b = static_cast('a'); ui = 0xFFFFFFFF; return std::to_integer( b ); // b gives us a glvalue expression of type std::byte which can alias // an object of type uint32_t } 

Degno di nota il char firmato non è incluso nella lista sopra, questa è una differenza notevole da C che dice un tipo di carattere .

Che cos'è Type Punning

Siamo arrivati ​​a questo punto e potremmo chiederci, perché vorremmo alias per? La risposta in genere consiste nel digitare il gioco di parole , spesso i metodi utilizzati violano le rigide regole di aliasing.

A volte vogliamo aggirare il sistema dei tipi e interpretare un object come un tipo diverso. Questo è chiamato tipo punning , per reinterpretare un segmento di memoria come un altro tipo. Digitare la punteggiatura è utile per le attività che desiderano accedere alla rappresentazione sottostante di un object da visualizzare, trasportare o manipolare. Le aree tipiche in cui viene utilizzato il tipo di punteggiatura sono compilatori, serializzazione, codice di rete, ecc ...

Tradizionalmente questo è stato ottenuto prendendo l'indirizzo dell'object, gettandolo su un puntatore del tipo che vogliamo reinterpretarlo come e quindi accedendo al valore, o in altre parole tramite aliasing. Per esempio:

 int x = 1 ; // In C float *fp = (float*)&x ; // Not a valid aliasing // In C++ float *fp = reinterpret_cast(&x) ; // Not a valid aliasing printf( “%f\n”, *fp ) ; 

Come abbiamo visto prima, questo non è un aliasing valido, quindi stiamo invocando un comportamento indefinito. Ma tradizionalmente i compilatori non si sono avvantaggiati delle rigide regole di aliasing e questo tipo di codice di solito ha funzionato, gli sviluppatori si sono purtroppo abituati a fare le cose in questo modo. Un metodo alternativo comune per la punteggiatura di tipo è attraverso i sindacati, che è valido in C ma il comportamento non definito in C ++ ( vedi esempio dal vivo ):

 union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB in C++ n is not the active member 

Questo non è valido in C ++ e alcuni considerano lo scopo dei sindacati essere esclusivamente per l'implementazione di tipi di varianti e sentire l'uso di unioni per punire il tipo è un abuso.

Come digitiamo Pun correttamente?

Il metodo standard per la punteggiatura di tipo in C e C ++ è memcpy . Questo può sembrare un po 'pesante, ma l'ottimizzatore dovrebbe riconoscere l'uso di memcpy per il tipo punning e ottimizzarlo e generare un registro per registrare lo spostamento. Ad esempio, se sappiamo che int64_t ha la stessa dimensione del doppio :

 static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message 

possiamo usare memcpy :

 void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //... 

At a sufficient optimization level any decent modern compiler generates identical code to the previously mentioned reinterpret_cast method or union method for type punning . Examining the generated code we see it uses just register mov ( live Compiler Explorer Example ).

C++20 and bit_cast

In C++20 we may gain bit_cast ( implementation available in link from proposal ) which gives a simple and safe way to type-pun as well as being usable in a constexpr context.

The following is an example of how to use bit_cast to type pun a unsigned int to float , ( see it live ):

 std::cout << bit_cast(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int) 

In the case where To and From types don't have the same size, it requires us to use an intermediate struct15. We will use a struct containing a sizeof( unsigned int ) character array ( assumes 4 byte unsigned int ) to be the From type and unsigned int as the To type.:

 struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4 }; // Assume len is a multiple of 4 int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { uint_chars f; std::memcpy( f.arr, &p[index], sizeof(unsigned int)); unsigned int result = bit_cast(f); result += foo( result ); } return result ; } 

It is unfortunate that we need this intermediate type but that is the current constraint of bit_cast .

Catching Strict Aliasing Violations

We don't have a lot of good tools for catching strict aliasing in C++, the tools we have will catch some cases of strict aliasing violations and some cases of misaligned loads and stores.

gcc using the flag -fstrict-aliasing and -Wstrict-aliasing can catch some cases although not without false positives/negatives. For example the following cases will generate a warning in gcc ( see it live ):

 int a = 1; short j; float f = 1.f; // Originally not initialized but tis-kernel caught // it was being accessed w/ an indeterminate value below printf("%i\n", j = *(reinterpret_cast(&a))); printf("%i\n", j = *(reinterpret_cast(&f))); 

although it will not catch this additional case ( see it live ):

 int *p; p=&a; printf("%i\n", j = *(reinterpret_cast(p))); 

Although clang allows these flags it apparently does not actually implement the warnings.

Another tool we have available to us is ASan which can catch misaligned loads and stores. Although these are not directly strict aliasing violations they are a common result of strict aliasing violations. For example the following cases will generate runtime errors when built with clang using -fsanitize=address

 int *x = new int[2]; // 8 bytes: [0,7]. int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address *u = 1; // Access to range [6-9] printf( "%d\n", *u ); // Access to range [6-9] 

The last tool I will recommend is C++ specific and not strictly a tool but a coding practice, don't allow C-style casts. Both gcc and clang will produce a diagnostic for C-style casts using -Wold-style-cast . This will force any undefined type puns to use reinterpret_cast, in general reinterpret_cast should be a flag for closer code review. It is also easier to search your code base for reinterpret_cast to perform an audit.

For C we have all the tools already covered and we also have tis-interpreter, a static analyzer that exhaustively analyzes a program for a large subset of the C language. Given a C verions of the earlier example where using -fstrict-aliasing misses one case ( see it live )

 int a = 1; short j; float f = 1.0 ; printf("%i\n", j = *((short*)&a)); printf("%i\n", j = *((int*)&f)); int *p; p=&a; printf("%i\n", j = *((short*)p)); 

tis-interpeter is able to catch all three, the following example invokes tis-kernal as tis-interpreter (output is edited for brevity):

 ./bin/tis-kernel -sa example1.c ... example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing rules by accessing a cell with effective type int. ... example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by accessing a cell with effective type float. Callstack: main ... example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by accessing a cell with effective type int. 

Finally there is TySan which is currently in development. This sanitizer adds type checking information in a shadow memory segment and checks accesses to see if they violate aliasing rules. The tool potentially should be able to catch all aliasing violations but may have a large run-time overhead.

According to the C89 rationale, the authors of the Standard did not want to require that compilers given code like:

 int x; int test(double *p) { x=5; *p = 1.0; return x; } 

should be required to reload the value of x between the assignment and return statement so as to allow for the possibility that p might point to x , and the assignment to *p might consequently alter the value of x . The notion that a compiler should be entitled to presume that there won’t be aliasing in situations like the above was non-controversial.

Unfortunately, the authors of the C89 wrote their rule in a way that, if read literally, would make even the following function invoke Undefined Behavior:

 void test(void) { struct S {int x;} s; sx = 1; } 

because it uses an lvalue of type int to access an object of type struct S , and int is not among the types that may be used accessing a struct S . Because it would be absurd to treat all use of non-character-type members of structs and unions as Undefined Behavior, almost everyone recognizes that there are at least some circumstances where an lvalue of one type may be used to access an object of another type. Unfortunately, the C Standards Committee has failed to define what those circumstances are.

Much of the problem is a result of Defect Report #028, which asked about the behavior of a program like:

 int test(int *ip, double *dp) { *ip = 1; *dp = 1.23; return *ip; } int test2(void) { union U { int i; double d; } u; return test(&u.i, &u.d); } 

Defect Report #28 states that the program invokes Undefined Behavior because the action of writing a union member of type “double” and reading one of type “int” invokes Implementation-Defined behavior. Such reasoning is nonsensical, but forms the basis for the Effective Type rules which needlessly complicate the language while doing nothing to address the original problem.

The best way to resolve the original problem would probably be to treat the footnote about the purpose of the rule as though it were normative, and made the rule unenforceable except in cases which actually involve conflicting accesses using aliases. Given something like:

  void inc_int(int *p) { *p = 3; } int test(void) { int *p; struct S { int x; } s; sx = 1; p = &s.x; inc_int(p); return sx; } 

There’s no conflict within inc_int because all accesses to the storage accessed through *p are done with an lvalue of type int , and there’s no conflict in test because p is visibly derived from a struct S , and by the next time s is used, all accesses to that storage that will ever be made through p will have already happened.

If the code were changed slightly…

  void inc_int(int *p) { *p = 3; } int test(void) { int *p; struct S { int x; } s; p = &s.x; sx = 1; // !!*!! *p += 1; return sx; } 

Here, there is an aliasing conflict between p and the access to sx on the marked line because at that point in execution another reference exists that will be used to access the same storage .

Had Defect Report 028 said the original example invoked UB because of the overlap between the creation and use of the two pointers, that would have made things a lot more clear without having to add “Effective Types” or other such complexity.

After reading many of the answers, I feel the need to add something:

Strict aliasing (which I’ll describe in a bit) is important because :

  1. Memory access can be expensive (performance wise), which is why data is manipulated in CPU registers before being written back to the physical memory.

  2. If data in two different CPU registers will be written to the same memory space, we can’t predict which data will “survive” when we code in C.

    In assembly, where we code the loading and unloading of CPU registers manually, we will know which data remains intact. But C (thankfully) abstracts this detail away.

Since two pointers can point to the same location in the memory, this could result in complex code that handles possible collisions .

This extra code is slow and hurts performance since it performs extra memory read / write operations which are both slower and (possibly) unnecessary.

The Strict aliasing rule allows us to avoid redundant machine code in cases in which it should be safe to assume that two pointers don’t point to the same memory block (see also the restrict keyword).

The Strict aliasing states it’s safe to assume that pointers to different types point to different locations in the memory.

If a compiler notices that two pointers point to different types (for example, an int * and a float * ), it will assume the memory address is different and it will not protect against memory address collisions, resulting in faster machine code.

For example :

Lets assume the following function:

 void merge_two_ints(int *a, int *b) { *b += *a; *a += *b; } 

In order to handle the case in which a == b (both pointers point to the same memory), we need to order and test the way we load data from the memory to the CPU registers, so the code might end up like this:

  1. load a and b from memory.

  2. add a to b .

  3. save b and reload a .

    (save from CPU register to the memory and load from the memory to the CPU register).

  4. add b to a .

  5. save a (from the CPU register) to the memory.

Step 3 is very slow because it needs to access the physical memory. However, it’s required to protect against instances where a and b point to the same memory address.

Strict aliasing would allow us to prevent this by telling the compiler that these memory addresses are distinctly different (which, in this case, will allow even further optimization which can’t be performsd if the pointers share a memory address).

  1. This can be told to the compiler in two ways, by using different types to point to. vale a dire:

     void merge_two_numbers(int *a, long *b) {...} 
  2. Using the restrict keyword. vale a dire:

     void merge_two_ints(int * restrict a, int * restrict b) {...} 

Now, by satisfying the Strict Aliasing rule, step 3 can be avoided and the code will run significantly faster.

In fact, by adding the restrict keyword, the whole function could be optimized to:

  1. load a and b from memory.

  2. add a to b .

  3. save result both to a and to b .

This optimization couldn’t have been done before, because of the possible collision (where a and b would be tripled instead of doubled).

Strict aliasing is not allowing different pointer types to the same data.

This article should help you understand the issue in full detail.

Technically in C++, the strict aliasing rule is probably never applicable.

Note the definition of indirection ( * operator ):

The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points .

Also from the definition of glvalue

A glvalue is an expression whose evaluation determines the identity of an object, (…snip)

So in any well defined program trace, a glvalue refers to an object. So the so called strict aliasing rule doesn’t apply, ever. This may not be what the designers wanted.