C Language
Aliasing och effektiv typ
Sök…
Anmärkningar
Brott mot aliasing-regler och brott mot den effektiva typen av objekt är två olika saker och bör inte förväxlas.
Aliasing är egenskapen för två pekare
a
ochb
som hänvisar till samma objekt, det vill sägaa == b
.Den effektiva typen av ett dataobjekt används av C för att bestämma vilka operationer som kan utföras på det objektet. Speciellt används den effektiva typen för att bestämma om två pekare kan alias varandra.
Aliasing kan vara ett problem för optimering, eftersom att ändra objektet genom en pekare, a
säga, kan ändra objektet som är synligt genom den andra pekaren, b
. Om din C-kompilator skulle behöva anta att pekare alltid kan alias varandra, oavsett typ och ursprung, skulle många optimeringsmöjligheter gå förlorade och många program skulle gå långsammare.
C: s stränga aliasregler hänvisar till fall i kompilatorn kan anta vilka objekt som gör (eller inte) alias varandra. Det finns två tumregler som du alltid bör ha i åtanke för datapekare.
Om inget annat sägs kan två pekare med samma bastyp alias.
Två pekare med olika bastyp kan inte alias, om inte minst en av de två typerna är en karaktärstyp.
Här bastyp innebär att vi åt sidan sätta typkvalifikationer såsom const
, t.ex. Om a
är double*
och b
är const double*
måste kompilatorn allmänhet anta att en förändring av *a
kan förändras *b
.
Att bryta mot den andra regeln kan ha katastrofala resultat. Om du bryter mot den strikta aliaseringsregeln betyder det att du presenterar två pekare a
och b
av olika typ för kompilatorn som i verkligheten pekar på samma objekt. Kompilatorn kan då alltid anta att de två pekar på olika objekt och inte kommer att uppdatera sin idé om *b
om du ändrade objektet genom *a
.
Om du gör det blir beteendet för ditt program odefinierat. Därför sätter C ganska allvarliga begränsningar för pekarkonvertering för att hjälpa dig att undvika att en sådan situation inträffar av misstag.
Om inte källan eller måltypen är
void
, måste alla pekarkonverteringar mellan pekare med olika bastyp vara tydliga .
Eller med andra ord, de behöver en roll , såvida du inte gör en konvertering som bara lägger till en kval som t.ex. const
till måltypen.
Att undvika pekarkonverteringar i allmänhet och i synnerhet skyddar dig från problem med aliasing. Om du verkligen behöver dem, och dessa fall är mycket speciella, bör du undvika dem som du kan.
Teckentyper kan inte nås via icke-teckentyper.
Om ett objekt definieras med statisk, tråd eller automatisk lagringsvaraktighet, och det har en karaktärstyp, antingen: char
, unsigned char
eller signed char
, kanske det inte går att komma åt det av en typ utan karaktär. I exemplet nedan tolkas en char
array som typen int
, och beteendet är odefinierat på varje förändring av int
pekaren 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;
}
Detta är odefinierat eftersom det bryter mot "effektiv typ" -regeln, inget dataobjekt som har en effektiv typ kan nås via en annan typ som inte är en karaktärstyp. Eftersom den andra typen här är int
, är detta inte tillåtet.
Även om justering och pekarstorlekar skulle vara kända för att passa, skulle detta inte undantas från denna regel, beteende skulle fortfarande vara odefinierat.
Detta innebär särskilt att det inte finns något sätt i standard C att reservera ett buffertobjekt av karaktärstyp som kan användas genom pekare med olika typer, eftersom du skulle använda en buffert som mottogs av malloc
eller liknande funktion.
Ett korrekt sätt att uppnå samma mål som i exemplet ovan skulle vara att använda en 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;
}
Här garanterar union
att kompilatorn från början vet att bufferten kan nås genom olika vyer. Detta har också fördelen att bufferten nu har en "vy" ai
som redan är av typ int
och ingen pekarkonvertering behövs.
Effektiv typ
Den effektiva typen av ett dataobjekt är den sista typinformationen som var associerad med den, om någon.
// 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);
Observera att för det senare var det inte nödvändigt att vi ens hade en uint32_t*
till det objektet. Det faktum att vi har kopierat ett annat uint32_t
objekt räcker.
Brott mot de strikta reglerna för aliasing
Låt oss i följande kod för enkelhets uint32_t
anta att float
och uint32_t
har samma storlek.
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
float b = *f;
print("%g should equal %g\n", a, b);
}
u
och f
har olika bastyp, och därmed kan kompilatorn anta att de pekar på olika objekt. Det finns ingen möjlighet att *f
kunde ha ändrats mellan de två initialiseringarna av a
och b
, så att kompilatorn kan optimera koden till något som motsvarar
void fun(uint32_t* u, float* f) {
float a = *f
*u = 22;
print("%g should equal %g\n", a, a);
}
Det vill säga den andra lastoperationen av *f
kan optimeras helt.
Om vi kallar denna funktion "normalt"
float fval = 4;
uint32_t uval = 77;
fun(&uval, &fval);
allt går bra och något liknande
4 bör vara lika med 4
är tryckt. Men om vi fuskar och passerar samma pekare efter att ha konverterat den,
float fval = 4;
uint32_t* up = (uint32_t*)&fval;
fun(up, &fval);
vi bryter mot den strikta aliaseringsregeln. Då blir beteendet odefinierat. Utdata kan vara som ovan, om kompilatorn hade optimerat den andra åtkomsten, eller något helt annat, och så ditt program hamnar i ett helt opålitligt tillstånd.
begränsa kvalifikationen
Om vi har två pekarargument av samma typ kan kompilatorn inte göra något antagande och måste alltid anta att ändringen till *e
kan ändras *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);
allt går bra och något liknande
är 4 lika med 4?
är tryckt. Om vi passerar samma pekare kommer programmet fortfarande att göra rätt sak och skriva ut
är 4 lika med 22?
Detta kan visa sig vara ineffektivt, om vi vet av extern information att e
och f
aldrig kommer att peka på samma dataobjekt. Vi kan reflektera den kunskapen genom att lägga till restrict
kvalifikationer till pekarparametrarna:
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);
}
Då kan kompilatorn alltid anta att e
och f
pekar på olika objekt.
Ändra byte
När ett objekt har en effektiv typ bör du inte försöka modifiera det genom en pekare av en annan typ, såvida inte den andra typen är en karaktärstyp, char
, signed char
eller 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);
}
Detta är ett giltigt program som skriver ut
a har nu värdet 707406378
Detta fungerar eftersom:
- Åtkomst görs till de enskilda byte som ses med typ av
unsigned char
så varje modifiering är väl definierad. - De två vyerna till objektet, via
a
och genom*ap
, alias, men eftersomap
är en pekare till en karaktärstyp gäller inte den stränga aliaseringsregeln. Således måste kompilatorn anta att värdet påa
kan ha ändrats ifor
slingan. Det modifierade värdet påa
måste konstrueras från byte som har ändrats. - Typen
a
,uint32_t
har inga stoppningsbitar. Alla dess bitar av representationen räknas för värdet, här707406378
, och det kan inte finnas någon fällrepresentation.