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")
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á programandoRespuestas:
Un valor de tipo
void**
es un puntero a un objeto de tipovoid*
. Un objeto de tipoFoo*
no es un objeto de tipovoid*
.Hay una conversión implícita entre valores de tipo
Foo*
yvoid*
. Esta conversión puede cambiar la representación del valor. Del mismo modo, puede escribirint n = 3; double x = n;
y esto tiene el comportamiento bien definido de establecerx
el valor3.0
, perodouble *p = (double*)&n;
tiene un comportamiento indefinido (y en la práctica no se estableceráp
en un "puntero a3.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 yvoid*
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 lavoid*
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
freefunc
una macro:Esto viene con las limitaciones habituales de las macros: falta de seguridad de tipo,
p
se evalúa dos veces. Tenga en cuenta que esto solo le da la seguridad de no dejar punteros colgantes alrededor sip
fuera el puntero único al objeto liberado.fuente
Foo*
yvoid*
tiene la misma representación en su arquitectura, todavía no está definido para escribirlos.A
void *
es tratado especialmente por el estándar C en parte porque hace referencia a un tipo incompleto. Este tratamiento no se extiende avoid **
como lo hace el punto a un tipo completo, específicamentevoid *
.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:
A lo que puedes llamar así:
Sin embargo, esto tiene una limitación, porque la macro anterior se evaluará
obj
dos veces. Si está utilizando GCC, esto se puede evitar con algunas extensiones, específicamente lastypeof
expresiones de palabras clave y declaraciones:fuente
#define
es que se evaluaráobj
dos 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 asignarlaobj
después de haber usado su valor.typeof
para evitar la evaluación deobj
dos veces:#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.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
free
acepta punteros nulos.fuente
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.float*
no modificará unint32_t
objeto, por ejemplo,int32_t*
no es necesarioint32_t *restrict ptr
que el compilador suponga que no está apuntando a la misma memoria. Lo mismo para las tiendas a través de unvoid**
ser que se supone que no modifica unFoo*
objeto.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:
En su
*obj = NULL;
declaración, el objeto tiene un tipo efectivoFoo*
pero se accede mediante la expresión lvalue*obj
con typevoid*
.En 6.7.5.1 párrafo 2, tenemos
Por lo tanto
void*
yFoo*
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:
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.
fuente
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:
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_free
función de ffmpeg / libavcodec . Creo que finalmente se solucionó con una macro o algún otro truco, pero no estoy seguro.Para (2), ambos
cudaMalloc
yposix_memalign
son 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.fuente
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 tipovoid *
y pasar la dirección de eso a la función de liberación, y luego simplemente ...(void **)
casting, produciendo un comportamiento indefinido.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.fuente