J'ai lu divers articles sur Stack Overflow RE: l'erreur de pointeur punencé par déréférencement. Ma compréhension est que l'erreur est essentiellement l'avertissement du compilateur du danger d'accéder à un objet via un pointeur d'un type différent (bien qu'une exception semble être faite pour char*
), ce qui est un avertissement compréhensible et raisonnable.
Ma question est spécifique au code ci-dessous: pourquoi la conversion de l'adresse d'un pointeur en un void**
qualifie-t-elle pour cet avertissement (promu en erreur via -Werror
)?
De plus, ce code est compilé pour plusieurs architectures cibles, dont une seule génère l'avertissement / l'erreur - cela peut-il impliquer qu'il s'agit légitimement d'une déficience spécifique à la version du compilateur?
// 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 ma compréhension, énoncée ci-dessus, est correcte, a void**
, n'étant encore qu'un pointeur, cela devrait être un casting sûr.
Existe-t-il une solution de contournement n'utilisant pas les valeurs l qui pacifierait cet avertissement / erreur spécifique au compilateur? C'est-à-dire que je comprends cela et pourquoi cela résoudra le problème, mais je voudrais éviter cette approche parce que je veux profiter de freeFunc()
NULL ing un out-arg prévu:
void* tmp = f;
freeFunc( &tmp );
f = NULL;
Compilateur de problèmes (l'un parmi les autres):
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$
Compilateur non conforme (l'un des nombreux):
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$
Mise à jour: j'ai en outre découvert que l'avertissement semble être généré spécifiquement lors de la compilation avec -O2
(toujours avec le "compilateur de problèmes" noté uniquement)
void**
, n'étant toujours qu'un pointeur, ce devrait être un casting sûr." Woah là skippy! On dirait que vous avez des hypothèses fondamentales en cours. Essayez de penser moins en termes d'octets et de leviers et plus en termes d'abstractions, car c'est ce que vous programmez avecRéponses:
Une valeur de type
void**
est un pointeur sur un objet de typevoid*
. Un objet de typeFoo*
n'est pas un objet de typevoid*
.Il existe une conversion implicite entre les valeurs de type
Foo*
etvoid*
. Cette conversion peut modifier la représentation de la valeur. De même, vous pouvez écrireint n = 3; double x = n;
et cela a le comportement bien défini de définirx
la valeur3.0
, maisdouble *p = (double*)&n;
a un comportement indéfini (et dans la pratique, ne définirap
pas un «pointeur vers3.0
» sur une architecture commune).Les architectures où différents types de pointeurs vers des objets ont des représentations différentes sont rares de nos jours, mais elles sont autorisées par la norme C. Il existe de (rares) vieilles machines avec des pointeurs de mots qui sont des adresses d'un mot en mémoire et des pointeurs d'octets qui sont des adresses d'un mot avec un décalage d'octet dans ce mot;
Foo*
serait un pointeur de mot etvoid*
serait un pointeur d'octet sur de telles architectures. Il existe de (rares) machines avec de gros pointeurs qui contiennent des informations non seulement sur l'adresse de l'objet, mais aussi sur son type, sa taille et ses listes de contrôle d'accès; un pointeur vers un type défini peut avoir une représentation différente de cellevoid*
qui a besoin d'informations de type supplémentaires lors de l'exécution.Ces machines sont rares, mais autorisées par la norme C. Et certains compilateurs C profitent de la permission de traiter les pointeurs de type punned comme distincts pour optimiser le code. Le risque d'alias de pointeurs est une limitation majeure de la capacité d'un compilateur à optimiser le code, de sorte que les compilateurs ont tendance à tirer parti de ces autorisations.
Un compilateur est libre de vous dire que vous faites quelque chose de mal, ou de faire tranquillement ce que vous ne vouliez pas, ou de faire tranquillement ce que vous vouliez. Un comportement indéfini permet tout cela.
Vous pouvez créer
freefunc
une macro:Cela vient avec les limitations habituelles des macros: le manque de sécurité de type,
p
est évalué deux fois. Notez que cela vous donne uniquement la sécurité de ne pas laisser de pointeurs pendants sip
c'était le seul pointeur vers l'objet libéré.la source
Foo*
etvoid*
ont la même représentation sur votre architecture, il n'est toujours pas défini de les punir.UNE
void *
est traité spécialement par la norme C en partie parce qu'il fait référence à un type incomplet. Ce traitement ne pas étendre àvoid **
comme il le fait le point à un type complet, en particuliervoid *
.Les règles strictes d'alias disent que vous ne pouvez pas convertir un pointeur d'un type en un pointeur d'un autre type et par la suite déréférencer ce pointeur car cela signifie réinterpréter les octets d'un type comme un autre. La seule exception est lors de la conversion en un type de caractère qui vous permet de lire la représentation d'un objet.
Vous pouvez contourner cette limitation en utilisant une macro de type fonction au lieu d'une fonction:
Que vous pouvez appeler comme ceci:
Cela a cependant une limitation, car la macro ci-dessus sera évaluée
obj
deux fois. Si vous utilisez GCC, cela peut être évité avec certaines extensions, en particulier lestypeof
expressions de mots clés et d'instructions:la source
#define
, c'est qu'il évalueraobj
deux fois. Je ne connais cependant pas un bon moyen d'éviter cette deuxième évaluation. Même une expression de déclaration (extension GNU) ne fera pas l'affaire comme vous devez l'attribuerobj
après avoir utilisé sa valeur.typeof
pour éviter d' évaluerobj
deux fois:#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.Déréférencer un pointeur de type punished est UB et vous ne pouvez pas compter sur ce qui va se passer.
Différents compilateurs génèrent des avertissements différents et, à cet effet, différentes versions du même compilateur peuvent être considérées comme des compilateurs différents. Cela semble une meilleure explication de la variance que vous voyez qu'une dépendance à l'architecture.
Un cas qui peut vous aider à comprendre pourquoi la punition de type dans ce cas peut être mauvais est que votre fonction ne fonctionnera pas sur une architecture pour laquelle
sizeof(Foo*) != sizeof(void*)
. Cela est autorisé par la norme, bien que je n'en connaisse aucune pour laquelle cela soit vrai.Une solution de contournement consisterait à utiliser une macro au lieu d'une fonction.
Notez que
free
accepte les pointeurs nuls.la source
sizeof Foo* != sizeof void*
. Je n'ai jamais rencontré de tailles de pointeur "à l'état sauvage" dépendant du type, donc au fil des ans, je suis venu à considérer comme axiomatique que les tailles de pointeur sont toutes les mêmes sur une architecture donnée.float*
ne modifiera pas unint32_t
objet, donc par exemple unint32_t*
n'a pas besoinint32_t *restrict ptr
que le compilateur suppose qu'il ne pointe pas vers la même mémoire. Idem pour les magasins via unvoid**
être supposé ne pas modifier unFoo*
objet.Ce code n'est pas valide selon la norme C, il peut donc fonctionner dans certains cas, mais il n'est pas nécessairement portable.
La "règle d'aliasing stricte" pour accéder à une valeur via un pointeur qui a été transtypé en un type de pointeur différent se trouve dans 6.5 paragraphe 7:
Dans votre
*obj = NULL;
instruction, l'objet a un type effectifFoo*
mais est accessible par l'expression lvalue*obj
de typevoid*
.Au 6.7.5.1, paragraphe 2, nous avons
Donc
void*
, ceFoo*
ne sont pas des types compatibles ou des types compatibles avec des qualificatifs ajoutés, et ne correspondent certainement pas aux autres options de la règle d'alias stricte.Bien que ce ne soit pas la raison technique pour laquelle le code n'est pas valide, il convient également de noter la section 6.2.5, paragraphe 26:
En ce qui concerne les différences d'avertissements, ce n'est pas un cas où la norme nécessite un message de diagnostic, il s'agit simplement de savoir dans quelle mesure le compilateur ou sa version est capable de remarquer les problèmes potentiels et de les signaler de manière utile. Vous avez remarqué que les paramètres d'optimisation peuvent faire une différence. Cela est souvent dû au fait que davantage d'informations sont générées en interne sur la façon dont les différents éléments du programme s'emboîtent réellement dans la pratique, et que des informations supplémentaires sont donc également disponibles pour les vérifications d'avertissement.
la source
En plus de ce que les autres réponses ont dit, ceci est un anti-modèle classique en C, et qui devrait être brûlé par le feu. Il apparaît dans:
void *
(qui ne souffre pas de ce problème car il implique une conversion de valeur au lieu de la punition de type ), renvoyant à la place un indicateur d'erreur et stockant le résultat via un pointeur à pointeur.Pour un autre exemple de (1), il y avait un cas tristement célèbre de longue date dans ffmpeg / libavcodec
av_free
fonction . Je crois que cela a finalement été corrigé avec une macro ou une autre astuce, mais je ne suis pas sûr.Pour (2), les deux
cudaMalloc
etposix_memalign
sont des exemples.Dans aucun des deux cas, l'interface ne nécessite intrinsèquement une utilisation non valide, mais elle l'encourage fortement et admet une utilisation correcte uniquement avec un objet temporaire supplémentaire de type
void *
qui va à l'encontre de l'objectif de la fonctionnalité free-and-null-out et rend l'allocation maladroite.la source
void *
et le lancer / le convertir chaque fois qu'il veut le déréférencer. C'est très peu probable. Si l'appelant stocke un autre type de pointeur, la seule façon d'appeler la fonction sans invoquer UB est de copier le pointeur sur un objet temporaire de typevoid *
et de transmettre l'adresse de celui-ci à la fonction de libération, puis il suffit ...(void **)
cast, produisant un comportement indéfini.Bien que C ait été conçu pour des machines qui utilisent la même représentation pour tous les pointeurs, les auteurs de la norme ont voulu rendre le langage utilisable sur des machines qui utilisent différentes représentations pour les pointeurs vers différents types d'objets. Par conséquent, ils n'exigeaient pas que les machines qui utilisent différentes représentations de pointeurs pour différents types de pointeurs prennent en charge un type de "pointeur vers n'importe quel type de pointeur", même si de nombreuses machines pouvaient le faire à un coût nul.
Avant la rédaction de la norme, les implémentations pour les plates-formes qui utilisaient la même représentation pour tous les types de pointeurs permettraient à l'unanimité
void**
d'utiliser, au moins avec une conversion appropriée, comme "pointeur vers n'importe quel pointeur". Les auteurs de la norme ont presque certainement reconnu que cela serait utile sur les plateformes qui la prenaient en charge, mais comme elle ne pouvait pas être universellement prise en charge, ils ont refusé de l'exiger. Au lieu de cela, ils s'attendaient à ce que la mise en œuvre de qualité traite des constructions telles que ce que la justification décrirait comme une "extension populaire", dans les cas où cela serait logique.la source