Une implémentation C conforme #define NULL peut-elle être farfelue

Je demande à cause de la discussion qui a été provoquée dans ce fil .

Essayer d’avoir une discussion sérieuse en utilisant des commentaires sous les réponses d’autres personnes n’est pas facile ni amusant. J’aimerais donc savoir ce que nos experts en C pensent sans être limités à 500 caractères à la fois.

La norme C contient peu de mots précieux sur les constantes de pointeur NULL et NULL. Je ne peux trouver que deux sections pertinentes. Premier:

3.2.2.3 Pointeurs

Une expression de constante intégrale avec la valeur 0, ou une telle expression convertie dans le type void *, est appelée constante de pointeur null. Si une constante de pointeur null est assignée ou comparée pour l’égalité à un pointeur, la constante est convertie en un pointeur de ce type. Un tel pointeur, appelé pointeur null, garantit la comparaison d’un pointeur avec un object ou une fonction.

et deuxieme:

4.1.5 Définitions communes

Les macros sont

 NULL 

qui se développe en une constante de pointeur null définie par la mise en oeuvre;

La question est de savoir si NULL peut être étendu à une constante de pointeur null définie par l’implémentation qui est différente de celles énumérées en 3.2.2.3.

En particulier, pourrait-il être défini comme:

 #define NULL __builtin_magic_null_pointer 

Ou même:

 #define NULL ((void*)-1) 

Mon interprétation de 3.2.2.3 est qu’elle spécifie qu’une expression constante intégrale de 0 et une expression constante intégrale de 0 transtypée en type void * doivent figurer parmi les formes de constante de pointeur nulle que l’implémentation reconnaît, mais ne le faisant pas censé être une liste exhaustive. Je pense que l’implémentation est libre de reconnaître d’autres constructions source en tant que constantes de pointeur nul, tant qu’aucune autre règle n’est cassée.

Ainsi, par exemple, il est prouvable que

 #define NULL (-1) 

n’est pas une définition légale, car

 if (NULL) do_stuff(); 

do_stuff() ne doit pas être appelé, alors que avec

 if (-1) do_stuff(); 

do_stuff() doit être appelé; comme ils sont équivalents, cela ne peut pas être une définition légale de NULL .

Toutefois, la norme indique que les conversions d’entier en pointeur (et vice-versa) sont définies par l’implémentation. Par conséquent, elle pourrait définir la conversion de -1 en pointeur en tant que conversion produisant un pointeur nul. Dans quel cas

 if ((void*)-1) 

évaluerait à faux, et tout irait bien.

Alors, que pensent les autres?

Je demanderais à tout le monde de garder particulièrement à l’esprit la règle “as-if” décrite au 2.1.2.3 Program execution . C’est énorme et quelque peu détourné, je ne vais donc pas le coller ici, mais cela indique essentiellement qu’une implémentation doit simplement produire les mêmes effets secondaires observables que ceux requirejs pour la machine abstraite décrite par la norme. Il indique que les optimisations, les transformations ou tout ce que le compilateur souhaite apporter à votre programme sont parfaitement légales tant qu’elles ne modifient pas les effets secondaires observables du programme.

Donc, si vous cherchez à prouver qu’une définition particulière de NULL ne peut pas être légale, vous devrez créer un programme capable de le prouver. Soit une personne comme la mienne qui enfreint de manière flagrante d’autres clauses de la norme, ou une autre qui peut détecter légalement la magie que doit avoir le compilateur pour faire fonctionner l’étrange définition NULL.

Steve Jessop a trouvé un exemple de méthode permettant à un programme de détecter que NULL n’est pas défini comme l’une des deux formes de constantes de pointeur nul dans 3.2.2.3, qui consiste à ssortingnger la constante:

 #define ssortingngize_helper(x) #x #define ssortingngize(x) ssortingngize_helper(x) 

En utilisant cette macro, on pourrait

 puts(ssortingngize(NULL)); 

et “détecte” que NULL ne s’étend pas à l’un des formulaires de 3.2.2.3. Est-ce suffisant pour rendre d’autres définitions illégales? Je ne sais tout simplement pas.

Merci!

Dans la norme C99, le §7.17.3 indique que NULL «se développe en une constante de pointeur nulle définie par l’implémentation». Pendant ce temps, le § 6.3.2.3.3 définit la constante de pointeur null comme «une expression constante entière avec la valeur 0, ou une telle expression transtypée en type void * ». Comme il n’y a pas d’autre définition pour une constante de pointeur null, une définition conforme de NULL doit être étendue à une expression constante entière avec la valeur zéro (ou cette conversion en void * ).

Citation supplémentaire de la question 5.5 de la FAQ sur le C (soulignement ajouté):

La section 4.1.5 de la norme C stipule que NULL “se développe en une constante de pointeur null définie par l’implémentation”, ce qui signifie que l’implémentation doit choisir la forme de 0 à utiliser et si elle doit utiliser une conversion `void * ‘; voir les questions 5.6 et 5.7. “Défini par l’implémentation” ne signifie pas ici que NULL peut être # défini pour correspondre à une valeur de pointeur null interne non nulle spécifique à l’implémentation .

C’est parfaitement logique. comme la norme requirejs une constante entière égale à zéro dans les contextes de pointeur pour être compilée en un pointeur nul (que la représentation interne de la machine ait ou non une valeur de zéro), le cas où NULL est défini à zéro doit être traité de toute façon. Le programmeur n’est pas obligé de taper NULL pour obtenir des pointeurs nuls; c’est juste une convention stylistique (et peut aider à rattraper les erreurs, par exemple lorsqu’un NULL défini comme (void *)0 est utilisé dans un contexte sans pointeur).

Edit: Une des sources de confusion semble ici être le langage concis utilisé par la norme, c’est-à-dire qu’il ne dit pas explicitement qu’aucune autre valeur ne peut être considérée comme une constante de pointeur nulle. Cependant, lorsque la norme dit “… est appelée une constante de pointeur null”, cela signifie que les définitions données sont exactement appelées constantes de pointeur null. Il n’est pas nécessaire de suivre explicitement chaque définition en indiquant ce qui est non conforme lorsque (par définition) la norme définit ce qui est conforme.

Cela élargit un peu certaines des autres réponses et fait ressortir certains points qui ont échappé aux autres.

Les citations sont à N1570 une ébauche de la norme ISO C 2011. Je ne pense pas qu’il y ait eu de changements significatifs dans ce domaine depuis la norme ANSI C de 1989 (équivalente à la norme ISO C de 1990). Une référence telle que “7.19p3” fait référence à la sous-section 7.19, paragraphe 3. (Les citations dans la question semblent se rapporter à la norme ANSI de 1989, qui décrit le langage dans la section 3 et la bibliothèque à la section 4. Toutes les éditions de la norme ISO décrivez le langage dans la section 6 et la bibliothèque dans la section 7.)

7.19p3 exige que la macro NULL étendue à “une constante de pointeur null définie par l’implémentation”.

6.3.2.3p3 dit:

Une expression constante entière avec la valeur 0, ou une telle expression convertie dans le type void * , est appelée constante de pointeur null .

Puisque la constante du pointeur nul est en italique, c’est la définition du terme (3p1 spécifie cette convention) – ce qui implique que rien de ce qui est spécifié ne peut être une constante du pointeur nul. (La norme ne suit pas toujours ssortingctement cette convention pour ses définitions, mais cela ne pose aucun problème si elle le fait dans ce cas.)

Donc, si nous “quelque chose de farfelu”, nous devons regarder ce que peut être une “expression constante entière”.

La phrase constante pointeur nul doit être considérée comme un terme unique et non comme une phrase dont la signification dépend des mots qui le constituent. En particulier, la constante entière 0 est une constante de pointeur nulle, quel que soit le contexte dans lequel elle apparaît; cela ne doit pas nécessairement donner une valeur de pointeur nulle, et il s’agit d’un type int , et non d’un type de pointeur.

Une “expression constante entière avec la valeur 0” peut être un nombre quelconque de choses (infiniment nombreuses si nous ignorons les limites de capacité). Un 0 littéral est le plus évident. Les autres possibilités sont 0x0 , 00000 , 1-1 , '\0' et '-'-'-' . (La formulation 0L pas clairement si “la valeur 0” se réfère spécifiquement à la valeur de type int , mais je pense que le consensus est que 0L est également une constante de pointeur null valide.)

Une autre clause pertinente est la 6.6p10:

Une implémentation peut accepter d’autres formes d’expressions constantes.

Ce n’est pas tout à fait clair (pour moi) quelle latitude cela est censé permettre. Par exemple, un compilateur peut prendre en charge les littéraux binarys en tant qu’extension; alors 0b0 serait une constante de pointeur null valide. Cela pourrait également permettre des références de style C ++ aux objects const , de sorte que

 const int x = 0; 

une référence à x pourrait être une expression constante (ce n’est pas en C standard).

Il est donc clair que 0 est une constante de pointeur nulle et qu’il s’agit d’une définition valide pour la macro NULL .

Il est également clair que (void*)0 est une constante de pointeur nulle, mais ce n’est pas une définition valide pour NULL , en raison de 7.1.2p5:

Toute définition d’une macro de type object décrite dans la présente clause doit être étendue à un code entièrement protégé par des parenthèses, le cas échéant, afin qu’elle soit regroupée dans une expression arbitraire comme s’il s’agissait d’un identificateur unique.

Si NULL étendu à (void*)0 , l’expression sizeof NULL serait une erreur de syntaxe.

Alors qu’en est-il de ((void*)0) ? Eh bien, je suis sûr à 99,9% qu’il s’agit d’une définition valide pour NULL , mais la version 6.5.1, qui décrit les expressions entre parenthèses, indique:

Une expression entre parenthèses est une expression primaire. Son type et sa valeur sont identiques à ceux de l’expression non mise entre parenthèses. Il s’agit d’une lvalue, d’un désignateur de fonction ou d’une expression vide si l’expression non entre-parentée est respectivement une lvalue, un désignateur de fonction ou une expression vide.

Il ne dit pas qu’une constante de pointeur null entre parenthèses est une constante de pointeur nulle. Néanmoins, autant que je sache, tous les compilateurs C supposent de manière raisonnable qu’une constante de pointeur null entre parenthèses est une constante de pointeur nulle, faisant de ((void*)0 une définition valide pour NULL .

Que se passe-t-il si un pointeur nul est représenté non pas en tant que tout-bits-zéro, mais en tant que tout autre motif de bits, par exemple, un équivalent à 0xFFFFFFFF . Alors (void*)0xFFFFFFFF , même s’il s’avère que l’évaluation en un pointeur nul n’est pas une constante du pointeur nul, tout simplement parce qu’elle ne satisfait pas la définition de ce terme.

Alors, quelles autres variantes sont autorisées par la norme?

Comme les implémentations peuvent accepter d’autres formes d’expression constante, un compilateur pourrait définir __null tant qu’expression constante de type int avec la valeur 0 , autorisant soit __null soit ((void*)__null) comme définition de NULL . Cela pourrait aussi faire de __null une constante de type pointeur , mais il ne pourrait pas ensuite utiliser __null comme définition de NULL , car il ne correspond pas à la définition de 6.3.2.3p3.

Un compilateur pourrait accomplir la même chose, sans magie de compilateur, comme ceci:

 enum { __null }; #define NULL __null 

Ici, __null est une expression constante entière de type int avec la valeur 0 ; elle peut donc être utilisée partout où une constante 0 peut être utilisée.

L’avantage de définir NULL en termes de symbole comme __null est que le compilateur peut alors émettre un avertissement (éventuellement optionnel) si NULL est utilisé dans une constante autre qu’un pointeur. Par exemple, ceci:

 char c = NULL; /* PLEASE DON'T DO THIS */ 

est parfaitement légal si NULL est défini sur 0 ; développer NULL avec un jeton reconnaissable tel que __null permettrait au compilateur de détecter plus facilement cette construction douteuse.

Eh bien, j’ai trouvé un moyen de prouver que

 #define NULL ((void*)-1) 

n’est pas une définition légale de NULL.

 int main(void) { void (*fp)() = NULL; } 

Initialiser un pointeur de fonction avec NULL est légal et correct, alors que …

 int main(void) { void (*fp)() = (void*)-1; } 

… est une violation de contrainte qui nécessite un diagnostic. Donc c’est fini.

Mais la définition de __builtin_magic_null_pointer de NULL ne souffrirait pas de ce problème. J’aimerais toujours savoir si quelqu’un peut trouver une raison pour laquelle cela ne peut pas être.

Âges plus tard, mais personne n’a soulevé ce point: supposons que l’implémentation choisisse effectivement d’utiliser

 #define NULL __builtin_null 

Mon interprétation de C99 est que c’est très bien tant que le mot clé spécial __builtin_null se comporte comme s’il était “une expression constante intégrale avec la valeur 0” ou “une expression constante intégrale avec la valeur 0, converti en void * “. En particulier, si la mise en œuvre choisit la première de ces options, alors

 int x = __builtin_null; int y = __builtin_null + 1; 

est une unité de traduction valide, définissant x et y sur les valeurs entières 0 et 1 respectivement. S’il choisit ce dernier, bien sûr, les deux sont des violations de contrainte (6.5.16.1, 6.5.6 respectivement; void * n’est pas un “pointeur sur un type d’object” selon 6.2.5p19; 6.7.8p11 applique les contraintes d’affectation à l’initialisation ). Et je ne vois pas pourquoi une implémentation le ferait si elle ne fournissait pas de meilleurs diagnostics pour un “mauvais usage” de NULL, de sorte qu’il semble probable que l’option qui invaliderait davantage de code serait prise.

Une expression de constante intégrale avec la valeur 0, ou une telle expression convertie dans le type void *, est appelée constante de pointeur null .

NULL qui se développe en une constante de pointeur null définie par l’implémentation;

donc soit

NULL == 0

ou

NULL == (vide *) 0

La constante du pointeur nul doit évaluer 0, sinon des expressions comme !ptr ne fonctionneraient pas comme prévu.

La macro NULL se développe en une expression de valeur 0; Autant que je sache, il a toujours