Guru Of the Week n° 32 : les macros préprocesseurs

| Herb Sutter | Traduction : l'équipe de la rubrique C++ |

Difficulté : 4 / 10
Avec toutes les caractéristiques type-safe de C++, pourquoi ne devriez-vous encore écrire #define ?

Retrouvez l'ensemble des Guru of the Week sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Guru Of the Week n° 32 : les macros préprocesseurs

- Auteurs : Herb Sutter, Traduction : l'équipe de la rubrique C++

- Parution : 31 mai 2012

- Licence : Copyright ® 2009 Herb Sutter. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Problème

- II. Solution

- II-A. Header Guards

- II-B. Accès aux caractéristiques préprocesseurs

- II-C. Code de sélection au moment de compilation (ou code spécifique du constructeur)

- II-C-1. Code de débogage

- II-C-2. Code spécifique de plateforme

- II-C-3. Variantes de représentation des données

- III. Remerciements

I - Problème

Avec des caractéristiques souples comme la surcharge et les templates type-safe, pourquoi un programmeur C++ devrait-il jamais écrire #define ?

II - Solution

Avec des caractéristiques souples comme la surcharge et les templates type-safe, pourquoi un programmeur C++ devrait-il jamais écrire #define ?

Les caractéristiques de C++ éliminent souvent, mais pas toujours, le besoin d'utiliser #define. Par exemple, la commande const int c = 42; est meilleure que #define c 42 parce qu'elle assure la sécurité du type, évite les éditions accidentelles préprocesseurs, et pour quelques autres raisons. Il existe néanmoins toujours quelques bonnes raisons d'écrire #define :

II-A - Header Guards

C'est une astuce habituelle pour éviter les inclusions multiples d'en-tête :

#ifndef MYPROG_X_H
#define MYPROG_X_H

// ... le reste du fichier d'en-tête x.h va ici...

#endif

II-B - Accès aux caractéristiques préprocesseurs

Souvent, on aime bien insérer des choses comme des numéros de ligne et des temps de construction en code diagnostic. Une façon facile de le faire consiste à utiliser des macro prédéfinies telles que __FILE__, __LINE__, __DATE__ et __TIME__. Pour la même raison et pour d'autres, il est souvent utile d'utiliser les opérateurs préprocesseurs de transformation en chaîne littérale et de concaténation de symboles (# et ##).

II-C - Code de sélection au moment de compilation (ou code spécifique du constructeur)

C'est la catégorie d'utilisation la plus riche et la plus importante pour le préprocesseur. Bien que je sois tout sauf un fan de la magie du préprocesseur, il y a des choses que vous ne pouvez pas faire aussi bien autrement, voire que vous ne pouvez pas faire du tout autrement.

II-C-1 - Code de débogage

Parfois, vous voulez bâtir votre système avec certaines pièces de code "supplémentaires" (typiquement des informations de débogage) et parfois vous ne le voulez pas :

void f()
{
#ifdef MY_DEBUG
cerr << "some trace logging" << endl;
#endif

// ... le reste de f() va ici...
}

Il est possible de faire ça au moment où vous lancez le programme, bien sûr. En prenant la décision au moment de la compilation, vous évitez le surcoût à l'exécution, mais vous perdez également la souplesse de différer la décision jusqu'au moment de lancer le programme.

II-C-2 - Code spécifique de plateforme

Habituellement, il est préférable de traiter le code spécifique à la plateforme dans une fabrique pour assurer une meilleure organisation du code et une plus grande souplesse pendant le fonctionnement du programme. Parfois, cependant, il existe trop peu de différences pour justifier une fabrique et le préprocesseur peut être une méthode pratique pour rendre du code facultatif. (1)

II-C-3 - Variantes de représentation des données

Un exemple courant est qu'un module peut définir une liste de codes d'erreur, que des utilisateurs extérieurs devraient voir comme une simple énumération avec commentaires, mais qui, à l'intérieur du module, devrait être stockée dans une carte pour un affichage facile. Cela donne :

// Pour les utilisateurs externes :
enum Errors {
ERR_OK = 0,           // No error
ERR_INVALID_PARAM = 1 // <description>
...
}

// Pour l'utilisation interne du module :
map<Error,const char*> lookup;
lookup.insert( make_pair( Error(0), "No error" ) );
lookup.insert( make_pair( Error(1), "<description>" ) );
...

Nous aimerions avoir les deux représentations sans définir les informations concrètes (paires de code/message) deux fois. Avec la magie des macros, on peut simplement écrire une liste d'erreurs comme suit, en créant la structure qui convient au moment de la compilation :

DERR_ENTRY( ERR_OK, 0, "No error" ),
DERR_ENTRY( ERR_INVALID_PARAM, 1, "<description>" ),
//...

Les implémentations de DERR_ENTRY et des macros qui y sont liées sont laissées au lecteur. Ce sont trois exemples courants ; il y en a beaucoup d'autres.

III - Remerciements

Cet article est une traduction par l'équipe de la rubrique C++ de l'article de Herb Sutter publié sur Guru of the Week. Vous pouvez retrouver cet article dans sa version originale sur le site de Guru of the Week : Preprocessor Macros.

Merci à Luc Hermitte et à Kalith pour leur relecture, à Gurdil le nain et à ClaudeLELOUP pour leur relecture orthographique.

(1)Note de traduction : d'autres solutions sont possible, comme par exemple placer le code spécifique de la plateforme dans des fichiers séparés et sélectionner le fichier adéquate en changeant les définitions des projets lors de la compilation.