2010-07-14 15 views
43

Utilicé el siguiente fragmento de código para leer datos de archivos como parte de un programa más grande.El puntero de punteo de tipo de desreferenciación romperá las reglas de alias estrictos

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 utilizar -O2 y me da aviso siguiente GCC: warning: dereferencing type-punned pointer will break strict-aliasing rules

Googleing encontré dos respuestas ortogonales:

vs

Al final no quiero hacer caso omiso de las advertencias. ¿Qué recomendarías?

[update] He sustituido el ejemplo de juguete con la función real.

+0

Su función devuelve un doble, pero lanza tu regreso a un int. ¿Por qué no lanzar al doble? –

+0

Mi lectura del enlace provisto: el enlace bytes.com parece ser en su mayoría erróneo (en realidad, las cosas han cambiado desde que se lanzó GCC 4.x), mientras que el enlace SO parece estar bien. Ver C99, "6.5 Expressions", cláusula 7. – Dummy00001

+0

Estoy un poco confundido por el mensaje de error porque pensé que las reglas de aliasing excluían los tipos 'char' (es decir, un puntero' char' siempre permite alias otros punteros a menos que sea 'restrict 'ed.) Tal vez tengas que hacer que 'unsigned char' para que se aplique ... Me interesaría ver la respuesta correcta. –

Respuesta

25

Se parece mucho como si realmente desea utilizar fread:

int data; 
fread(&data, sizeof(data), 1, stream); 

Dicho esto, si quieres ir a la ruta de la lectura de caracteres, a continuación, la reinterpretación de ellos como un int, la manera segura de hacerlo en C (pero no en C++) es el uso de un sindicato:

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 data en el código original es 3. asumo que querías 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 enteros de varias longitudes de un archivo, de forma portátil, intentar algo como esto:

unsigned result=0; 
for(int i=0; i<4; i++) 
    result = (result << 8) | fgetc(stream); 

(Nota: En un programa real, tendría, además, querer poner a prueba el valor de retorno de fgetc() contra EOF.)

Esto lee un 4 bytes sin signo del archivo en formato little-endian, independientemente de lo que el orden de bits del sistema es. Debería funcionar en casi cualquier sistema donde un unsigned tenga al menos 4 bytes.

Si desea ser endian-neutral, no use punteros o uniones; usar bit-shift en su lugar.

+6

+1. Para insistir nuevamente: una unión es una forma oficial de mantener el código estricto con aliasing. Esto no es específico de gcc, es solo que el optimizador de gcc está más roto en el respeto. Las advertencias no se deben ignorar: deshabilite explícitamente la optimización de alias fstrict o corrija el código. – Dummy00001

+0

He arreglado el '3-byte-int'. ¿Un sindicato sería portátil? – Framester

+1

@Framester: Depende de lo que desee exportar. La mayoría de los sistemas de escritorio y parientes significan lo mismo con un 'int' de 32 bits, pero algunos son big-endian y otros son small-endian, lo que significa que el orden de los bytes en' int' puede variar. –

1

Básicamente puedes leer el mensaje de gcc como chico que estás buscando problemas, no digas que no te advertí.

Lanzar una matriz de tres bytes a un 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 el int es más ancho) se obtienen 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.

+4

Hola, he sustituido el ejemplo de juguete con la función real. Y el int con 3 bytes fue solo un error de mi parte. – Framester

-4

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 está en lo cierto al emitir una advertencia aunque sea 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 puede probar esto y ver lo que se obtiene:

char *q = reinterpret_cast<char*>(p); 
+3

'reinterpret_cast' es C++. Esto es C. – ptomato

+3

"_la norma permite que sizeof (char *) sea diferente de sizeof (int *) _" o podrían tener el mismo tamaño pero diferentes representaciones, pero de todos modos esto no tiene nada que ver con el problema aquí. Esta pregunta es sobre el juego de palabras, no la representación del puntero. "' char * q = (char *) & p [0] '" el problema no es cómo obtener dos punteros de diferentes tipos para apuntar a la misma dirección. Esta pregunta es sobre el tipo de juego de palabras, no el juego de punteros. – curiousguy

7

Usando 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 puede realizar optimizaciones que romperán su código (como optimizar la escritura).

+0

"_de un miembro no escrito de la unión no está definido_" En este caso simple: 'unión U {int i; pantalones cortos; } u; u.s = 1; devuelve u.i; ', sí. En general, depende. – curiousguy

+2

En C, la unión es un comportamiento bien definido; en C++ es un comportamiento indefinido. –

36

El problema se debe a que acceda a un char-matriz a través de un double*:

char data[8]; 
... 
return *(double*)data; 

Pero gcc supone que su programa nunca acceder a las variables a pesar de punteros de tipo diferente. Esta suposición se llama estricta-aliasing y permite que el compilador para hacer algunas optimizaciones:

Si el compilador sabe que su *(double*) puede en ningún solapamiento camino con data[], que prohibe todo tipo de cosas como reordenar el código en:

return *(double*)data; 
for(int i=7;i>=0;i--) 
    data[i] = fgetc(stream); 

el bucle se haya optimizado de distancia y que terminan con sólo:

return *(double*)data; 

Lo que deja sus datos [] no inicializado. 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. Por lo que puede volver a escribir en:

double data; 
... 
*(((char*)&data) + i) = fgetc(stream); 
... 
return data; 

advertencias aliasing estrictas son realmente importantes para entender o solucionar. Causan los tipos de errores que son imposibles de reproducir internamente porque ocurren solo en un compilador particular en un sistema operativo particular en una máquina en particular y solo en la luna llena y una vez al año, etc.

0

Los autores del estándar C quería que los escritores de compiladores generaran código eficiente en circunstancias en las que sería teóricamente posible pero poco probable que una variable global pudiera tener acceso a su valor usando un puntero aparentemente no relacionado.La idea no fue prohibir tipo de juegos de palabras por colada y eliminación de referencias a un puntero en una sola expresión, sino más bien quiere decir que dado algo como:

int x; 
int foo(double *d) 
{ 
    x++; 
    *d=1234; 
    return x; 
} 

un compilador tendría derecho a suponer que la escritura a * d ganó no afecta 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 del estándar intentaron describir un estándar de conformidad mínima en los casos en que el compilador no tendría razón para creer que las cosas podrían alias, la regla no requiere que los compiladores reconozcan el aliasing en casos donde 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 aliasing en En los casos donde es obvio (sin dejar de suponer que las cosas que no se ven como si fueran alias, no lo harán) preferirían requerir que los programadores usen el memcpy, lo que requiere un compilador que permita la posibilidad de que los punteros de origen desconocido podría alias casi cualquier cosa, por lo tanto impedir optimización de ing.

4

Este documento resume la situación: http://dbp-consulting.com/tutorials/StrictAliasing.html

Hay varias soluciones diferentes allí, pero la más portátil/segura es utilizar memcpy(). (. Las llamadas a funciones pueden ser optimizadas a cabo, así que no es tan ineficaz como aparece) Por ejemplo, sustituir este:

return *(short*)data; 

Con esta:

short temp; 
memcpy(&temp, data, sizeof(temp)); 
return temp; 
+0

esta es la mejor respuesta. – Bob

Cuestiones relacionadas