grim7reaper

A Code Craftsman

Convertir un #define en chaîne de caractères

Cet article provient de mon ancien site Internet. Il a été rédigé le 22 janvier 2011.

À quoi ça sert ?

Très bonne question !

À quoi ça peut bien servir de convertir un #define en chaîne de caractères ? Et bien, pour l’afficher pardi !

J’en vois déjà certains venir en grognant : « Le debug à coup de printf c’est crade ! ». Bah non ! Enfin, ça dépend de comment c’est mis en place : si c’est sous forme de logs, ça peut être très propre. Et puis des fois, on n’a que les logs pour debugger…

Certains me diront que l’on peut utiliser directement printf, et ils n’ont pas tout à fait tort. Mais en fait ça devient vite chiant pour deux raisons :

  1. Si vous changez le type d’un de vos #define, vous êtes bon pour modifier toutes vos chaînes de format. Ok, certains compilateurs (comme gcc une fois bien réglé) peuvent vous aider à repérer ces endroits, mais le remplacement reste à votre charge ;
  2. Si vous voulez afficher une macro substituée (toujours à des fins de debug, pour voir si une macro est bien développée comme prévu), vous allez faire comment avec printf ? Certes, gcc (et clang) propose l’option -E pour voir le code après passage du préprocesseur (fort pratique), mais quid des autres compilateurs (on n’a pas toujours la possibilité de bosser avec de bons compilateurs) ?

Et comment on fait ça ?

Première tentative

Comme vous êtes des gens un minimum cultivé en C, vous avez tout de suite pensé à l’opérateur # qui permet de convertir un paramètre de macro en chaîne de caractères.

Bien vu ! C’est comme cela qu’il faut procéder. Allez, faisons quelques tests.

Premier jet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

#define TO_STR(a) #a

#define DOT       '.'
#define INTEGER   42
#define MY_PI     3.14
#define STRING    "42"
#define SQUARE(x) ((x)*(x))

int main(void)
{
    puts(TO_STR(DOT));
    puts(TO_STR(INTEGER));
    puts(TO_STR(MY_PI));
    puts(TO_STR(STRING));
    puts(TO_STR(SQUARE(SQUARE(INTEGER))));

    return 0;
}

Et maintenant, testons ce code.

Résultat
1
2
3
4
5
DOT
INTEGER
MY_PI
STRING
SQUARE(SQUARE(INTEGER))

Hum :-/, ce n’est pas vraiment le résultat attendu (on affiche les symboles à la place des valeurs). Mais alors, où est le problème ?

Petite explication

Et bien comme souvent, il suffit de lire la norme. Et la norme (ISO/IEC 9899:TC3, 6.10.3 Macro replacement, page 152) nous dit :

If a # preprocessing token, followed by an identifier, occurs lexically at the point at which a preprocessing directive could begin, the identifier is not subject to macro replacement.

Donc, si le symbole qui suit l’opérateur # est en fait un identifiant, il ne sera pas substitué. Cela explique la sortie produite par le code précèdent.

Par exemple, pour INTEGER, TO_STR(INTEGER) est remplacé par #INTEGER lors de la substitution de la macro, puis l’opérateur # est appliqué (ici pas de substitution à cause de la règle précedemment citée, donc INTEGER n’est pas remplacé par 42) et on obtient la chaîne de caractères INTEGER.

La solution

La solution est simple, si l’opérateur # ne fait pas la substitution il nous suffit de la faire nous‑même avant l’appel. Pour cela, nous allons utiliser une macro intermédiaire.

Notre définition de TO_STR devient donc :

Version fonctionelle
1
2
#define TO_STR_(a) #a
#define TO_STR(a)  TO_STR_(a)

Et voilà, maintenant on obtient bien le résultat attendu.

Résultat
1
2
3
4
5
'.'
42
3.14
"42"
((((42)*(42)))*(((42)*(42))))

Par exemple, pour INTEGER, TO_STR(INTEGER) est remplacé par TO_STR_(42) (la première macro fait la substitution) puis la seconde macro est remplacée à son tour et l’on obtient #42. Une fois l’opérateur # appliqué on obtient bien la chaîne de caractères 42.