Préprocesseur C, option -E incomplète ?

Cette option ne permet manifestement pas de visualiser toutes les opérations réalisées par le préprocesseur

Le problème exposé dans ce sujet a été résolu.

Bonjour,

On m’a dit un jour que le sizeof(int) était traité par le préprocesseur. Je fais donc le code suivant pour vérifier, par curiosité :

#include <stdio.h>

#define TAILLE 100

int main()
{
	int a = TAILLE;
	int b = sizeof(int);

	return 0;
}

et je le compile avec la commande : gcc *.c -E -P -o fichier et quand j’ouvre fichier, je vois ceci : int a = 100; int b = sizeof(int);

donc le sizeof(int) n’a pas été traité par le préprocesseur ? Vérifions ça avec un moyen simple. Essayons de compiler le code suivant avec la norme ansi (dans cette version du C, on ne peut pas déclarer de tableau de longueur variable !). Si le code se compile, c’est que le sizeof(int) est bel et bien traité par le préprocesseur. Sinon, ce n’est pas le cas.

int main()
{
	/*
	int len = 5;
	int tab1[len];
	*/

	int tab2[sizeof(int)];

	return 0;
}

compilation avec la commande : gcc *.c -ansi -pedantic -o exe

Et une preuve supplémentaire que la norme ansi est bien respectée ici : si vous décommentez la partie commentée, vous aurez bien un warning comme quoi on ne peut pas déclarer de tableau de longueur variable dans cette version du C.

Conclusion : le sizeof(int) est bien forcément traité par le préprocesseur mais manifestement l’option -E de gcc présentée dans le cours de C ne permet pas de "réellement" voir tout le résultat du préprocesseur… Auriez-vous une idée pour tout voir ?

+0 -0

Conclusion : le sizeof(int) est bien forcément traité par le préprocesseur mais manifestement l’option -E de gcc présentée dans le cours de C ne permet pas de "réellement" voir tout le résultat du préprocesseur… Auriez-vous une idée pour tout voir ?

Tu fais l’erreur de penser que ton exemple ne peut fonctionner qu’avec une constante définie par le préprocesseur. C’est faux.

Regarde le code suivant pour t’en convaincre :

int main()
{
#if sizeof(int) == 4
	printf("J'ai raison\n");
#else
	printf("J'ai tort\n");
#endif

	return 0;
}

Il ne compile pas, parce que l’opérateur sizeof() ne vient pas du préprocesseur. C’est un opérateur géré par le compilateur lui même, et comme ce qu’il retourne est constant à la compilation, il peut parfaitement être utilisé pour définir la taille d’un tableau de longueur fixe.

+4 -0

?? Ce n’est pas traité par le préproc! On t’a menti (probablement pas volontairement)

Le préproc mange du texte et recrache du texte (appelé unité de compilation/traduction), et c’est cette UT qui est compilée. Il est facile de voir le résultat du préproc avec -E.

Quant à tes tests, c’est juste que sizeof renvoie une expression constante (ou équivalent — ouais le C n’a pas poussé la distingo aussi loin je pense, mais c’est la même idée) qui peut être employée pour définir des tableaux non VLA du C89.

De la même façon que 2 * 2 + 1 sera une expression constante évaluée par le compilo et pas par le préprocesseur, il en va de même pour sizeof

(Et moi j’avais espoir en const, mais je vois que le C ne transforme pas const int l = 42; en expression constante …)

PS: De plus la taille des int dépend de chaque système. Le préprocesseur ne peut pas avoir accès à ce genre d’infos. Et encore moins être capable de comprendre qu’elle sera la vraie taille de struct { int; double; };

PPS: pourquoi tu veux savoir ça?

Le C a une catégorie d’expressions particulière pour les valeurs connues à la compilation.

C’est le cas de sizeof(int) mais également celui de 5 + 4. Bref, il y a une différence entre expression constante (on peut calculer la valeur à la compilation) et valeur constante (elle n’a pas le droit d’être modifiée, on devrait dire immuable et ce n’est pas tout à fait la même chose). Le plus proche d’une constante en langage C est une static const.

Un exemple pour t’en convaincre :

#include <stdio.h>
#include <stdint.h>

int main() {
    const int b = 10;
    int* a = (int*)&b;
    *a = 5;
    printf("%d\n", b); // Affiche 5 ! Mais en vérité, c'est certainement un comportement indéterminé.
}
+0 -0

@ache : je comprends pas ce que tu cherches à montrer avec ton exemple. C’est un UB, et donc un programme invalide (d’ailleurs sur ma machine, compiler avec GCC en O0 donne 5, en O1 donne 10, et compiler avec clang donne toujours 10, je suis même étonné que ça sorte pas une protection error). Ça ne donne pas de raison convaincante pour laquelle une const initialisée avec une expression constante ne pourrait pas être considérée comme une expression constante également. C’est tout de même un choix plutôt bizarre.

+0 -0

La différence, c’est que même constante, b existe à l’exécution, contrairement à une expression constante.

Je sais que c’est un comportement indéterminé, je l’ai explicitement dit.

On est d’accord qu’il y a un problème de conception. Comme je l’ai dit, ça aurait dû être appelé immutable et pas const. (Je sais qu’on dit immuable en français, c’est en anglais dans l’exemple).

a ne donne pas de raison convaincante pour laquelle une const initialisée avec une expression constante ne pourrait pas être considérée comme une expression constante également.

Comme je l’ai dit, elle existe à la compilation et le const n’impose pas de ne pas être modifiée mais seulement de ne pas être modifiable. Tu ne connais pas forcément sa valeur comme dans :

#include <stdio.h>
#include <stdint.h>

const extern int b;

int main() {
    int tab[b]; // Ici ? Le tableau doit faire quelle taille ?
    // On est en ansi (C89), donc pas de VLA.
}

Ou encore, dans des environnements non normaux comme les microcontrôleurs où une variable peut-être mappée à un registre en lecture seule (donc const) mais le registre peut changer de valeur lors de l’exécution du programme. Ça serait typiquement le cas par exemple d’un registre qui indique si on a eu un débordement de valeur (overflow).

Ça te convainc plus ? L’intention n’était pas de créer quelque chose qui fonctionnellement serait identique à un define mais d’avoir quelque chose pour indiquer qu’on s’interdit des opérations.

PS: Si tu actives les optimisations, le compilateur va partir du principe qu’un UB ne peut pas arriver et va modifier le code en conséquence. Bon, ça tu le sais très bien je pense. Je comptais sur le fait que l’assembleur généré n’était pas modifié.

+0 -0

@ache Merci pour tes précisions. Si jamais tu te souviens où tu as appris ce que tu mentionnes sur les expressions constantes (dont les valeurs sont calculées à la compilation) ou si tu as des tutoriels plus détaillés que ceux donnés sur ce site je suis preneur. Aurais-tu en têtes d’autres expressions dont la valeur est extraite directement à la compilation ?

@lmghs Je m’intéresse à tout ça par curiosité. Je veux comprendre ce langage de programmation à la fois pour mes études, mon métier et d’un point de vue personnel ;)

Si tu veux allez plus loin, il n’y pas trop le choix, il faut lire la norme du langage C. Ma référence pour le point en question est par exemple, le chapitre 6.6 de la norme C99.

PS: La norme n’est pas disponible gratuitement, généralement, on se base sur un draf très proche de cette dernière.

+1 -0

PS: La norme n’est pas disponible gratuitement, généralement, on se base sur un draft très proche de cette dernière.

J’en profite pour préciser qu’il n’y a normalement aucune différence technique entre le final draft et le standard en tant que tel. S’il y a des points techniques à modifier, ce n’est pas la version finale. Les seules différences entre les deux sont normalement uniquement des différences éditoriales (typographie, orthographe, par exemple).

Ça te convainc plus ?

Ben non, les deux cas que tu mentionnes étant distincts d’une const initialisée par une expression constante. const pourrait parfaitement agir de façon neutre vis-à-vis des expressions constantes connues au compile-time. C’est vrai en C++ si je ne m’abuse (jusqu’à un certain point du moins, d’où l’introduction de constexpr et maintenant constinit). Bon de toute façon, le standard C va certainement pas bouger dans ce sens…

PS: Si tu actives les optimisations, le compilateur va partir du principe qu’un UB ne peut pas arriver et va modifier le code en conséquence. Bon, ça tu le sais très bien je pense. Je comptais sur le fait que l’assembleur généré n’était pas modifié.

Comme il y a UB, tu n’as aucune garantie sur l’assembleur généré (ni surtout le comportement du programme) avec ou sans optimisations, et ce même sur le reste du programme qui conduit à l’UB. L’exemple que tu as donné n’a pas de sens, et le résultat de la compilation ne fait donc qu’exposer des détails d’implémentations du compilateur utilisé sans montrer quoique ce soit sur le langage C. On pourrait même espèrer qu’on bon compilo rejette complètement le programme lorsqu’on peut prouver qu’un UB peut se produire. C’est un problème fondamental des UB par rapport aux implementation defined (sur lesquel on n’a pas de comportement fixe, mais on sait que le programme reste valide), et il est important de le garder en tête.

Ça te convainc plus ?

🙄. Je m’y attendais je dois dire.

Ben non, les deux cas que tu mentionnes étant distincts d’une const initialisée par une expression constante. const pourrait parfaitement agir de façon neutre vis-à-vis des expressions constantes connues au compile-time. C’est vrai en C++ si je ne m’abuse (jusqu’à un certain point du moins, d’où l’introduction de constexpr et maintenant constinit). Bon de toute façon, le standard C va certainement pas bouger dans ce sens…

Ce que tu proposes c’est d’avoir 2 comportements distincts ? Littéralement, quand une variable non modifiable est définie avec une expression constante alors elle devrait être équivalent à une constante de préprocesseur typée ?

Alors pourquoi ne pas utiliser une constante de préprocesseur directement ? C’est la méthode utilisée généralement en C.

C’est malheureux, mais const ne sert pas à créer des constantes. Non le standard ne va pas changer ça effectivement. On peut se plaindre de comment c’est fait mais au moins, le comportement implémenter est cohérent.

PS: Si tu actives les optimisations, le compilateur va partir du principe qu’un UB ne peut pas arriver et va modifier le code en conséquence. Bon, ça tu le sais très bien je pense. Je comptais sur le fait que l’assembleur généré n’était pas modifié.

Comme il y a UB, tu n’as aucune garantie sur l’assembleur généré (ni surtout le comportement du programme) avec ou sans optimisations, et ce même sur le reste du programme qui conduit à l’UB. L’exemple que tu as donné n’a pas de sens, et le résultat de la compilation ne fait donc qu’exposer des détails d’implémentations du compilateur utilisé sans montrer quoique ce soit sur le langage C. On pourrait même espèrer qu’on bon compilo rejette complètement le programme lorsqu’on peut prouver qu’un UB peut se produire. C’est un problème fondamental des UB par rapport aux implementation defined (sur lesquel on n’a pas de comportement fixe, mais on sait que le programme reste valide), et il est important de le garder en tête.

adri1

Je sais tout ça. Et je suis sûr que tu sais que je le sais. Je me suis rendu compte que les gens sont plus intéressés par ce qui se passe vraiment que par des détails précis sur les comportements indéterminés. C’est pour cela que j’expose les deux.

+0 -0

C’est vrai en C++ si je ne m’abuse (jusqu’à un certain point du moins, d’où l’introduction de constexpr et maintenant constinit). Bon de toute façon, le standard C va certainement pas bouger dans ce sens…

Le C++ reconnaît si la "variable" const est effectivement définie à partir d’une expression constante, et upgrade alors (plus ou moins?) cette "variable" en vraie constante de compilation utilisable dans des expressions constantes comme si elle avait été déclarée constexpr, dans le cas contraire ce n’est qu’une variable immuable. Avec constexpr on exige que l’expression soit constante.

Je ne pense pas non plus que le C aille changer la sémantique de const, même si les compilos sauraient parfaitement le traiter comme en C++, vu que ce sont les mêmes.

Alors pourquoi ne pas utiliser une constante de préprocesseur directement ? C’est la méthode utilisée généralement en C.

Parce que les erreurs causées "par" le préprocesseur sont abominables? OK, les dernières générations de compilateurs vont enfin nous dire que le problème est dans l’expression qui sert dans la macro. Accessoirement, le typage doit se faire avec des suffixes, ou il va exiger potentiellement des connaissances sur les promotions de types. C’est peut-être moins critique en C où l’on ne va pas se rajouter pléthore de nouveaux types que l’on emploiera comme constantes…

+1 à "il ne sert à rien de raisonner sur du code UB" … à part pour démontrer qu’il ne faut pas avoir d’UB dans nos codes.

C’est vrai en C++ si je ne m’abuse (jusqu’à un certain point du moins, d’où l’introduction de constexpr et maintenant constinit). Bon de toute façon, le standard C va certainement pas bouger dans ce sens…

Le C++ reconnaît si la "variable" const est effectivement définie à partir d’une expression constante, et upgrade alors (plus ou moins?) cette "variable" en vraie constante de compilation utilisable dans des expressions constantes comme si elle avait été déclarée constexpr, dans le cas contraire ce n’est qu’une variable immuable. Avec constexpr on exige que l’expression soit constante.

Je ne pense pas non plus que le C aille changer la sémantique de const, même si les compilos sauraient parfaitement le traiter comme en C++, vu que ce sont les mêmes.

lmghs

Pour cela il faudrait que le “type” constexpr existe, or ce n’est pas le cas. Et non, les compilos C et C++ ne sont pas les même. O_o

Parce que les erreurs causées "par" le préprocesseur sont abominables?

Mais en C on a que ça ! Il faut tout changer alors. Et puis surtout, on a connu plus dangereux que des defines. Pour les macros, je veux bien. Mais les defines …

Je veux bien que tu argumentes plus sur ces erreurs.

+1 à "il ne sert à rien de raisonner sur du code UB" … à part pour démontrer qu’il ne faut pas avoir d’UB dans nos codes.

Ouais … N’empèche que ça intéresse les gens. Et que :

int main() {
    volatile const int b = 10;
    volatile int* a = (int*)&b;
    *a = 5;
    printf("%d\n", b);
}

Et bien ça donnera 5 et puis c’est tout.

+0 -0

Et non, les compilos C et C++ ne sont pas les même. O_o

$ ls -al clang++
lrwxrwxrwx 1 luc luc 5 févr. 22 04:29 clang++ -> clang*

Même binaire, juste un mode par défaut qui change des petites règles par ci par là. Ils sont "suffisants" les mêmes pour qu’ils sachent exploiter const différemment le jour hypothétique où la norme C déciderait de changer. AMA. Mais je peux me tromper.


Parce que les erreurs causées "par" le préprocesseur sont abominables?

Mais en C on a que ça ! Il faut tout changer alors. Et puis surtout, on a connu plus dangereux que des defines. Pour les macros, je veux bien. Mais les defines …

En C, vous n’avez que ça je vois en effet. J’avais raté les implications de la sémantique particulière de const en C, et croyais que cela donnerait les mêmes garanties qu’en C++.

Je veux bien que tu argumentes plus sur ces erreurs.

https://gcc.godbolt.org/z/Yfcv6hM5M Et encore, c’est une version simple où l’erreur n’est pas enterrées sous 12 couches d’intermédiaires.

#if 0
#define TOTO 42
#else
#define TOTO "toto"
#endif

int g(int);

int f(int i) {
    return g(TOTO);
}

Bref, ça compile avec juste un

<source>: In function 'f':
<source>:4:14: warning: passing argument 1 of 'g' makes integer from pointer without a cast [-Wint-conversion]
    4 | #define TOTO "toto"
      |              ^~~~~~
      |              |
      |              char *
<source>:10:14: note: in expansion of macro 'TOTO'
   10 |     return g(TOTO);
      |              ^~~~
<source>:7:7: note: expected 'int' but argument is of type 'char *'
    7 | int g(int);
      |       ^~~

On est chanceux aujourd’hui, gcc & clang savent suivre les macros. Mais avec un gcc 4.8.5 typique des RedHat 7 que l’on se coltine toujours, c’est

<source>: In function 'f':
<source>:10:5: warning: passing argument 1 of 'g' makes integer from pointer without a cast [enabled by default]
     return g(TOTO);
     ^
<source>:7:5: note: expected 'int' but argument is of type 'char *'
 int g(int);
     ^

La même en C++ avec du constexpr auto, cela devient une erreur:

<source>: In function 'int f(int)':
<source>:10:14: error: invalid conversion from 'const char*' to 'int' [-fpermissive]
   10 |     return g(TOTO);
      |              ^~~~
      |              |
      |              const char*
<source>:7:7: note:   initializing argument 1 of 'int g(int)'
    7 | int g(int);
      |       ^~~

Cela devient encore plus sympa quand des macros sont utilisées sans CRIER

#define direction 1
......

if (toto.direction == 42)

Qui irait faire ça, hein?

if (toto.min > seuil)

ce n’est pas comme si un célèbre compilateur venait avec une lib qui définissait min() comme une macro…


Ouais … N’empèche que ça intéresse les gens. Et que […] Et bien ça donnera 5 et puis c’est tout.

Jusqu’au jour où les compilos décideront de traiter l’UB différemment… (je vous fais confiance si vous dites que cette utilisation particulière de const + volatile est UB en C). Autant il y a des endroits où cela ne devrait pas être très grave (genre supposer que x+1 > x est toujours vrai (avec des signés)), autant il y en a où je ne me risquerai pas.

Et non, les compilos C et C++ ne sont pas les même. O_o

$ ls -al clang++
lrwxrwxrwx 1 luc luc 5 févr. 22 04:29 clang++ -> clang*

Même binaire, juste un mode par défaut qui change des petites règles par ci par là. Ils sont "suffisants" les mêmes pour qu’ils sachent exploiter const différemment le jour hypothétique où la norme C déciderait de changer. AMA. Mais je peux me tromper.

Intéressant ! Après, c’est juste clang qui fait ça. gcc ne me semble par fait pareil et tcc ne compile même pas de C++.

Je veux bien que tu argumentes plus sur ces erreurs.

Clairement, le coup de min est une hérésie. Malheureusement, les macro / define n’ont pas de namespace.

Jusqu’au jour où les compilos décideront de traiter l’UB différemment…

Tout à fait vrai. Il est claire que baser son code sur un UB (et même une implementation defined) est un problème majeur, je ne le nie pas.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte