El puntero con punteo de tipo de desreferenciación romperá las reglas de alias estrictos
optimization gcc (7)
Usé el siguiente fragmento de código para leer datos de archivos como parte de un programa más grande.
double data_read(FILE *stream,int code) {
char data[8];
switch(code) {
case 0x08:
return (unsigned char)fgetc(stream);
case 0x09:
return (signed char)fgetc(stream);
case 0x0b:
data[1] = fgetc(stream);
data[0] = fgetc(stream);
return *(short*)data;
case 0x0c:
for(int i=3;i>=0;i--)
data[i] = fgetc(stream);
return *(int*)data;
case 0x0d:
for(int i=3;i>=0;i--)
data[i] = fgetc(stream);
return *(float*)data;
case 0x0e:
for(int i=7;i>=0;i--)
data[i] = fgetc(stream);
return *(double*)data;
}
die("data read failed");
return 1;
}
Ahora me dicen que use -O2
y recibo la siguiente advertencia de gcc: warning: dereferencing type-punned pointer will break strict-aliasing rules
Googleing encontré dos respuestas ortogonales:
- Concluyendo: no hay necesidad de preocuparse; gcc intenta ser más obediente a la ley que la ley real.
vs
- Así que, básicamente, si tiene un int * y un flotante * no se les permite señalar a la misma ubicación de memoria. Si su código no respeta esto, entonces el optimizador del compilador muy probablemente romperá su código.
Al final no quiero ignorar las advertencias. ¿Qué recomendarías?
[actualización] Sustituí el ejemplo de juguete con la función real.
Aparentemente, el estándar permite que sizeof (char *) sea diferente de sizeof (int *), por lo que gcc se queja cuando intentas un lanzamiento directo. void * es un poco especial ya que todo se puede convertir hacia y desde el vacío *. En la práctica, no conozco muchas arquitecturas / compiladores en las que un puntero no sea siempre el mismo para todos los tipos, pero gcc tiene razón al emitir una advertencia incluso si es molesto.
Creo que la manera segura sería
int i, *p = &i;
char *q = (char*)&p[0];
o
char *q = (char*)(void*)p;
También puedes probar esto y ver lo que obtienes:
char *q = reinterpret_cast<char*>(p);
Básicamente, puedes leer el mensaje de gcc como hombre en busca de problemas, no digas que no te advertí .
Convertir una matriz de caracteres de tres bytes en int
es una de las peores cosas que he visto. Normalmente su int
tiene al menos 4 bytes. Entonces para el cuarto (y tal vez más si int
es más amplio) obtienes datos aleatorios. Y luego lanzas todo esto a un double
.
Simplemente no hagas nada de eso. El problema de aliasing que advierte gcc es inocente en comparación con lo que estás haciendo.
El problema ocurre porque accedes a una matriz char a través de un double*
:
char data[8];
...
return *(double*)data;
Pero gcc asume que su programa nunca accederá a variables a través de punteros de diferente tipo. Esta suposición se denomina aliasing estricto y permite al compilador realizar algunas optimizaciones:
Si el compilador sabe que su *(double*)
puede solapar de ninguna manera con los data[]
, se le permite todo tipo de cosas, como reordenar el código en:
return *(double*)data;
for(int i=7;i>=0;i--)
data[i] = fgetc(stream);
Lo más probable es que el bucle esté optimizado y termines con solo:
return *(double*)data;
Lo cual deja tus datos [] sin inicializar. En este caso particular, el compilador podría ver que los punteros se superponen, pero si lo hubiera declarado char* data
, podría haber dado errores.
Pero, la regla de alias estrictos dice que un char * y void * pueden apuntar a cualquier tipo. Entonces puedes reescribirlo en:
double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;
Las advertencias de alias estrictas son realmente importantes para comprender o corregir. Causan los tipos de errores que son imposibles de reproducir internamente porque ocurren solo en un compilador en particular en un sistema operativo particular en una máquina en particular y solo en luna llena y una vez al año, etc.
Este documento resume la situación: http://dbp-consulting.com/tutorials/StrictAliasing.html
Aquí hay varias soluciones diferentes, pero la más portátil / segura es usar memcpy (). (Las llamadas a funciones pueden optimizarse, por lo que no es tan ineficiente como parece). Por ejemplo, reemplace esto:
return *(short*)data;
Con este:
short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
Los autores del estándar C querían que los escritores de compiladores generaran código eficiente en circunstancias en las que sería teóricamente posible, pero poco probable que se accediera a una variable global utilizando un puntero aparentemente no relacionado. La idea no era prohibir el juego de palabras tipo lanzando y desmarcando un puntero en una sola expresión, sino más bien decir algo dado como:
int x;
int foo(double *d)
{
x++;
*d=1234;
return x;
}
un compilador tendría derecho a suponer que la escritura a * d no afectará a x. Los autores del Estándar querían enumerar situaciones en las que una función como la anterior que recibió un puntero de una fuente desconocida tendría que suponer que podría aliar un mundo aparentemente no relacionado, sin requerir que los tipos coincidan perfectamente. Desafortunadamente, aunque la lógica sugiere que los autores de la Norma intentaron describir una norma para la conformidad mínima en casos en que un compilador no tendría motivos para creer que las cosas podrían ser alias , la regla no exige que los compiladores reconozcan el alias en los casos en que es obvio y los autores de gcc han decidido que preferirían generar el programa más pequeño posible al tiempo que se ajustan al lenguaje mal escrito del estándar, que generar el código que es realmente útil, y en lugar de reconocer el alias en casos donde es obvio (mientras sigan siendo capaces de suponer que las cosas que no se ven como si fueran alias, no) requerirían que los programadores usen memcpy
, lo que requiere un compilador que permita la posibilidad de que los apuntadores de origen desconocido puedan alias casi cualquier cosa, impidiendo así la optimización.
Parece mucho que realmente quieres usar fread:
int data;
fread(&data, sizeof(data), 1, stream);
Dicho esto, si quieres ir a la ruta de lectura de caracteres, luego reinterpretándolos como un int, la forma más segura de hacerlo en C (pero no en C ++) es usar una unión:
union
{
char theChars[4];
int theInt;
} myunion;
for(int i=0; i<4; i++)
myunion.theChars[i] = fgetc(stream);
return myunion.theInt;
No estoy seguro de por qué la longitud de los data
en su código original es 3. Supongo que quería 4 bytes; al menos no conozco ningún sistema donde un int tenga 3 bytes.
Tenga en cuenta que tanto su código como el mío son altamente no portátiles.
Editar: si desea leer entradas de varias longitudes desde un archivo, de manera portable, intente algo como esto:
unsigned result=0;
for(int i=0; i<4; i++)
result = (result << 8) | fgetc(stream);
(Nota: en un programa real, también desearía probar el valor de retorno de fgetc () contra EOF).
Esto dice un 4-byte sin firmar del archivo en formato little-endian, independientemente de cuál sea la endianidad del sistema. Debería funcionar en casi cualquier sistema donde un unsigned tenga al menos 4 bytes.
Si quiere ser endian-neutral, no use punteros o uniones; usar bit-shift en su lugar.
Usar una unión no es lo correcto para hacer aquí. La lectura de un miembro no escrito de la unión no está definida, es decir, el compilador tiene la libertad de realizar optimizaciones que romperán su código (como optimizar la escritura).