Pourquoi printf (“% f”, 0); donner un comportement indéfini?

La déclaration

printf("%f\n",0.0f); 

imprime 0.

Cependant, la déclaration

 printf("%f\n",0); 

imprime des valeurs aléatoires.

Je me rends compte que j’expose une sorte de comportement indéfini, mais je ne peux pas comprendre pourquoi spécifiquement.

Une valeur à virgule flottante dans laquelle tous les bits sont 0 est toujours un float valide avec la valeur 0.
float et int ont la même taille sur ma machine (si cela est même pertinent).

Pourquoi l’utilisation d’un littéral entier au lieu d’un littéral à virgule flottante dans printf provoque ce comportement?

PS le même comportement peut être vu si j’utilise

 int i = 0; printf("%f\n", i); 

Le format "%f" nécessite un argument de type double . Vous lui donnez un argument de type int . C’est pourquoi le comportement n’est pas défini.

Le standard ne garantit pas que tous les bits zéro est une représentation valide de 0.0 (bien que ce soit souvent), ou de toute valeur double , ou que int et double aient la même taille (rappelez-vous que c’est double , pas float ), ou même si elles ont la même taille, elles sont transmises comme arguments à une fonction variadique de la même manière.

Cela pourrait arriver à “fonctionner” sur votre système. C’est le pire symptôme possible d’un comportement indéfini, car il est difficile de diagnostiquer l’erreur.

N1570 7.21.6.1 paragraphe 9:

… Si un argument n’est pas le type correct pour la spécification de conversion correspondante, le comportement n’est pas défini.

Les arguments de type float sont promus en double , ce qui explique pourquoi printf("%f\n",0.0f) fonctionne. Les arguments de types entiers plus étroits que int sont promus dans int ou unsigned int . Ces règles de promotion (spécifiées par N1570 6.5.2.2 paragraphe 6) ne sont d’aucune aide dans le cas de printf("%f\n", 0) .

Tout d’abord, comme mentionné dans plusieurs autres réponses, mais pas clairement, à mon avis: Cela fonctionne pour fournir un entier dans la plupart des contextes où une fonction de bibliothèque prend un argument double ou float . Le compilateur insérera automatiquement une conversion. Par exemple, sqrt(0) est bien défini et se comporte exactement comme sqrt((double)0) , et il en va de même pour toute autre expression de type entier utilisée ici.

printf est différent. C’est différent car il faut un nombre variable d’arguments. Son prototype de fonction est

 extern int printf(const char *fmt, ...); 

Par conséquent, quand vous écrivez

 printf(message, 0); 

le compilateur n’a aucune information sur le type printf . Il n’a que le type de l’expression d’argument, qui est int , pour passer. Par conséquent, contrairement à la plupart des fonctions de bibliothèque, vous devez vous assurer que la liste des arguments correspond aux attentes de la chaîne de format.

(Les compilateurs modernes peuvent chercher dans une chaîne de format et vous indiquer une incompatibilité de type, mais ils ne vont pas commencer à insérer des conversions pour accomplir ce que vous vouliez dire, car mieux vaut que votre code se casse maintenant , que des années plus tard, lors de la reconstruction avec un compilateur moins utile.)

Maintenant, l’autre moitié de la question était: Étant donné que (int) 0 et (float) 0.0 sont, sur la plupart des systèmes modernes, tous deux représentés comme 32 bits, tous sont nuls, pourquoi cela ne fonctionne-t-il pas par accident? La norme C dit simplement que «ce n’est pas nécessaire pour travailler, vous êtes tout seul», mais laissez-moi préciser les deux raisons les plus courantes pour lesquelles cela ne fonctionnerait pas; cela vous aidera probablement à comprendre pourquoi ce n’est pas nécessaire.

Tout d’abord, pour des raisons historiques, lorsque vous passez un float travers une liste d’arguments de variable, il est promu en double , ce qui, sur la plupart des systèmes modernes, a une largeur de 64 bits. Ainsi, printf("%f", 0) ne transmet que 32 bits zéro à un destinataire qui en attend 64.

La deuxième raison tout aussi importante est que les arguments de fonction à virgule flottante peuvent être passés à un endroit différent des arguments entiers. Par exemple, la plupart des processeurs ont des fichiers de registre séparés pour les entiers et les valeurs à virgule flottante, il pourrait donc être une règle que les arguments 0 à 4 soient dans les registres r0 à r4 s’ils sont des entiers, mais f0 à f4 s’ils sont en virgule flottante. Donc, printf("%f", 0) recherche ce zéro dans le registre f1, mais il n’y est pas du tout.

Habituellement, lorsque vous appelez une fonction qui attend un double , mais que vous fournissez un int , le compilateur se convertira automatiquement en double pour vous. Cela ne se produit pas avec printf , car les types d’arguments ne sont pas spécifiés dans la fonction prototype – le compilateur ne sait pas qu’une conversion doit être appliquée.

Pourquoi l’utilisation d’un littéral entier au lieu d’un littéral flottant provoque ce comportement?

Parce que printf() n’a pas de parameters typés en dehors de la const char* formatssortingng de const char* formatssortingng . Il utilise des points de suspension de style c ( ... ) pour tout le rest.

Il s’agit simplement de décider comment interpréter les valeurs transmises en fonction des types de formatage donnés dans la chaîne de format.

Vous auriez le même type de comportement indéfini que lorsque vous essayez

  int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB 

L’utilisation d’un printf() mal apparié "%f" et du type (int) 0 conduit à un comportement indéfini.

Si une spécification de conversion n’est pas valide, le comportement est indéfini. C11dr §7.21.6.1 9

Causes candidates de UB.

  1. C’est UB par spec et la compilation est ornementale – a dit Nuf.

  2. double et int sont de tailles différentes.

  3. double et int peuvent transmettre leurs valeurs en utilisant différentes stacks (stack générale vs FPU ).

  4. Un double 0.0 peut ne pas être défini par un modèle de bit zéro. (rare)

C’est l’une de ces formidables opportunités d’apprendre de vos avertissements de compilateur.

 $ gcc -Wall -Wextra -pedantic fnord.c fnord.c: In function 'main': fnord.c:8:2: warning: format '%f' expects argument of type 'double', but argument 2 has type 'int' [-Wformat=] printf("%f\n",0); ^ 

ou

 $ clang -Weverything -pedantic fnord.c fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat] printf("%f\n",0); ~~ ^ %d 1 warning generated. 

Ainsi, printf produit un comportement indéfini car vous lui transmettez un type d’argument incompatible.

Je ne suis pas sûr de ce qui est déroutant.

Votre chaîne de format attend un double ; vous fournissez à la place un int .

Que les deux types aient la même largeur de bits est totalement inutile, sauf que cela peut vous aider à éviter des exceptions de violation de mémoire de code comme celles-ci.

"%f\n" garantit un résultat prévisible uniquement lorsque le second paramètre printf() a un type de double . Ensuite, un argument supplémentaire des fonctions variadiques est sujet à la promotion des arguments par défaut. Les arguments entiers relèvent de la promotion entière, ce qui n’entraîne jamais de valeurs typées à virgule flottante. Et les parameters float sont promus à double .

Pour couronner le tout: standard permet au second argument d’être ou de float ou de double et rien d’autre.

Pourquoi il est formellement UB a maintenant été discuté dans plusieurs réponses.

La raison pour laquelle vous obtenez spécifiquement ce comportement dépend de la plate-forme, mais est probablement la suivante:

  • printf attend ses arguments selon la propagation vararg standard. Cela signifie qu’un float sera un double et que tout élément plus petit qu’un int sera un int .
  • Vous passez un int où la fonction attend un double . Votre int est probablement 32 bits, votre double 64 bits. Cela signifie que les quatre octets de stack commençant à l’endroit où l’argument est supposé se trouver sont à 0 , mais les quatre octets suivants ont un contenu arbitraire. C’est ce qui est utilisé pour construire la valeur qui est affichée.

La cause principale de ce problème de «valeur indéterminée» réside dans la dissortingbution du pointeur sur la valeur int transmise à la section des parameters de la variable printf à un pointeur sur double types double va_arg macro va_arg .

Cela provoque une référence à une zone de mémoire qui n’a pas été complètement initialisée avec la valeur passée en paramètre à printf, car double zone de mémoire tampon de taille double est supérieure à la taille int .

Par conséquent, lorsque ce pointeur est déréférencé, une valeur indéterminée lui est renvoyée, ou mieux une “valeur” contenant en partie la valeur passée en paramètre à printf , et la partie restante peut provenir d’une autre zone tampon ou même d’une zone de code. (augmentation d’une exception de défaut de mémoire), un véritable dépassement de tampon .

Il peut considérer ces parties spécifiques des implémentations de code représentées par “printf” et “va_arg” …

printf

 va_list arg; .... case('%f') va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf.. .... 

la véritable implémentation dans vprintf (en considérant les implémentations de gnu) de la gestion des cas de code de parameters à double valeur est la suivante:

 if (__ldbl_is_dbl) { args_value[cnt].pa_double = va_arg (ap_save, double); ... } 

va_arg

 char *p = (double *) &arg + sizeof arg; //printf parameters area pointer double i2 = *((double *)p); //casting to double because va_arg(arg, double) p += sizeof (double); 

les références

  1. Implémentation du projet gnu glibc de “printf” (vprintf))
  2. exemple de code de semplification de printf
  3. exemple de code de semplification de va_arg