Buscar..


Observaciones

Las violaciones de las reglas de creación de alias y de violar el tipo efectivo de un objeto son dos cosas diferentes y no deben confundirse.

  • El aliasing es la propiedad de dos punteros a y b que se refieren al mismo objeto, es decir, a == b .

  • C utiliza el tipo efectivo de un objeto de datos para determinar qué operaciones se pueden realizar en ese objeto. En particular, el tipo efectivo se utiliza para determinar si dos punteros pueden alias entre sí.

El aliasing puede ser un problema para la optimización, porque cambiar el objeto a través de un puntero, a ejemplo, puede cambiar el objeto que es visible a través del otro puntero, b . Si su compilador de C tuviera que asumir que los punteros siempre se podrían aliar entre sí, independientemente de su tipo y procedencia, se perderían muchas oportunidades de optimización y muchos programas se ejecutarían más lentamente.

Las reglas estrictas de alias de C se refieren a los casos en el compilador que puede asumir qué objetos se hacen (o no) alias entre sí. Hay dos reglas básicas que siempre debe tener en cuenta para los punteros de datos.

A menos que se indique lo contrario, dos punteros con el mismo tipo de base pueden ser alias.

Dos punteros con un tipo de base diferente no pueden ser alias, a menos que al menos uno de los dos tipos sea un tipo de carácter.

Aquí , tipo base significa que dejamos de lado las calificaciones de tipo como const , por ejemplo, si a es double* y b es const double* , el compilador generalmente debe asumir que un cambio de *a puede cambiar *b .

Violar la segunda regla puede tener resultados catastróficos. En este caso, violar la regla de alias estricta significa que presenta dos punteros a y b de diferente tipo al compilador que en realidad apuntan al mismo objeto. Entonces, el compilador siempre puede suponer que los dos apuntan a objetos diferentes, y no actualizará su idea de *b si cambia el objeto a través de *a .

Si lo haces, el comportamiento de tu programa se vuelve indefinido. Por lo tanto, C pone restricciones muy severas en las conversiones de punteros para ayudarlo a evitar que esa situación ocurra accidentalmente.

A menos que el tipo de origen o destino sea void , todas las conversiones de punteros entre punteros con un tipo de base diferente deben ser explícitas .

O en otras palabras, necesitan un molde, a menos que haga una conversión que sólo añade un calificativo como const al tipo de destino.

Evitar las conversiones de punteros en general y las conversiones en particular lo protege de los problemas de aliasing. A menos que realmente los necesite, y estos casos son muy especiales, debe evitarlos como pueda.

No se puede acceder a los tipos de caracteres a través de tipos que no son de caracteres.

Si un objeto se define con una duración de almacenamiento estático, de subproceso o automático y tiene un tipo de carácter, ya sea: char , unsigned char , o signed char , no se puede acceder a él por un tipo que no sea de carácter. En el ejemplo siguiente un char array se reinterpreta como el tipo int , y el comportamiento es indefinido en cada dereference de la int puntero 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;
}

Esto no está definido porque viola la regla del "tipo efectivo", no se puede acceder a ningún objeto de datos que tenga un tipo efectivo a través de otro tipo que no sea un tipo de carácter. Dado que el otro tipo aquí es int , esto no está permitido.

Incluso si se sabe que la alineación y el tamaño de los punteros se ajustan, esto no estaría exento de esta regla, el comportamiento aún sería indefinido.

Esto significa, en particular, que no hay forma en el estándar C de reservar un objeto de tipo de carácter de búfer que pueda usarse a través de punteros con diferentes tipos, ya que usaría un búfer recibido por malloc o una función similar.

Una forma correcta de lograr el mismo objetivo que en el ejemplo anterior sería utilizar una 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;
}

Aquí, la union garantiza que el compilador sepa desde el principio que se puede acceder al búfer a través de diferentes vistas. Esto también tiene la ventaja de que ahora el búfer tiene una "vista" ai que ya es de tipo int y no se necesita conversión de puntero.

Tipo efectivo

El tipo efectivo de un objeto de datos es el último tipo de información que se asoció con él, en su caso.

// 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);

Observe que para este último, no fue necesario que tengamos un puntero uint32_t* para ese objeto. El hecho de que hayamos copiado otro objeto uint32_t es suficiente.

Violando las estrictas reglas de alias.

En el siguiente código, asumamos por simplicidad que float y uint32_t tienen el mismo tamaño.

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    float b = *f;
    print("%g should equal %g\n", a, b);
}

u y f tienen un tipo de base diferente, y por lo tanto el compilador puede asumir que apuntan a objetos diferentes. No hay posibilidad de que *f haya cambiado entre las dos inicializaciones de a y b , por lo que el compilador puede optimizar el código a algo equivalente a

void fun(uint32_t* u, float* f) {
    float a = *f
    *u = 22;
    print("%g should equal %g\n", a, a);
}

Es decir, la segunda operación de carga de *f puede optimizarse completamente.

Si llamamos a esta función "normalmente"

 float fval = 4;
 uint32_t uval = 77;
 fun(&uval, &fval);

todo va bien y algo así

4 debería ser igual a 4

está impreso. Pero si hacemos trampa y pasamos el mismo puntero, después de convertirlo,

 float fval = 4;
 uint32_t* up = (uint32_t*)&fval;
 fun(up, &fval);

violamos la estricta regla de aliasing. Entonces el comportamiento se vuelve indefinido. La salida podría ser la anterior, si el compilador hubiera optimizado el segundo acceso, o algo completamente diferente, y así su programa termine en un estado completamente no confiable.

restringir calificación

Si tenemos dos argumentos de puntero del mismo tipo, el compilador no puede hacer ninguna suposición y siempre tendremos que asumir que el cambio a *e puede cambiar *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);

todo va bien y algo así

¿Es 4 igual a 4?

está impreso. Si pasamos el mismo puntero, el programa seguirá haciendo lo correcto e imprimirá

¿Es 4 igual a 22?

Esto puede resultar ineficiente, si sabemos por alguna información externa que e y f nunca apuntarán al mismo objeto de datos. Podemos reflejar ese conocimiento al agregar calificadores de restrict a los parámetros del puntero:

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);
}

Entonces, el compilador siempre puede suponer que e y f apuntan a objetos diferentes.

Cambiando bytes

Una vez que un objeto tiene un tipo efectivo, no debe intentar modificarlo a través de un puntero de otro tipo, a menos que ese otro tipo sea un tipo de carácter, 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);
}

Este es un programa válido que imprime

un ahora tiene valor 707406378

Esto funciona porque:

  • El acceso se realiza a los bytes individuales que se ven con el tipo unsigned char para que cada modificación esté bien definida.
  • Las dos vistas al objeto, a través de a y a través de *ap , alias, pero como ap es un puntero a un tipo de carácter, la regla de alias estricta no se aplica. Por lo tanto, el compilador debe asumir que el valor de a puede haber sido cambiado en el bucle for . El valor modificado de a debe construirse a partir de los bytes que se han cambiado.
  • El tipo de a , uint32_t no tiene bits de relleno. Todos sus bits de la representación cuentan para el valor, aquí 707406378 , y no puede haber representación de captura.


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow