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

3 4 5 6 7
Avatar
gl
gl a écrit :
Marc Espie a écrit :
Vincent a souleve un probleme subtil: des pointeurs nuls de type
distincts
n'ont pas forcement la meme representation en memoire. Si tu es dans un
contexte ou il n'y a pas de conversion implicite (ce qui est le cas sur
une ellipse), tu peux avoir un probleme.



Je comprend bien le problème évoqué ici.
Mais je ne vois rien qui dans ce cas permet à une implémentation de
pouvoir considérer que (void*)0 n'est pas un pointeur nul et que la
comparaison entre(void*)0 et un quelconque pointeur pour déterminer sa
nullité ne soit pas possible.



Quoique effectivement, une interprétation stricte de "If a null
>pointer constant is *converted to* a pointer type" peut effectivement
"permettre" ce problème.
Au temps pour moi.
Avatar
Vincent Lefevre
Dans l'article <g5oi6o$2nvs$,
Marc Espie écrit:

In article <20080717220053$,
Vincent Lefevre <vincent+ wrote:
>Dans l'article <g5oed0$2n3a$,
> Marc Espie écrit:
>
>> Euh, les assert, c'est au runtime (en C). Donc beaucoup trop tard en prod.
>
>Pas trop tard: tout d'abord, c'est reproductible, car ça se met
>typiquement au début du programme, et ça se détecte lors du beta test
>de la toute première version.



Bzzzt! Ca suppose que tu aies des tests de couverture *complets*, deja,
et qui tombent sur ces bugs-la. C'est quand la derniere fois que tu as
vu ce genre de choses ?



Je parle juste d'assert pour vérifier la taille des types en début
de programme. Pas plus. Rien à voir avec des tests de couverture.

--
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
Vincent Lefevre
Dans l'article <487fcc94$0$914$,
gl écrit:

Mais l'utilisation d'un pointeur (void*)0 en guide de sentinelle de fin
(le cas expliciter ci-dessus) ne devrait pas poser de problème (enfin
disons plutôt que je n'en vois pas pour l'instant) même si le pointeur
attendu n'est pas char* ou void* puisqu'il sera justement utiliser dans
une comparaison de type != NUMLL pour arrêter le traitement.



Le problème est que si tu stockes un (void *) 0 et que tu le relis en
T *, rien n'est garanti par la norme: tu n'es pas certain d'obtenir
un pointeur nul, ni même une valeur valide de pointeur vers T (e.g.
ça peut planter à ce moment). Si ton implémentation te dit que la
représentation en mémoire de (void *) 0 et (T *) 0 est la même, alors
tout se passe bien. Sinon ton programme risque d'avoir des problèmes.

--
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
gl
Vincent Lefevre a écrit :
Le problème est que si tu stockes un (void *) 0 et que tu le relis en
T *, rien n'est garanti par la norme: tu n'es pas certain d'obtenir
un pointeur nul, ni même une valeur valide de pointeur vers T (e.g.
ça peut planter à ce moment). Si ton implémentation te dit que la
représentation en mémoire de (void *) 0 et (T *) 0 est la même, alors
tout se passe bien. Sinon ton programme risque d'avoir des problèmes.




Oui, tout a fait.

Comme je l'indiquais hier dans un autre message, j'avais une
interprétation beaucoup trop rapide et laxiste de "If a null pointer
constant is converted to a pointer type, the resulting pointer, called a
null pointer, is guaranteed to compare unequal to a pointer to any
object or function." en n'accordant pas tout son importance à "converted
to", d'où ma confusion. En prenant en compte cette obligation, il est
clair qu'il existe dans ce cas de figure un problème potentielle.
Avatar
espie
In article <487fd0dc$0$919$, gl wrote:
Mais je ne vois rien qui dans ce cas permet à une implémentation de
pouvoir considérer que (void*)0 n'est pas un pointeur nul et que la
comparaison entre(void*)0 et un quelconque pointeur pour déterminer sa
nullité ne soit pas possible.



Tu ne recuperes pas un pointer nul, tu as un parametre d'un certain type A
et d'une certaine representation passe a une fonction variadique.
Celle-ci va recuperer ce parametre a coup de va_arg, et donc l'interpreter
comme etant d'un certain type B.

-> tu te reposes donc sur le fait que la representation d'un pointeur nul
de type A est la meme que la representation d'un pointeur nul de type B.
Avatar
espie
In article <20080718002833$,
Vincent Lefevre <vincent+ wrote:
Dans l'article <g5oi6o$2nvs$,
Marc Espie écrit:

In article <20080717220053$,
Vincent Lefevre <vincent+ wrote:
>Dans l'article <g5oed0$2n3a$,
> Marc Espie écrit:
>
>> Euh, les assert, c'est au runtime (en C). Donc beaucoup trop tard en prod.
>
>Pas trop tard: tout d'abord, c'est reproductible, car ça se met
>typiquement au début du programme, et ça se détecte lors du beta test
>de la toute première version.



Bzzzt! Ca suppose que tu aies des tests de couverture *complets*, deja,
et qui tombent sur ces bugs-la. C'est quand la derniere fois que tu as
vu ce genre de choses ?



Je parle juste d'assert pour vérifier la taille des types en début
de programme. Pas plus. Rien à voir avec des tests de couverture.



ce qui ne resoud absolument rien.

Ca revient a dire que ton programme est portable en dehors des implementations
bizarres de C99, ce qui n'est pas forcement ce que tu veux, d'une part.

Et aussi, il faut encore avoir conscience du probleme: tu n'as pas vraiment
de facon simple de detecter qu'il y a quelque part dans ton code une ligne
qui convertit un size_t en un type plus petit et qui peut faire un overflow.
Avatar
Vincent Lefevre
Dans l'article <g5puje$2t5c$,
Marc Espie écrit:

In article <20080718002833$,
Vincent Lefevre <vincent+ wrote:
>Dans l'article <g5oi6o$2nvs$,
> Marc Espie écrit:
>
>> In article <20080717220053$,
>> Vincent Lefevre <vincent+ wrote:
>> >Dans l'article <g5oed0$2n3a$,
>> > Marc Espie écrit:
>> >
>> >> Euh, les assert, c'est au runtime (en C). Donc beaucoup trop tard en prod.
>> >
>> >Pas trop tard: tout d'abord, c'est reproductible, car ça se met
>> >typiquement au début du programme, et ça se détecte lors du beta test
>> >de la toute première version.
>
>> Bzzzt! Ca suppose que tu aies des tests de couverture *complets*, deja,
>> et qui tombent sur ces bugs-la. C'est quand la derniere fois que tu as
>> vu ce genre de choses ?
>
>Je parle juste d'assert pour vérifier la taille des types en début
>de programme. Pas plus. Rien à voir avec des tests de couverture.



ce qui ne resoud absolument rien.



Si. L'assert échoue et le problème de non portabilité est détecté.

Et aussi, il faut encore avoir conscience du probleme: tu n'as pas vraiment
de facon simple de detecter qu'il y a quelque part dans ton code une ligne
qui convertit un size_t en un type plus petit et qui peut faire un overflow.



C'est un autre problème, et ce n'est pas le but de l'assert mentionné
ci-dessus, qui est d'imposer des limites sur l'implémentation (i.e.
d'empêcher du code de tourner sur une implémentation dont on sait
qu'elle n'est pas supportée et qu'elle posera problème). Si l'overflow
est non volontaire, c'est un bug, et je ne prétends évidemment pas
trouver tous les bugs.

--
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 <20080719125354$,
Vincent Lefevre <vincent+ wrote:
ce qui ne resoud absolument rien.



Si. L'assert échoue et le problème de non portabilité est détecté.



Et s'il s'agit de le corriger ?

Et aussi, il faut encore avoir conscience du probleme: tu n'as pas vraiment
de facon simple de detecter qu'il y a quelque part dans ton code une ligne
qui convertit un size_t en un type plus petit et qui peut faire un overflow.





C'est un autre problème, et ce n'est pas le but de l'assert mentionné
ci-dessus, qui est d'imposer des limites sur l'implémentation (i.e.
d'empêcher du code de tourner sur une implémentation dont on sait
qu'elle n'est pas supportée et qu'elle posera problème). Si l'overflow
est non volontaire, c'est un bug, et je ne prétends évidemment pas
trouver tous les bugs.



Oui, mais ca n'est pas ma problematique ici.
Je pars avec du code conforme, et portable selon C90.

C99 me balance dans les pattes des implementations ou ce code n'est plus
portable.

Je peux donc avoir un joli warning global `ah, ton code est susceptible
d'echouer, alors vas-y, fouille dans les 10000+ lignes pour voir si
effectivement il y a probleme'.

Je n'appelle pas ca une solution...
Avatar
Vincent Lefevre
Dans l'article <g5stb1$6s3$,
Marc Espie écrit:

In article <20080719125354$,
Vincent Lefevre <vincent+ wrote:
>> ce qui ne resoud absolument rien.
>
>Si. L'assert échoue et le problème de non portabilité est détecté.



Et s'il s'agit de le corriger ?



On ne parle pas de corriger, mais de détecter. Je rappelle le contexte
(mon message <20080717213141$ du 17 juillet à 23:36):

| Cependant on ne devrait pas se baser sur les warnings (complètement
| optionnels) pour vérifier des propriétés de ce genre. Un assert
| suffit généralement dans la pratique pour *détecter* des problèmes
| éventuels. Et grâce aux optimisations, cela ne doit pas générer de
| code supplémentaire quand tout fonctionne bien.

Je pars avec du code conforme, et portable selon C90.



C99 me balance dans les pattes des implementations ou ce code n'est
plus portable.



Est-ce que C90 garantissait que "unsigned long" était le plus long
type entier disponible? Par exemple, une implémentation n'avait-elle
pas le droit de définir d'une manière ou d'une autre size_t comme
étant un type plus grand (e.g. en le mappant sur un type interne plus
grand)?

D'autre part, le problème est que de nombreux codes sont
volontairement non conformes, car cela prendrait trop de temps de tout
faire "correctement" pour des implémentations qui risquent de ne plus
exister et sur lesquelles on ne peut pas tester (donc difficile d'être
sûr que le code est correct), e.g. des implémentations avec des bits
de padding ou dont certains types ont une taille qui n'est pas une
puissance de 2, ou encore dont la taille d'un long ne tient pas dans
un int... C'est pour ça qu'il est important de détecter les problèmes
même si on ne les corrige pas.

Je peux donc avoir un joli warning global `ah, ton code est susceptible
d'echouer, alors vas-y, fouille dans les 10000+ lignes pour voir si
effectivement il y a probleme'.



Je n'appelle pas ca une solution...



Le programmeur n'a pas fait son boulot correctement: il a fait du
bidouillage au lieu d'utiliser des types abstraits indiquant clairement
ce à quoi ils sont destinés (e.g. un typedef définissant my_uintmax).
Tant pis pour lui. Alternativement, il peut toujours compiler son code
avec un compilateur C90.

--
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 <20080719165908$,
Vincent Lefevre <vincent+ wrote:
Est-ce que C90 garantissait que "unsigned long" était le plus long
type entier disponible? Par exemple, une implémentation n'avait-elle
pas le droit de définir d'une manière ou d'une autre size_t comme
étant un type plus grand (e.g. en le mappant sur un type interne plus
grand)?



Oui, c'etait garanti conforme en C90. En particulier, size_t n'avait pas
le droitd'etre plus long.

D'autre part, le problème est que de nombreux codes sont
volontairement non conformes, car cela prendrait trop de temps de tout
faire "correctement" pour des implémentations qui risquent de ne plus
exister et sur lesquelles on ne peut pas tester (donc difficile d'être
sûr que le code est correct), e.g. des implémentations avec des bits
de padding ou dont certains types ont une taille qui n'est pas une
puissance de 2, ou encore dont la taille d'un long ne tient pas dans
un int... C'est pour ça qu'il est important de détecter les problèmes
même si on ne les corrige pas.




Mais tu ne detectes rien ici !!! ca suppose que tu sais suffisamment
de choses sur le comportement du code pour dire `attention, sur les 5000
lignes de code presentes, il y en a 5 qui peuvent ne pas marcher sur
cette machine.'


Le programmeur n'a pas fait son boulot correctement: il a fait du
bidouillage au lieu d'utiliser des types abstraits indiquant clairement
ce à quoi ils sont destinés (e.g. un typedef définissant my_uintmax).
Tant pis pour lui. Alternativement, il peut toujours compiler son code
avec un compilateur C90.



Dans la pratique, en C, les types abstraits dont tu me parles m'ont
toujours complique la vie en terme de portage.

Ca va certainement te paraitre choquant, mais dans de l'audit de code,
moins on a d'indirection a la con a travers des types abstraits, moins
on se tape de bugs cretins.

Le C n'est pas fait pour utiliser des types abstraits. Il n'a pas de
notion de type abstrait. Typedef fait de l'equivalence de types.
Pas de garde-fou. Nada. Rien.

J'ai deja passe quelques heures a debugguer du code bati aux dessus de
typedef foireux, je m'en passerais bien. Je prefere largement limiter
le nombre de niveaux d'indirections. Et j'ai deja passe du temps a debugguer
des include foireux qui definissaient du typedef a gogo.

Le langage est pourri de ce point de vue. Les noms de types sont globaux.
Des que tu as deux projets un peu gros que tu veux utiliser simultanement,
les types abstraits en question te foutent la zone.
3 4 5 6 7