C Language
Aliasing e tipo effettivo
Ricerca…
Osservazioni
Le violazioni delle regole di aliasing e la violazione del tipo effettivo di un oggetto sono due cose diverse e non devono essere confuse.
L'aliasing è la proprietà di due puntatori
a
eb
che si riferiscono allo stesso oggetto, cioèa == b
.Il tipo effettivo di un oggetto dati viene utilizzato da C per determinare quali operazioni possono essere eseguite su quell'oggetto. In particolare, il tipo effettivo viene utilizzato per determinare se due puntatori possono alias l'un l'altro.
Aliasing può essere un problema di ottimizzazione, perché cambiando l'oggetto i puntatore, a
esempio, può cambiare l'oggetto che è visibile attraverso l'altro puntatore, b
. Se il compilatore C dovesse assumere che i puntatori potrebbero sempre essere alias l'un l'altro, indipendentemente dal loro tipo e provenienza, molte opportunità di ottimizzazione andrebbero perse e molti programmi sarebbero più lenti.
Le rigide regole di aliasing di C si riferiscono ai casi in cui il compilatore può assumere che gli oggetti (o non) si alias l'un l'altro. Ci sono due regole pratiche che dovresti sempre avere in mente per i puntatori di dati.
Salvo diversa indicazione, due puntatori con lo stesso tipo di base possono essere alias.
Due puntatori con un tipo di base diverso non possono essere pseudonimi, a meno che almeno uno dei due tipi sia un tipo di carattere.
Qui il tipo di base significa che mettiamo da parte le qualifiche di tipo come const
, ad esempio se a
è double*
e b
è const double*
, il compilatore deve generalmente assumere che un cambiamento di *a
possa cambiare *b
.
La violazione della seconda regola può avere risultati catastrofici. Qui violare la rigida regola di aliasing significa che si presentano due puntatori a
e b
di tipo diverso al compilatore che in realtà puntano allo stesso oggetto. Il compilatore quindi può sempre presumere che i due punti a oggetti diversi, e non aggiornerà la sua idea di *b
se hai cambiato l'oggetto tramite *a
.
Se lo fai, il comportamento del tuo programma diventa indefinito. Pertanto, C pone restrizioni piuttosto severe alle conversioni del puntatore al fine di evitare che tale situazione si verifichi accidentalmente.
A meno che il tipo di origine o di destinazione sia
void
, tutte le conversioni di puntatore tra i puntatori con diverso tipo di base devono essere esplicite .
O in altre parole, hanno bisogno di un cast , a meno che non si faccia una conversione che aggiunge solo un qualificatore come const
al tipo di destinazione.
Evitare le conversioni puntatore in generale e le cas in particolare ti protegge dai problemi di aliasing. A meno che tu non abbia davvero bisogno di loro, e questi casi sono molto speciali, dovresti evitarli il più possibile.
Non è possibile accedere ai tipi di caratteri tramite tipi non di caratteri.
Se un oggetto è definito con statico, thread o durata della memorizzazione automatica e ha un tipo di carattere: char
, unsigned char
o signed char
, potrebbe non essere accessibile da un tipo non di caratteri. Nell'esempio seguente un array di char
viene reinterpretato come il tipo int
e il comportamento non è definito su ogni dereferenziazione del puntatore int
b
.
int main( void )
{
char a[100];
int* b = ( int* )&a;
*b = 1;
static char c[100];
b = ( int* )&c;
*b = 2;
_Thread_local char d[100];
b = ( int* )&d;
*b = 3;
}
Non è definito perché viola la regola del "tipo effettivo", non è possibile accedere a nessun oggetto dati di tipo efficace tramite un altro tipo che non è un tipo di carattere. Poiché l'altro tipo qui è int
, questo non è permesso.
Anche se l'allineamento e le dimensioni del puntatore sarebbero note per adattarsi, questo non sarebbe esente da questa regola, il comportamento sarebbe ancora indefinito.
Ciò significa in particolare che non esiste alcun modo in standard C di riservare un oggetto buffer di tipo carattere che può essere utilizzato attraverso puntatori con tipi diversi, poiché si utilizzerà un buffer ricevuto da malloc
o funzione simile.
Un modo corretto per raggiungere lo stesso obiettivo dell'esempio precedente sarebbe utilizzare un union
.
typedef union bufType bufType;
union bufType {
char c[sizeof(int[25])];
int i[25];
};
int main( void )
{
bufType a = { .c = { 0 } }; // reserve a buffer and initialize
int* b = a.i; // no cast necessary
*b = 1;
static bufType a = { .c = { 0 } };
int* b = a.i;
*b = 2;
_Thread_local bufType a = { .c = { 0 } };
int* b = a.i;
*b = 3;
}
Qui, l' union
assicura che il compilatore sappia dall'inizio che il buffer può essere consultato attraverso diverse viste. Questo ha anche il vantaggio che ora il buffer ha una "vista" ai
che è già di tipo int
e non è necessaria alcuna conversione del puntatore.
Tipo efficace
Il tipo effettivo di un oggetto dati è l'ultimo tipo di informazione ad esso associato, se presente.
// a normal variable, effective type uint32_t, and this type never changes
uint32_t a = 0.0;
// effective type of *pa is uint32_t, too, simply
// because *pa is the object a
uint32_t* pa = &a;
// the object pointed to by q has no effective type, yet
void* q = malloc(sizeof uint32_t);
// the object pointed to by q still has no effective type,
// because nobody has written to it
uint32_t* qb = q;
// *qb now has effective type uint32_t because a uint32_t value was written
*qb = 37;
// the object pointed to by r has no effective type, yet, although
// it is initialized
void* r = calloc(1, sizeof uint32_t);
// the object pointed to by r still has no effective type,
// because nobody has written to or read from it
uint32_t* rc = r;
// *rc now has effective type uint32_t because a value is read
// from it with that type. The read operation is valid because we used calloc.
// Now the object pointed to by r (which is the same as *rc) has
// gained an effective type, although we didn't change its value.
uint32_t c = *rc;
// the object pointed to by s has no effective type, yet.
void* s = malloc(sizeof uint32_t);
// the object pointed to by s now has effective type uint32_t
// because an uint32_t value is copied into it.
memcpy(s, r, sizeof uint32_t);
Osserva che per quest'ultimo non era necessario avere un puntatore uint32_t*
su quell'oggetto. Il fatto che abbiamo copiato un altro oggetto uint32_t
è sufficiente.
Violare le rigide regole di aliasing
Nel seguente codice supponiamo per semplicità che float
e uint32_t
abbiano la stessa dimensione.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
e f
hanno un diverso tipo di base, e quindi il compilatore può assumere che punti a oggetti diversi. Non c'è alcuna possibilità che *f
avrebbe potuto cambiare tra i due inizializzazioni di a
e b
, e quindi il compilatore può ottimizzare il codice per qualcosa di equivalente a
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
Cioè, la seconda operazione di caricamento di *f
può essere completamente ottimizzata.
Se chiamiamo questa funzione "normalmente"
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
tutto va bene e qualcosa di simile
4 dovrebbe essere uguale a 4
è stampato Ma se imbrogliamo e passiamo lo stesso puntatore, dopo averlo convertito,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
violiamo la rigida regola di aliasing. Quindi il comportamento diventa indefinito. L'output potrebbe essere come sopra, se il compilatore ha ottimizzato il secondo accesso, o qualcosa di completamente diverso, e così il tuo programma finisce in uno stato completamente inaffidabile.
limitare la qualifica
Se abbiamo due argomenti puntatori dello stesso tipo, il compilatore non può assumere alcuna ipotesi e dovrà sempre presumere che la modifica a *e
potrebbe cambiare *f
:
void fun(float* e, float* f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
float fval = 4;
float eval = 77;
fun(&eval, &fval);
tutto va bene e qualcosa di simile
è 4 uguale a 4?
è stampato Se passiamo lo stesso puntatore, il programma farà ancora la cosa giusta e stamperà
è 4 uguale a 22?
Questo può rivelarsi inefficiente, se sappiamo da alcune informazioni esterne che e
e f
non puntano mai allo stesso oggetto dati. Possiamo riflettere questa conoscenza aggiungendo qualificatori restrict
ai parametri del puntatore:
void fan(float*restrict e, float*restrict f) {
float a = *f
*e = 22;
float b = *f;
print("is %g equal to %g?\n", a, b);
}
Quindi il compilatore può sempre supporre che e
e f
puntino a oggetti diversi.
Modifica dei byte
Una volta che un oggetto ha un tipo efficace, non si deve tentare di modificarlo tramite un puntatore di un altro tipo, a meno che quell'altro tipo sia un tipo di carattere, char
, signed char
o unsigned char
.
#include <inttypes.h>
#include <stdio.h>
int main(void) {
uint32_t a = 57;
// conversion from incompatible types needs a cast !
unsigned char* ap = (unsigned char*)&a;
for (size_t i = 0; i < sizeof a; ++i) {
/* set each byte of a to 42 */
ap[i] = 42;
}
printf("a now has value %" PRIu32 "\n", a);
}
Questo è un programma valido che stampa
un ora ha il valore 707406378
Funziona perché:
- L'accesso viene effettuato ai singoli byte visualizzati con
unsigned char
pertanto ogni modifica è ben definita. - Le due viste dell'oggetto, tramite
a
e attraverso*ap
, alias, ma poichéap
è un puntatore a un tipo di carattere, la regola di aliasing rigorosa non si applica. Quindi il compilatore deve assumere che il valore dia
possa essere stato modificato nel ciclofor
. Il valore modificato dia
deve essere costruito dai byte che sono stati modificati. - Il tipo di
a
,uint32_t
non ha bit di riempimento. Tutti i suoi bit della rappresentazione contano per il valore, qui707406378
, e non può esserci alcuna rappresentazione di trap.