¿Por qué se afirma que esto hace referencia a la advertencia de puntero de tipo punteado específico del compilador?

38

He leído varias publicaciones en Stack Overflow RE: el error de puntero de tipo punteado de desreferencia. Entiendo que el error es esencialmente la advertencia del compilador del peligro de acceder a un objeto a través de un puntero de un tipo diferente (aunque parece haber una excepción char*), que es una advertencia comprensible y razonable.

Mi pregunta es específica del código a continuación: ¿ por qué la conversión de la dirección de un puntero a una void**calificación para esta advertencia (promovida a error a través de -Werror)?

Además, este código se compila para múltiples arquitecturas de destino, solo una de las cuales genera la advertencia / error. ¿Podría esto implicar que es legítimamente una deficiencia específica de la versión del compilador?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Si mi entendimiento, indicado anteriormente, es correcto, un void** , siendo solo un puntero, esto debería ser un casting seguro.

¿Hay alguna solución alternativa que no utilice valores que alivien esta advertencia / error específico del compilador? Es decir, entiendo eso y por qué esto resolverá el problema, pero me gustaría evitar este enfoque porque quiero aprovechar freeFunc() NULL en un argumento fuera de discusión:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Compilador de problemas (uno de uno):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Compilador que no se queja (uno de muchos):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Actualización: descubrí que la advertencia parece generarse específicamente cuando se compila -O2(solo con el "compilador del problema")

Lanzamiento De Piedra
fuente
1
"a void**, siendo solo un puntero, esto debería ser un casting seguro". Woah hay skippy! Parece que tienes algunas suposiciones fundamentales. Trate de pensar menos en términos de bytes y palancas y más en términos de abstracciones, porque eso es con lo que realmente está programando
Lightness Races en órbita el
77
Tangencialmente, ¡los compiladores que está usando tienen 15 y 17 años! No confiaría en ninguno de los dos.
Tavian Barnes
44
@TavianBarnes Además, si debe confiar en GCC 3 por algún motivo, lo mejor es usar la versión final al final de la vida, que fue 3.4.6, creo. ¿Por qué no aprovechar todas las soluciones disponibles para esa serie antes de que se descartara?
Kaz
¿Qué tipo de estándar de codificación C ++ prescribe todos esos espacios?
Peter Mortensen

Respuestas:

33

Un valor de tipo void**es un puntero a un objeto de tipo void*. Un objeto de tipo Foo*no es un objeto de tipovoid* .

Hay una conversión implícita entre valores de tipo Foo*y void*. Esta conversión puede cambiar la representación del valor. Del mismo modo, puede escribir int n = 3; double x = n;y esto tiene el comportamiento bien definido de establecer xel valor 3.0, pero double *p = (double*)&n;tiene un comportamiento indefinido (y en la práctica no se establecerá pen un "puntero a 3.0" en ninguna arquitectura común).

Las arquitecturas donde diferentes tipos de punteros a objetos tienen diferentes representaciones son raras hoy en día, pero están permitidas por el estándar C. Hay máquinas antiguas (raras) con punteros de palabras que son direcciones de una palabra en la memoria y punteros de bytes que son direcciones de una palabra junto con un desplazamiento de bytes en esta palabra; Foo*sería un puntero de palabras y void*sería un puntero de bytes en tales arquitecturas. Hay máquinas (raras) con punteros gordos que contienen información no solo sobre la dirección del objeto, sino también sobre su tipo, su tamaño y sus listas de control de acceso; un puntero a un tipo definido podría tener una representación diferente de la void*que necesita información de tipo adicional en tiempo de ejecución.

Tales máquinas son raras, pero están permitidas por el estándar C. Y algunos compiladores de C aprovechan el permiso para tratar los punteros punteados como distintos para optimizar el código. El riesgo de alias de punteros es una limitación importante a la capacidad de un compilador para optimizar el código, por lo que los compiladores tienden a aprovechar tales permisos.

Un compilador es libre de decirle que está haciendo algo mal, o de hacer en silencio lo que no quería, o de hacer en silencio lo que quería. El comportamiento indefinido permite cualquiera de estos.

Puedes hacer freefuncuna macro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Esto viene con las limitaciones habituales de las macros: falta de seguridad de tipo, pse evalúa dos veces. Tenga en cuenta que esto solo le da la seguridad de no dejar punteros colgantes alrededor si pfuera el puntero único al objeto liberado.

Gilles 'SO- deja de ser malvado'
fuente
1
Y es bueno saber que incluso si Foo*y void*tiene la misma representación en su arquitectura, todavía no está definido para escribirlos.
Tavian Barnes
12

A void *es tratado especialmente por el estándar C en parte porque hace referencia a un tipo incompleto. Este tratamiento no se extiende a void **como lo hace el punto a un tipo completo, específicamente void *.

Las estrictas reglas de alias dicen que no puede convertir un puntero de un tipo en un puntero de otro tipo y, posteriormente, desreferenciar ese puntero porque hacerlo significa reinterpretar los bytes de un tipo como otro. La única excepción es cuando se convierte a un tipo de carácter que le permite leer la representación de un objeto.

Puede sortear esta limitación utilizando una macro similar a una función en lugar de una función:

#define freeFunc(obj) (free(obj), (obj) = NULL)

A lo que puedes llamar así:

freeFunc(f);

Sin embargo, esto tiene una limitación, porque la macro anterior se evaluará objdos veces. Si está utilizando GCC, esto se puede evitar con algunas extensiones, específicamente las typeofexpresiones de palabras clave y declaraciones:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
fuente
3
+1 para proporcionar una mejor implementación del comportamiento previsto. El único problema que veo con el #definees que se evaluará objdos veces. Sin embargo, no conozco una buena manera de evitar esa segunda evaluación. Incluso una expresión de declaración (extensión GNU) no funcionará, ya que debe asignarla objdespués de haber usado su valor.
cmaster - reinstalar a monica el
2
@cmaster: Si usted está dispuesto a utilizar extensiones de GNU, como expresiones de los estados, a continuación, se puede utilizar typeofpara evitar la evaluación de objdos veces: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh
@ruakh Muy bueno :-) Sería genial si dbush editara eso en la respuesta, por lo que no se eliminará en masa con los comentarios.
cmaster - reinstalar a monica el
9

Desreferenciar un puntero de tipo punteado es UB y no puede contar con lo que sucederá.

Diferentes compiladores generan diferentes advertencias, y para este propósito, diferentes versiones del mismo compilador pueden considerarse como diferentes compiladores. Esta parece una mejor explicación para la variación que ve que una dependencia de la arquitectura.

Un caso que puede ayudarlo a comprender por qué la escritura de tipos en este caso puede ser mala es que su función no funcionará en una arquitectura para la cual sizeof(Foo*) != sizeof(void*). Eso está autorizado por el estándar, aunque no conozco ninguno para el cual esto sea cierto.

Una solución alternativa sería utilizar una macro en lugar de una función.

Tenga en cuenta que freeacepta punteros nulos.

Un programador
fuente
2
Fascinante que sea posible eso sizeof Foo* != sizeof void*. Nunca he encontrado que los tamaños de puntero "en estado salvaje" sean dependientes del tipo, por lo que a lo largo de los años, he llegado a considerar axiomático que los tamaños de puntero son todos iguales en una arquitectura dada.
StoneThrow
1
@Stonethrow, el ejemplo estándar son los punteros gordos que se usan para direccionar bytes en la arquitectura direccionable de palabras. Pero creo que las máquinas direccionables de palabras actuales usan la alternativa sizeof char == sizeof word .
Programador
2
Tenga en cuenta que el tipo debe estar entre paréntesis para sizeof ...
Antti Haapala
@StoneThrow: independientemente del tamaño del puntero, el análisis de alias basado en tipos lo hace inseguro; esto ayuda a los compiladores a optimizar al suponer que una tienda a través de un float*no modificará un int32_tobjeto, por ejemplo, int32_t*no es necesario int32_t *restrict ptrque el compilador suponga que no está apuntando a la misma memoria. Lo mismo para las tiendas a través de un void**ser que se supone que no modifica un Foo*objeto.
Peter Cordes el
4

Este código no es válido según el estándar C, por lo que podría funcionar en algunos casos, pero no es necesariamente portátil.

La "regla de alias estricto" para acceder a un valor a través de un puntero que se ha convertido a un tipo de puntero diferente se encuentra en 6.5 párrafo 7:

Un objeto tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos:

  • un tipo compatible con el tipo efectivo del objeto,

  • una versión calificada de un tipo compatible con el tipo efectivo del objeto,

  • un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,

  • un tipo que es el tipo con signo o sin signo correspondiente a una versión calificada del tipo efectivo del objeto,

  • un tipo agregado o de unión que incluye uno de los tipos antes mencionados entre sus miembros (incluido, recursivamente, un miembro de una unión agregada o contenida), o

  • un tipo de personaje

En su *obj = NULL;declaración, el objeto tiene un tipo efectivo Foo*pero se accede mediante la expresión lvalue *objcon type void*.

En 6.7.5.1 párrafo 2, tenemos

Para que dos tipos de puntero sean compatibles, ambos deberán estar identificados de forma idéntica y ambos deberán ser punteros a tipos compatibles.

Por lo tanto void*y Foo*no son compatibles con los tipos o tipos compatibles con calificadores adicionales, y ciertamente no se ajustan a ninguna de las otras opciones de la regla de alias estricto.

Aunque no es la razón técnica por la que el código no es válido, también es relevante tener en cuenta la sección 6.2.5, párrafo 26:

Un puntero a voidtendrá los mismos requisitos de representación y alineación que un puntero a un tipo de carácter. Del mismo modo, los punteros a versiones calificadas o no calificadas de tipos compatibles tendrán los mismos requisitos de representación y alineación. Todos los punteros a los tipos de estructura deben tener los mismos requisitos de representación y alineación que los demás. Todos los punteros a tipos de unión tendrán los mismos requisitos de representación y alineación que los demás. Los punteros a otros tipos no necesitan tener los mismos requisitos de representación o alineación.

En cuanto a las diferencias en las advertencias, este no es un caso en el que el Estándar requiere un mensaje de diagnóstico, por lo que es solo una cuestión de cuán bueno es el compilador o su versión para detectar posibles problemas y señalarlos de manera útil. Notó que la configuración de optimización puede marcar la diferencia. Esto a menudo se debe a que se genera más información internamente acerca de cómo varias partes del programa realmente encajan en la práctica, y esa información adicional también está disponible para verificaciones de advertencia.

aschepler
fuente
2

Además de lo que han dicho las otras respuestas, este es un antipatrón clásico en C, y uno que debe quemarse con fuego. Aparece en:

  1. Funciones libres y nulas como la que encontró la advertencia.
  2. Funciones de asignación que evitan el idioma estándar de retorno de C void *(que no sufre de este problema porque implica una conversión de valor en lugar de un punteo de tipo ), en cambio devuelve un indicador de error y almacena el resultado mediante un puntero a puntero.

Para otro ejemplo de (1), hubo un caso infame de larga data en la av_freefunción de ffmpeg / libavcodec . Creo que finalmente se solucionó con una macro o algún otro truco, pero no estoy seguro.

Para (2), ambos cudaMallocy posix_memalignson ejemplos.

En ninguno de los casos, la interfaz requiere de manera inherente un uso no válido, pero lo alienta encarecidamente y admite el uso correcto solo con un objeto temporal adicional de tipo void *que anula el propósito de la funcionalidad de libre y nulo, y hace que la asignación sea incómoda.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
¿Tiene un enlace que explique más sobre por qué (1) es un antipatrón? No creo que esté familiarizado con esta situación / argumento y me gustaría aprender más.
StoneThrow
1
@StoneThrow: Es realmente simple: la intención es evitar el mal uso anulando el objeto que almacena el puntero en la memoria que se está liberando, pero la única forma en que puede hacerlo es si la persona que llama realmente está almacenando el puntero en un objeto de tipear void *y lanzar / convertir cada vez que quiera desreferenciarlo. Esto es muy improbable. Si la persona que llama está almacenando algún otro tipo de puntero, la única forma de llamar a la función sin invocar a UB es copiar el puntero a un objeto temporal de tipo void *y pasar la dirección de eso a la función de liberación, y luego simplemente ...
R .. GitHub DEJA DE AYUDAR AL HIELO
1
... anula un objeto temporal en lugar del almacenamiento real donde la persona que llama tenía el puntero. Por supuesto, lo que realmente sucede es que los usuarios de la función terminan haciendo un (void **)casting, produciendo un comportamiento indefinido.
R .. GitHub DEJA DE AYUDAR AL HIELO
2

Aunque C fue diseñado para máquinas que usan la misma representación para todos los punteros, los autores de la Norma querían hacer que el lenguaje sea utilizable en máquinas que usan diferentes representaciones para punteros a diferentes tipos de objetos. Por lo tanto, no requerían que las máquinas que usan diferentes representaciones de puntero para diferentes tipos de punteros admitan un tipo de "puntero a cualquier tipo de puntero", aunque muchas máquinas podrían hacerlo a un costo cero.

Antes de que se redactara el Estándar, las implementaciones para plataformas que usaban la misma representación para todos los tipos de puntero permitirían unánimemente que void**se utilizara un, al menos con una conversión adecuada, como un "puntero a cualquier puntero". Es casi seguro que los autores de la Norma reconocieron que esto sería útil en plataformas que lo admitieran, pero como no podía ser universalmente admitido, se negaron a exigirlo. En cambio, esperaban que la implementación de calidad procesara construcciones tales como lo que la Justificación describiría como una "extensión popular", en los casos en que hacerlo tendría sentido.

Super gato
fuente