Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Conversion des arguments de printf()

69 réponses
Avatar
candide
Ce post reprend Message-ID: <g5ift4$1ckv$1@biggoron.nerim.net>

Marc Espie a écrit :
> In article <487cb490$0$6783$426a34cc@news.free.fr>,
> candide <candide@free.invalid> wrote:
>> Je ne comprends pas ce que ça ajoute par rapport à
>>
>> printf("%u\n",strlen("toto"));
>>
>> puisque size_t est un type entier non signé.
>
> Et quelle taille ? printf est une fonction a nombre variable d'arguments,
> de prototype
> int printf(const char *, ...);
>
> il n'y a donc pas de verif de type passe le format. Si tu es sur une
plateforme
> ou size_t vaut unsigned long, ton printf ne marchera pas: il
recuperera juste
> un unsigned int comme parametre.

Bon j'ai cherché à me documenter sur la question, hélas rien de très
clair. Si je lis la FAQ de clc, il est effectivement explicitement dit
que je dois caster, je le saurai pour la prochaine fois.

Mais que se passe-t-il, étape par étape, quand l'instruction

printf("%u\n",strlen("toto"));


est exécutée ? La valeur de strlen("toto") est convertie dans le type
int parce que printf est variadique, c'est cela ?



_Ensuite_, cette valeur est convertie en unsigned int à cause du
spécificateur %u, c'est ça ?


Maintenant que se passe-t-il, étape par étape, quand l'instruction

printf("%u\n",(unsigned)strlen("toto"));

est exécutée ?

L'expression strlen("toto") est évaluée puis sa valeur est convertie en
unsigned int. Mais ensuite, pourquoi l'argument (unsigned)strlen("toto")
n'est-il pas converti en int puisque c'est un argument d'une fonction
variadique ?


Et puis, je ne vois pas où il est dit dans la norme que les arguments
entiers sont convertis en int. Si j'ai repéré l'endroit adéquat, la
conversion s'appelle "default argument promotion". Pour moi la
promotion, c'est la promotion :

char, short, champs de bit -> int ou unsigned int.




Mais la conversion

size_t (le type le plus large) -> int

c'est plus de la promotion, c'est une dégradation.

10 réponses

1 2 3 4 5
Avatar
candide
Marc Espie a écrit :


(sinon, tu fais comment pour passer des long a des fonctions
variadiques, gros malin ?)


Je suppose que c'est en cherchant le spécificateur %l, non ?



Evidemment, si tu confonds les specificites du fonctionnement de printf
avec le passage de parametres a une fonction, tu ne vas pas t'en servir.





Donc, il y a bien deux phases de conversion complètement distinctes :

1) les conversions de type dues à la promotion des arguments défaillants
2) les conversions vers le format indiqué par les spécificateur d, u, f,
etc.

Alors, peut-être que j'ai mal compris mais je trouve ambigu que la norme
emploi le même terme pour dire des choses différentes dans un contexte
où les deux sens coexistent. Exemple tiré de C99


-------------- 8< ----------------------------------------------------
7.19.6.1p8
The conversion specifiers and their meanings are:
d,i
The int argument is converted to signed decimal in the style
[−]dddd.
-------------- >8 ----------------------------------------------------

le "converted" utilisé ici ne se réfère pas à une conversion de type
("signed decimal" n'est pas un type, c'est juste un mode de représentation)


Principe de base du C: la bibliotheque standard n'est pas magique. Elle
est implementable en C standard (mais non portable) si tu te donnes des
primitives d'entrees-sorties raisonnables. C'est lie a la genese du C, et
ce principe est encore aujourd'hui respecte.

En particulier, le seul mecanisme dont tu disposes pour avoir des fonctions
a nombre variables de parametres, c'est l'ellipse. Pour invoquer printf,
tu invoques juste une fonction f de prototype
int f(const char *, ...);

C'est le *seul* mecanisme dont dispose le langage pour donner une semantique
a cet appel de fonction, il n'y a pas de magie specifique a printf.
[ ca ne veut pas dire qu'une implementation n'a pas le droit de profiter
du caractere magique de printf, juste que la semantique du langage est
definie pour que ca fonctionne meme sans magie ]




OK. je vois parfaitement et très clairement ce que tu me dis là mais je
ne crois pas en saisir la véritable portée (j'entends Pierre Maurette
désespérer de mon cas).


Mais alors à quoi servent les promotions des arguments par défaut
("default argument promotions") ?






Elles servent a rester compatible avec du C sans prototype, ou les choses
fonctionnaient comme cela.



Ah bon, c'est juste pour la compatibilité ?

Ca sert a s'assurer que, meme en l'absence
complete de prototype, on a facilement quelque chose qui marche sur des
appels simples de fonction a base de char, short, ou int.



Et ça c'est encore la compatibilité ?



long et double sortent du lot, et necessitent des precautions particulieres:
c'est un compromis d'efficacite.

Rappel: int est sense representer le type entier *natif* du systeme, a
savoir celui qui represente le meilleur compromis en terme d'efficacite.
C'est normal que, par defaut, les parametres entiers soient promus vers
celui-ci. Sur pas mal d'archis, passer explicitement des trucs plus courts
a une fonction necessite des contorsions couteuses en temps.




OK.



si tu fais

short d = 42;
printf("%h", d);

ton d sera promu en int lors de l'appel de printf.




OK, ça s'est la promotion des arguments. Une précision quand même : que
ce soit converti, parfait, maintenant quelle conséquence *pratique* pour
l'*utilisateur* de printf() ? c'est complètement transparent pour lui, non ?


Finalement, pour une utilisation des bons types pour printf, il suffit
de caster par le bon type attendu par la norme. Ainsi, si mon argument
est de type size_t, mon spécificateur va être %u (car size_t est un type
non signé) et je caste sans me poser de question en unsigned. La norme
dit en fait que les arguments ne peuvent être que parmi un nombre limité
de types (int, unsigned int, double et pointeur vers void et int
peut-être aussi). Il n'y a qu'avec le modificateur h que je n'ai pas
vraiment compris puisque les short sont promus en int et la norme dit
que la valeur entière obtenue va être reconvertie dans le type short !!

D'ailleurs, je ne vois pas pourquoi on lit souvent le cast suivant :

printf("%un", (unsigned long)strlen("toto"));


Le spécificateur u attend un unsigned int. Que va-t-il se passer s'il
reçoit un unsigned long ? La norme dit :


-------------- 8< ----------------------------------------------------
If any argument is not the correct type for the corresponding conversion
specification, the behavior is undefined.
-------------- >8 ----------------------------------------------------

Je parle de tout ça parce que je suis à la recherche d'une règle
pratique d'utilisation de printf.
Avatar
Antoine Leca
En news:487e7ce3$0$6997$, candide va escriure:
Antoine Leca a écrit :

Enfin, le compilateur attribue le type size_t au résultat, et on
peut passer à la suite.



Je note ce point important. Ainsi le type size_t du résultat n'est pas
ignoré, il est mis de côté en attendant la suite.

(...) pour l'expression, on applique les règles de promotions:



l'expression ? de quelle expression tu parles ? strlen("toto") ?



Oui. Plus exactement, le résultat de son évaluation (y compris son type, en
fait tout ce que l'on a mis de côté), tel que pris en compte comme opérande
de l'opérateur (), en fait ici l'opérateur (,,...).

je croyais qu'on n'en parlait plus de celle-là maintenant que son type
a été repéré par le compilateur.



On ne parle plus de l'évaluation de l'expression en question. On parle ici
de l'évaluation de l'opérateur (,,...), appel de fonction variadique, qui
implique des « micro-opérations » sur ses opérandes ; et une de ces
micro-opérations est la promotion des paramètres de type entier.


Mais de toute façon, je ne comprends pas pourquoi tu raisonnes en
terme de rang. La norme C90 (je n'ai pas lu C99) définit la promotion
numérique d'abord en terme de type et accessoirement en terme de rang



Et bien, je t'invite à te plonger dans la partie 6.3.1.1 de C99 (environ une
page), qui reprend et étend ce concept, d'une manière considéree comme plus
claire que dans la norme C90 (sachant qu'il faut consulter plusieurs
correctifs techniques pour bien maîtriser C90 sur ce point).

Si tu restreint cet alinéa aux types de C90, tu retombes sur le texte de
C90, mais de manière plus claire (en particulier dans le cas qui nous occupe
si size_t est un type entier non-standard...)


Je ne vois pas où la norme quand elle parle de promotion numérique
dit qu'un unsigned int reste inchangé.



Elle ne dit à aucun endroit qu'il est modifié, ce qui revient au même.


Par ailleurs, si j'ai bien compris ton exposé, jusqu'à présent le
spécificateur de conversion %u n'a pas été utilisé par le compilateur,
vrai ?



Oui. En fait, le spécificateur n'est utilisé à aucun moment par le
compilateur (sauf pour donner des avertissements dans certaines
implémentations de qualité, mais ce n'est nullement requis).
C'est pour cela que j'avais découpé en deux temps.


Le compilateur passe ensuite à la génération du code d'appel d'une
fonction variadique



>[...]

(...) et le résultat de l'expression est passé conformément à son
type


À quoi tu te réfères :
*) de quelle expression parles-tu ? encore de strlen("toto") ?



oui, enfin le résultat de son évaluation.


après promotion: par exemple, si c'est un unsigned long de taille
double de


Tu parles de ce qui représente size_t ?



Oui

celle d'un int, il va utiliser deux fois plus de place...



Que vient faire le int ici.



C'est un exemple, pour essayer de montrer les différentes possibilités de
génération de code, et donc le fait qu'il y a une incertitude sous-jacente
dans cette formulation (qui est la raison profonde du problème).


Il y a peut-être un problème de vocabulaire : le terme "conversion"
est employé dans deux contextes différents : d'une part, conversion
au sens
de conversion d'une valeur d'un type dans un autre et d'autre part,
les conversions opérées par les spécificateurs de conversion, les
machins %d, %u, %f, etc.



Pour le compilateur (le premier temps), seuls les premières sont
considérées.


(...) Puis va faire l'appel à
printf, avec comme paramètre un pointeur vers {'%', 'u', NL, 0} et
d'autre part le 4 (converti): (...)



"converti" en fonction (cf. ce que tu as dit plus haut) du rang de
size_t ?



Je dirais converti _par_ une fonction _depuis_ le range de size_t _vers_ le
rang correspondant pour un paramètre d'appel de fonction variadique.
Évidemment, dans le cas normal, la « fonction » en question consiste à
empiler la valeur telle quelle, éventuellement avec extension à 0 ; mais la
norme ne prescrit pas une architecture de pile.

l'appel de fonction variadique, va analyser la chaîne, repérer le
%u, aller chercher dans les paramètres passés une valeur de type
unsigned



Je comprends pas : il y a encore une conversion.



Là c'est moi qui ne comprend pas : je n'ai pas parlé de conversion ci-dessus
(du moins je n'en ai pas eu l'impression).


Je comprends pas "aller chercher dans les paramètres passés". Il n'y a
plus de paramètres, il n'y a que des arguments,



Juste, erreur de ma part, il fallait lire « aller chercher dans les
arguments passés »

Je comprends pas ce que ça veut dire aller chercher une valeur de type
unsigned.



Sur une machine à pile, dépiler (cf. la remarque ci-dessus).
Si tu considères une implémentation de printf écrite en C, cela revient à un
appel à va_arg(). Qui nécessite de donner un type, ici ce sera unsigned.


Et est-la première fois que l'on tient compte de %u ?



Oui.

On en tiendra compte une seconde fois un poil plus tard, au moment de faire
la « conversion » sous-entendue dans l'expression "conversion specifier",
qui est la conversion d'un entier en chaîne de caractères. La norme précise
que pour %u, cette conversion doit utiliser la base 10, sans zéro initial
(sauf pour la valeur 0) et sans caractère espace ajouté ni signe +.


Maintenant que se passe-t-il, étape par étape, quand l'instruction

printf("%un",(unsigned)strlen("toto"));

est exécutée ?



Par rapport à ci-dessus, après l'appel à strlen, le résultat subit
une conversion forcée vers unsigned int (ce qui peut provoquer un
appel de fonction ou pas, l'émission de code ou pas, etc.)
Ensuite, au moment de l'évaluation des paramètres dans l'opération
d'appel de fonction variadique, le cas est plus déterministe, il n'y
a pas de promotion.



Et pourquoi ? tu disais tout à l'heure dans le premier exemple que
dans une fonction variadique, les arguments par défaut subissent
la promotion.



Mais ici nous sommes sûrs qu'il n'y a pas d'effet, puisque nous considérons
un unsigned.


promotion. Et plus bas, le paramètre est passé comme unsigned.



"plus bas" ?



Au moment de l'« empilage ».


Dans le deuxième temps, au moment d'interpréter la conversion %u,
print ira chercher un unsigned, et donc trouvera toujours 4.




On va vers le nord ou vers le sud ? ;) on en est à la compilation ou à
l'exécution ?



Deuxième temps = exécution.



Mais pourquoi la norme dit-elle seulement que size_t est un "type
entier non signé" et nous fait-elle des cachoteries ?



La norme est un contrat (entre l'implémenteur et le programmeur).
Les termes du contrat sont opportunément vagues, pour couvrir plusieurs
implémentations possibles et plusieurs (beaucoup de) programmes possibles.
Les « cachoteries » sont en fait les degrés de liberté accordés aux uns et
aux autres, comme dans n'importe quel contrat.

Quand le mécanisme est trop laxiste, ce qui est le souci associé avec les
fonctions variadiques et aussi la raison pour laquelle le C possède une
telle flexibilité, le programmeur est obligé de « sur-spécifier » pour
couvrir les libertés accordées aux implémenteurs.
C'est le cas ici.


Antoine
Avatar
Antoine Leca
En news:487efd1b$0$27507$, candide va escriure:
Marc Espie a écrit :
short d = 42;
printf("%h", d);

ton d sera promu en int lors de l'appel de printf.






OK, ça s'est la promotion des arguments. Une précision quand même :
que ce soit converti, parfait, maintenant quelle conséquence *pratique*
pour l'*utilisateur* de printf() ?



Aucune. En fait, le h dans
printf("%hd", d);
n'a pas d'utilité technique.
Ce qui ne veut pas dire qu'il est inutile : pour le programmeur, cela
représente une documentation : par exemple, on s'attendra à ce que d reste
une valeur petite.

Un autre intérêt est qu'une instruction scanf correspondante devra spécifier
ce h, donc ici on a une tentative de symétrie (attention cependant, cette
symétrie n'est pas totale, c'est d'ailleurs un gros problème pour
l'utilisation de scanf).


Finalement, pour une utilisation des bons types pour printf, il suffit
de caster par le bon type attendu par la norme.



Oui.

Ainsi, si mon argument est de type size_t, mon spécificateur va être %u
(car size_t est un type non signé) et je caste sans me poser de
question en unsigned.



Non.
Si mon argument est une taille d'objet « raisonnable », j'utilise %u pour
imprimer, donc je dois transtyper en (unsigned) si le résultat n'a pas déjà
ce type.
Si mon argument peut être n'importe quelle taille d'objet (cas classique
d'un size_t dont on ne sait rien), j'utilise %zu (avec une implémentation
C99, y compris la bibliothèque) et passer le size-t, ou bien %lu et
transtyper en (unsigned long) si je veux rester conforme C90.


D'ailleurs, je ne vois pas pourquoi on lit souvent le cast suivant :
printf("%un", (unsigned long)strlen("toto"));



Pabô : cast inutile, et désaccord entre spécificateur et argument donc
comportement indéfini.
J'ajouterai, exemple-type des « améliorations » que certains portent aux
programmes, qui en fait introduisent des bogues.


Antoine
Avatar
candide
Antoine Leca a écrit :

Et bien, je t'invite à te plonger dans la partie 6.3.1.1 de C99 (environ une
page), qui reprend et étend ce concept, d'une manière considéree comme plus
claire que dans la norme C90 (sachant qu'il faut consulter plusieurs
correctifs techniques pour bien maîtriser C90 sur ce point).



Ah oui, les TC1, TC2. Mais à vrai dire, je crois pas que je vais me
lancer dans l'étude détaillée de ce que dis C99 trop conceptuel) pas
plus que les TCi, ça devient carrément pour spécialistes là.


Je ne vois pas où la norme quand elle parle de promotion numérique
dit qu'un unsigned int reste inchangé.



Elle ne dit à aucun endroit qu'il est modifié, ce qui revient au même.





OK.


Par ailleurs, si j'ai bien compris ton exposé, jusqu'à présent le
spécificateur de conversion %u n'a pas été utilisé par le compilateur,
vrai ?



Oui. En fait, le spécificateur n'est utilisé à aucun moment par le
compilateur



Ah OK.




Il y a peut-être un problème de vocabulaire : le terme "conversion"
est employé dans deux contextes différents : d'une part, conversion
au sens
de conversion d'une valeur d'un type dans un autre et d'autre part,
les conversions opérées par les spécificateurs de conversion, les
machins %d, %u, %f, etc.



Pour le compilateur (le premier temps), seuls les premières sont
considérées.



OK




l'appel de fonction variadique, va analyser la chaîne, repérer le
%u, aller chercher dans les paramètres passés une valeur de type
unsigned


Je comprends pas : il y a encore une conversion.



Là c'est moi qui ne comprend pas : je n'ai pas parlé de conversion ci-dessus
(du moins je n'en ai pas eu l'impression).



Oui, moi j'en étais resté au size_t et comme tu me parles de unsigned
int je croyais en une nouvelle conversion mais en lisant la suite de ton
message, je comprends que ce type unsigned dont tu parles fait tout
simplement référence au %u.



Sur une machine à pile, dépiler (cf. la remarque ci-dessus).
Si tu considères une implémentation de printf écrite en C, cela revient à un
appel à va_arg(). Qui nécessite de donner un type, ici ce sera unsigned.



OK. A vrai dire, je n'ai jamais eu l'occasion d'écrire une fonction avec
ellipse. Mais je vois ce que tu veux m'expliquer.




Et est-la première fois que l'on tient compte de %u ?



Oui.




OK, ça commence à s'éclaircir.


printf("%un",(unsigned)strlen("toto"));









Mais ici nous sommes sûrs qu'il n'y a pas d'effet, puisque nous considérons
un unsigned.




Donc, pour être pratique et ne pas avoir de problème avec le type des
arguments passés à printf, en cas de non conformité, je caste
systématiquement par le type que la norme attend ?



Les « cachoteries » sont en fait les degrés de liberté accordés aux uns et
aux autres, comme dans n'importe quel contrat.



Oui mais finalement ici, le degré de liberté est quasiment inexistant,
puisque size_t est le type de retour de sizeof et que sizeof d'un char
quel qu'il soit est 1, size_t sera un alias de unsigned char ou char (si
les caractères sont non signés) ou à la rigueur d'une version qualifiée
de ces derniers, non ?
Avatar
candide
Antoine Leca a écrit :
Si mon argument est une taille d'objet « raisonnable », j'utilise %u pour
imprimer, donc je dois transtyper en (unsigned) si le résultat n'a pas déjà
ce type.
Si mon argument peut être n'importe quelle taille d'objet (cas classique
d'un size_t dont on ne sait rien), j'utilise %zu (avec une implémentation
C99, y compris la bibliothèque) et passer le size-t, ou bien %lu et
transtyper en (unsigned long) si je veux rester conforme C90.



OK, voilà une règle pratique et sans ambiguïté, merci.
Avatar
Vincent Lefevre
Dans l'article <g5n3tk$7cn$,
Antoine Leca écrit:

Si mon argument peut être n'importe quelle taille d'objet (cas classique
d'un size_t dont on ne sait rien), j'utilise %zu (avec une implémentation
C99, y compris la bibliothèque) et passer le size-t, ou bien %lu et
transtyper en (unsigned long) si je veux rester conforme C90.



Mais la solution C90 risque de ne pas fonctionner avec certaines
implémentations C99. Je suppose que tu fais cela avec un système
style autoconf, ou alors que tu écris du code pour une ou plusieurs
plateformes spécifiques.

--
Vincent Lefèvre - Web: <http://www.vinc17.org/>
100% accessible validated (X)HTML - Blog: <http://www.vinc17.org/blog/>
Work: CR INRIA - computer arithmetic / Arenaire project (LIP, ENS-Lyon)
Avatar
espie
In article <g5n3tk$7cn$,
Antoine Leca wrote:
En news:487efd1b$0$27507$, candide va escriure:
Marc Espie a écrit :
short d = 42;
printf("%h", d);

ton d sera promu en int lors de l'appel de printf.






OK, ça s'est la promotion des arguments. Une précision quand même :
que ce soit converti, parfait, maintenant quelle conséquence *pratique*
pour l'*utilisateur* de printf() ?



Aucune. En fait, le h dans
printf("%hd", d);
n'a pas d'utilité technique.



Faudrait verifier. Je ne suis pas sur que ton implementation de printf
n'ait pas le droit de supposer que d, cote intervalle de valeurs, est
un short (et donc faire n'importe quoi si tu lui passes une valeur
plus grande que SHRT_MAX).
Avatar
espie
In article <20080717104138$,
Vincent Lefevre <vincent+ wrote:
Dans l'article <g5n3tk$7cn$,
Antoine Leca écrit:

Si mon argument peut être n'importe quelle taille d'objet (cas classique
d'un size_t dont on ne sait rien), j'utilise %zu (avec une implémentation
C99, y compris la bibliothèque) et passer le size-t, ou bien %lu et
transtyper en (unsigned long) si je veux rester conforme C90.



Mais la solution C90 risque de ne pas fonctionner avec certaines
implémentations C99. Je suppose que tu fais cela avec un système
style autoconf, ou alors que tu écris du code pour une ou plusieurs
plateformes spécifiques.



Lesquelles ? As-tu deja rencontre des implementations suffisamment
tarees pour que size_t soit strictement plus grand que unsigned long ?

C'est un changement qui a ete suffisamment decrie pour ne pas etre
suivi en pratique, et je crois qu'il a meme fait l'objet de plusieurs
defect reports...
Avatar
Antoine Leca
En news:487f1669$0$25171$, candide va escriure:
Donc, pour être pratique et ne pas avoir de problème avec le type des
arguments passés à printf, en cas de non conformité, je caste
systématiquement par le type que la norme attend ?




*OUI* !

Et de plus, c'est vrai avec toutes les fonctions « variadiques », pas
seulement pour printf(). Par exemple, pour la fonction execl() de Posix (qui
remplace le programme par un autre avec une liste d'arguments), il faut
écrire

char *nom_programme, *args[3];

execl(nom_programme, args[1], args[2], args[3], (char*)0 );

pour indiquer que le pointeur nul à la fin (qui fait partie de la
spécification) est bien un char* comme attendu.


Les « cachoteries » sont en fait les degrés de liberté accordés aux
uns et aux autres, comme dans n'importe quel contrat.



Oui mais finalement ici, le degré de liberté est quasiment inexistant,



Ici on voit surtout la liberté de l'implémenteur, car l'on s'est approché
des spécificités de l'architecture.
Mais le programmeur a aussi sa part : le concept même de size_t permet
d'avoir le même programme qui s'exécute à la fois sur MSDos (size_t sur
16/32 bits, int sur 16) et sur un OS moderne 64 bits (long sur 64/32 bits
selon que *nix ou Windows, size_td), ce que ne permettait pas le type
unsigned du K&R auquel il a (partiellement) succedé.


Antoine
Avatar
Thierry B.
--{ Antoine Leca a plopé ceci: }--

Mais le programmeur a aussi sa part : le concept même de size_t permet
d'avoir le même programme qui s'exécute à la fois sur MSDos (size_t sur
16/32 bits, int sur 16) et sur un OS moderne 64 bits (long sur 64/32 bits
selon que *nix ou Windows, size_td), ce que ne permettait pas le type
unsigned du K&R auquel il a (partiellement) succedé.



Si je me souviens bien, il y a eu un compilateur C pour MSDOS qui
avait les int en 32 bits; peut-être le Lattice ?

--
In fmbd, no one can hear you scream.
1 2 3 4 5