C Language
Aliasing en effectief type
Zoeken…
Opmerkingen
Schendingen van aliasregels en van het overtreden van het effectieve type van een object zijn twee verschillende dingen en mogen niet worden verward.
Aliasing is de eigenschap van twee verwijzingen
a
enb
die naar hetzelfde object verwijzen, dat wil zeggena == b
.Het effectieve type van een gegevensobject wordt door C gebruikt om te bepalen welke bewerkingen op dat object kunnen worden uitgevoerd. In het bijzonder wordt het effectieve type gebruikt om te bepalen of twee pointers elkaar kunnen gebruiken.
Aliasing problemen voor optimalisatie, omdat aan het doel via een wijzer, a
bijvoorbeeld, kan het object dat zichtbaar is door de andere aanwijzer wijzigen, b
. Als uw C-compiler zou moeten aannemen dat pointers altijd aliassen kunnen gebruiken, ongeacht hun type en herkomst, gaan veel optimalisatiemogelijkheden verloren en lopen veel programma's langzamer.
De strikte aliasing-regels van C verwijzen naar cases in de compiler die ervan uit kunnen gaan welke objecten al dan niet een alias gebruiken. Er zijn twee vuistregels die u altijd in gedachten moet houden voor gegevensaanwijzers.
Tenzij anders vermeld, kunnen twee aanwijzers met hetzelfde basistype alias zijn.
Twee wijzers met een ander basistype kunnen geen alias gebruiken, tenzij ten minste een van de twee typen een tekentype is.
Hier basistype betekent dat we terzijde typekwalificatie als const
, bijvoorbeeld als a
is double*
en b
is const double*
, moet de compiler algemeen aan dat een verandering van *a
kunnen veranderen *b
.
Het overtreden van de tweede regel kan catastrofale gevolgen hebben. Het overtreden van de strikte aliasingregel betekent dat u twee aanwijzers a
en b
van een ander type presenteert aan de compiler die in werkelijkheid naar hetzelfde object wijzen. De compiler mag er dan altijd van uitgaan dat de twee naar verschillende objecten wijzen en zal het idee van *b
niet bijwerken als u het object via *a
hebt gewijzigd.
Als u dit doet, wordt het gedrag van uw programma ongedefinieerd. Daarom legt C vrij ernstige beperkingen op aanwijzerconversies om u te helpen een dergelijke situatie per ongeluk te voorkomen.
Tenzij het bron- of doeltype
void
, moeten alle pointerconversies tussen pointers met een ander basistype expliciet zijn .
Of met andere woorden, ze hebben een cast nodig , tenzij je een conversie uitvoert die alleen een kwalificatie zoals const
toevoegt aan het doeltype.
Het vermijden van pointerconversies in het algemeen en casts in het bijzonder beschermt u tegen aliasingproblemen. Tenzij je ze echt nodig hebt, en deze gevallen zijn heel bijzonder, moet je ze zo veel mogelijk vermijden.
Tekensoorten zijn niet toegankelijk via niet-tekensoorten.
Als een object is gedefinieerd met een statische, thread- of automatische opslagduur en het een karaktertype heeft: char
, unsigned char
of signed char
, is er mogelijk geen toegang voor een niet-karaktertype. In het onderstaande voorbeeld wordt een char
array opnieuw geïnterpreteerd als het type int
, en het gedrag is niet gedefinieerd op elke dereferentie van de int
pointer 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;
}
Dit is niet gedefinieerd omdat het in strijd is met de regel "effectief type". Er is geen toegang tot een gegevensobject met een effectief type via een ander type dat geen tekentype is. Aangezien het andere type hier int
, is dit niet toegestaan.
Zelfs als bekend zou zijn dat de uitlijning en de grootte van de aanwijzer passen, zou dit niet van deze regel zijn uitgesloten, maar gedrag zou nog steeds niet zijn gedefinieerd.
Dit betekent in het bijzonder dat er in standaard C geen manier is om een bufferobject van het karaktertype te reserveren dat kan worden gebruikt door middel van verwijzingen met verschillende typen, zoals u een buffer zou gebruiken die werd ontvangen door malloc
of een vergelijkbare functie.
Een juiste manier om hetzelfde doel te bereiken als in het bovenstaande voorbeeld, is om een union
te gebruiken.
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;
}
Hier zorgt de union
ervoor dat de compiler vanaf het begin weet dat de buffer toegankelijk is via verschillende weergaven. Dit heeft ook het voordeel dat de buffer nu een "view" ai
die al van het type int
en geen pointerconversie nodig is.
Effectief type
Het effectieve type van een gegevensobject is de laatste type-informatie die hieraan is gekoppeld, indien aanwezig.
// 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);
Merk op dat het voor dit laatste niet nodig was dat we zelfs een uint32_t*
naar dat object hadden. Het feit dat we nog een object uint32_t
hebben gekopieerd, is voldoende.
De strikte aliasregels overtreden
Laten we in de volgende code voor de eenvoud aannemen dat float
en uint32_t
dezelfde grootte hebben.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
en f
hebben een ander basistype, en dus kan de compiler ervan uitgaan dat ze naar verschillende objecten wijzen. Er is geen mogelijkheid dat *f
kan zijn veranderd tussen de twee initialisaties van a
en b
, en dus kan de compiler de code optimaliseren tot iets dat equivalent is aan
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
Dat wil zeggen dat de tweede laadbewerking van *f
kan worden geoptimaliseerd.
Als we deze functie "normaal" noemen
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
alles gaat goed en zoiets
4 moet gelijk zijn aan 4
is afgedrukt. Maar als we vals spelen en dezelfde wijzer doorgeven, na het omzetten,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
we overtreden de strikte aliasregel. Dan wordt het gedrag ongedefinieerd. De uitvoer kan zijn zoals hierboven, als de compiler de tweede toegang had geoptimaliseerd, of iets heel anders, en dus uw programma in een volledig onbetrouwbare staat terechtkomt.
kwalificatie beperken
Als we twee pointer-argumenten van hetzelfde type hebben, kan de compiler geen aanname doen en moet hij er altijd van uitgaan dat de wijziging in *e
*f
kan wijzigen:
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);
alles gaat goed en zoiets
is 4 gelijk aan 4?
is afgedrukt. Als we dezelfde aanwijzer passeren, zal het programma nog steeds het juiste doen en afdrukken
is 4 gelijk aan 22?
Dit kan blijken te zijn inefficiënt te zijn, als we weten door een externe informatie die e
en f
nooit zal wijzen op dezelfde gegevens object. We kunnen die kennis weerspiegelen door restrict
kwalificaties toe te voegen aan de aanwijzerparameters:
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);
}
Dan kan de compiler altijd veronderstellen dat e
en f
naar verschillende objecten wijzen.
Bytes wijzigen
Als een object eenmaal een effectief type heeft, moet u niet proberen het te wijzigen via een aanwijzer van een ander type, tenzij dat andere type een tekentype, char
, signed char
of 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);
}
Dit is een geldig programma dat wordt afgedrukt
a heeft nu waarde 707406378
Dit werkt omdat:
- De toegang wordt gemaakt tot de individuele bytes die worden gezien met het type
unsigned char
dus elke wijziging is goed gedefinieerd. - De twee weergaven van het object, via
a
en via*ap
, alias, maar omdatap
een pointer is naar een karaktertype, is de strikte aliasregel niet van toepassing. De compiler moet er dus van uitgaan dat de waarde vana
mogelijk is gewijzigd in defor
lus. De gewijzigde waarde vana
moet worden opgebouwd uit de bytes die zijn gewijzigd. - Het type
a
,uint32_t
heeft geen opvulbits. Alle bits van de representatie tellen voor de waarde, hier707406378
, en er kan geen trap-representatie zijn.