Espaces insérés par le préprocesseur C

Supposons que nous recevions cette entrée de code C:

#define Y 20 #define A(x) (10+x+Y) A(A(40)) 

gcc -E sorties gcc -E sont comme ça (10+(10+40 +20)+20) .

gcc -E -traditional-cpp sorties gcc -E -traditional-cpp sont comme ça (10+(10+40+20)+20) .

Pourquoi le cpp par défaut insère-t-il l’espace après 40 ?

Où puis-je trouver la spécification la plus détaillée du cpp qui couvre cette logique?

Le standard C ne spécifie pas ce comportement, car la sortie de la phase de pré-traitement est simplement un stream de jetons et d’espaces. La sérialisation du stream de jetons dans une chaîne de caractères, comme le fait gcc -E , n’est pas requirejse, ni même mentionnée par la norme, et ne fait pas partie des processus de traduction spécifiés par la norme.

En phase 3, le programme “est décomposé en jetons de pré-traitement et en séquences de caractères d’espace blanc”. Outre le résultat de l’opérateur de concaténation, qui ignore les espaces, et de l’opérateur de chaîne de caractères, qui préserve les espaces, les jetons sont alors fixes et les espaces ne sont plus nécessaires. Cependant, les espaces sont nécessaires pour:

  • parsingr les directives du préprocesseur
  • traiter correctement l’opérateur de ssortingngification

Les éléments d’espacement dans le stream ne sont pas éliminés avant la phase 7, bien qu’ils ne soient plus pertinents à l’issue de la phase 4.

Gcc est capable de produire diverses informations utiles aux programmeurs, mais ne correspondant à rien dans la norme. Par exemple, la phase de pré-traitement de la traduction peut également produire des informations de dépendance utiles pour l’insertion dans un fichier Makefile, à l’aide de l’une des options -M . Sinon, une version lisible par l’homme du code compilé peut être générée à l’aide de l’option -S . Et une version compilable du programme prétraité, correspondant approximativement au stream de jetons produit par la phase 4, peut être sortie à l’aide de l’option -E . Aucun de ces formats de sortie n’est en aucune manière contrôlé par le standard C, qui ne concerne que l’exécution réelle du programme.

Pour produire la sortie -E , gcc doit sérialiser le stream de jetons et d’espaces dans un format ne modifiant pas la sémantique du programme. Il existe des cas dans lesquels deux jetons consécutifs dans le stream seraient mal collés ensemble en un seul jeton s’ils ne sont pas séparés les uns des autres. Gcc doit donc prendre certaines précautions. Il ne peut pas réellement insérer d’espaces dans le stream en cours de traitement, mais rien ne l’empêche d’append des espaces lorsqu’il présente le stream en réponse à gcc -E .

Par exemple, si l’appel de macro dans votre exemple a été modifié pour

 A(A(0x40E)) 

alors la sortie naïve du stream de jetons aurait pour résultat

 (10+(10+0x40E+20)+20) 

qui n’a pas pu être compilé car 0x40E+20 est un jeton de numéro de pp unique qui ne peut pas être converti en un jeton numérique. L’espace avant le + empêche cela de se produire.

Si vous essayez d’implémenter un préprocesseur en tant que transformation de chaîne, vous vous exposerez sans aucun doute à de graves problèmes. La stratégie d’implémentation correcte consiste à tokenize tout d’abord, comme indiqué dans la norme, puis à exécuter la phase 4 en tant que fonction d’un stream de jetons et d’espaces.

La chaîne de caractères est un cas particulièrement intéressant dans lequel les espaces ont une incidence sur la sémantique et permet de voir à quoi ressemble le stream de jetons. Si vous codifiez le développement de A(A(40)) , vous pouvez voir qu’aucun espace n’a été réellement inséré:

 $ gcc -E -xc - <<<' #define Y 20 #define A(x) (10+x+Y) #define Q_(x) #x #define Q(x) Q_(x) Q(A(A(40)))' "(10+(10+40+20)+20)" 

Le traitement des espaces en chaîne est spécifié dans la norme: (§6.10.3.2, paragraphe 2, merci beaucoup à John Bollinger pour avoir trouvé la spécification.)

Chaque occurrence d'espace blanc entre les jetons de prétraitement de l'argument devient un caractère d'espacement unique dans le littéral de chaîne de caractères. L'espace avant le premier jeton de prétraitement et après le dernier jeton de prétraitement composant l'argument est supprimé.

Voici un exemple plus subtil où des espaces supplémentaires sont requirejs dans la sortie gcc -E , mais ne sont pas réellement insérés dans le stream de jetons (de nouveau montré en utilisant une chaîne de caractères pour produire le stream de jetons réel.) La macro I (identifier) ​​est utilisée pour permettre à deux jetons d'être insérés dans le stream de jetons sans intervention d'espaces; c'est une astuce utile si vous souhaitez utiliser des macros pour composer l'argument de la directive #include (non recommandé, mais cela peut être fait).

Cela pourrait peut-être être un cas de test utile pour votre préprocesseur:

 #define Q_(x) #x #define Q(x) Q_(x) #define I(x) x #define C(x,...) x(__VA_ARGS__) // Uncomment the following line to run the program //#include  char*quoted=Q(C(I(int)I(main),void){I(return)I(C(puts,quoted));}); C(I(int)I(main),void){I(return)I(C(puts,quoted));} 

Voici la sortie de gcc -E (juste les bonnes choses à la fin):

 $ gcc -E squish.c | tail -n2 char*quoted="intmain(void){returnputs(quoted);}"; int main(void){return puts(quoted);} 

Dans le stream de jetons qui est sorti de la phase 4, les jetons int et main ne sont pas séparés par des espaces (et ne return ni ne puts ). Cela est clairement démontré par la chaîne de caractères, dans laquelle aucun espace ne sépare le jeton. Cependant, le programme comstack et exécute très bien, même s'il est passé explicitement via gcc -E :

 $ gcc -E squish.c | gcc -xc - && ./a.out intmain(void){returnputs(quoted);} 

et comstackr la sortie de gcc -E .


Différents compilateurs et différentes versions du même compilateur peuvent produire différentes sérialisations d'un programme prétraité. Donc, je ne pense pas que vous trouverez un algorithme testable avec une comparaison caractère par caractère avec la sortie -E d'un compilateur donné.

L'algorithme de sérialisation le plus simple possible consisterait à sortir inconditionnellement un espace entre deux jetons consécutifs. Évidemment, cela produirait des espaces inutiles, mais cela ne modifierait jamais le programme de manière syntaxique.

Je pense que l'algorithme d'espace minimal consisterait à enregistrer l'état DFA à la fin du dernier caractère d'un jeton, de manière à pouvoir émettre ultérieurement un espace entre deux jetons consécutifs s'il existe une transition entre l'état à la fin du premier jeton. sur le premier caractère du jeton suivant. (Conserver l'état DFA dans le jeton n'est pas insortingnsèquement différent de conserver le type de jeton dans le jeton, car vous pouvez dériver le type de jeton à partir d'une simple recherche à partir de l'état DFA.) Cet algorithme n'insère pas d'espace après 40 dans votre cas de test d'origine, mais il insérerait un espace après 0x40E . Donc, ce n'est pas l'algorithme utilisé par votre version de gcc.

Si vous utilisez l'algorithme ci-dessus, vous devrez parsingr à nouveau les jetons créés par concaténation de jetons. Cependant, cela est néanmoins nécessaire, car vous devez signaler une erreur si le résultat de la concaténation n'est pas un jeton de prétraitement valide.

Si vous ne voulez pas enregistrer les états (bien que, comme je l'ai dit, cela ne coûte pratiquement rien), et que vous ne voulez pas régénérer l'état en réanalysant le jeton au fur et à mesure que vous le sortez (ce qui serait également relativement peu coûteux. ), vous pouvez précalculer un tableau booléen à deux dimensions avec clé de type de jeton et caractère suivant. Le calcul serait essentiellement le même que ci-dessus: pour chaque état DFA acceptant qui retourne un type de jeton particulier, entrez une valeur vraie dans le tableau pour ce type de jeton et tout caractère avec une transition hors de l'état DFA. Ensuite, vous pouvez rechercher le type de jeton d'un jeton et le premier caractère du jeton suivant pour voir si un espace peut être nécessaire. Cet algorithme ne produit pas de sortie à espacement minimal: il mettrait, par exemple, un espace après le 40 dans votre exemple, car 40 est un pp-number et il est possible que certains pp-number de pp-number soient étendus avec un + ( même si vous ne pouvez pas prolonger de 40 de cette façon). Il est donc possible que gcc utilise une version de cet algorithme.

Ajouter un peu de contexte historique à l’excellente réponse de rici.

Si vous pouvez vous procurer une copie de travail de gcc 2.7.2.3, essayez son pré-processeur. À cette époque, le préprocesseur était un programme distinct du compilateur et utilisait un algorithme très naïf pour la sérialisation du texte, qui tendait à insérer beaucoup plus d’espaces que nécessaire. Lorsque Neil Booth, Per Bothner et moi avons implémenté le préprocesseur intégré (apparaissant dans gcc 3.0 et depuis), nous avons décidé de rendre la sortie de -E un peu plus intelligente en même temps, mais sans rendre l’implémentation trop compliquée. Le kernel de cet algorithme est la fonction de bibliothèque cpp_avoid_paste , définie à l’ adresse https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libcpp/lex.c#l2990 , et son appelant est ici: https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/c-family/c-ppoutput.c#l177 (recherchez “Une logique subtile pour afficher un espace … “).

Dans le cas de votre exemple

 #define Y 20 #define A(x) (10+x+Y) A(A(40)) 

cpp_avoid_paste sera appelé avec un jeton CPP_NUMBER (ce que rici a appelé un “numéro de pp”) à gauche et un jeton “+” à droite. Dans ce cas, il dit inconditionnellement “oui, vous devez insérer un espace pour éviter le collage” plutôt que de vérifier si le dernier caractère du jeton numérique est un eEpP.

La conception du compilateur se résume souvent à un compromis entre précision et simplicité de mise en œuvre.