Musée des horreursmacros

Quelques raccourcis :

Ce qui suit liste quelques horreurs quant à l'utilisation de macros, en C ou en C++. Notez qu'en C++, la plupart des cas où une macro serait tentante peuvent être avantageusement remplacés par des templates (pas tous, tristement, mais plusieurs d'entre eux).

Entrée 00 – Des macros qu'on prend pour des fonctions

Soit le programme C++ suivant :

#include <algorithm>
#include <iostream>
int main()
{
   int a, b;
   if (std::cin >> a >> b)
      std::cout << std::min(a,b) << std::endl;
}

Ce programme compile et fonctionne normalement. Maintenant, supposons que l'on lui applique la modification suivante :

#include <windows.h> // <-- ICI
#include <algorithm>
#include <iostream>
int main()
{
   int a, b;
   if (std::cin >> a >> b)
      std::cout << std::min(a,b) << std::endl;
}

...soudainement, le programme ne compile plus! Et qui plus est, l'erreur de compilation fait mention d'un '(' illégal à côté du "::" qui précède le mot min dans std::min. Que se passe-t-il?

L'horreur : ici, le problème n'est pas vraiment que l'on ait inclus <windows.h>, bien que cet en-tête massif n'apporte rien au programme, mais bien que cet en-tête met en relief quelques très mauvaises pratiques de programmation. En particulier, on y trouve des macros pour simuler des fonctions comme min(a,b) et max(a,b) en langage C, qui ne permet pas de surcharger les fonctions sur la base du type de leurs paramètres.

En effet, l'usage avec C, désuet en C++, est de réaliser la généricité sur la base de macros, et il se trouve que <windows.h> (et ce n'est pas le seul coupable), sans doute parce que ces macros étaient utilisées à plusieurs endroits dans les sources de l'entreprise à l'époque, expose ces macros au mileu d'une multitude d'outils dont les développeurs ont besoin sur cette plateforme. Les macros provoquant une transformation lexicale dans le code, et min(a,b) étant sans doute implémenté comme suit :

#define min(a,b) (((a)<(b))?(a):(b))

... notre programme, une fois transformé par le préprocesseur, devient grammaticalement incorrect :

Avant le préprocesseur Après le préprocesseur
#include <windows.h>
#include <algorithm>
#include <iostream>
int main()
{
   int a, b;
   if (std::cin >> a >> b)
      std::cout << std::min(a,b) << std::endl;
}
#include <windows.h>
#include <algorithm>
#include <iostream>
int main()
{
   int a, b;
   if (std::cin >> a >> b)
      std::cout << std::(((a)<(b))?(a):(b)) << std::endl; // <-- ICI
}

Le bon usage : évidemment, le plus simple est de ne pas inclure des en-têtes ayant un comportement hygiénique discutable. Ici, ce serait la solution idéale, puisque <windows.h> n'apporte rien au programme. On n'a toutefois pas toujours ce luxe.

De l'autre côté de la clôture, un sain usage de macros est de toujours leur donner des noms en majuscules, pour éviter justement de tels conflits. Ici, MIN(a,b) et MAX(a,b) auraient été bien moins irritants en pratique.

Revenons du côté du code client. Une alternative est d'éliminer les macros offensantes par un #undef. Ainsi, présumant que l'on tienne à <windows.h> ici, on pourrait simplement écrire :

#include <windows.h>
#ifdef min // <-- ICI
#undef min // <-- ICI
#endif     // <-- ICI
#include <algorithm>
#include <iostream>
int main()
{
   int a, b;
   if (std::cin >> a >> b)
      std::cout << std::min(a,b) << std::endl;
}

Note au passage : certains parmi vous pourraient remarquer qu'un using aurait masqué le problème ici. En effet, si nous avions écrit ceci :

#include <windows.h>
#include <algorithm>
#include <iostream>
using namespace std; // <-- ICI
int main()
{
   int a, b;
   if (cin >> a >> b)
      cout << min(a,b) << endl;
}

... alors le code aurait compilé sans peine. On serait alors tenté d'utiliser des using globaux en pensant qu'il s'agit d'une solution, mais en fait nous n'avons fait que nous fermer les yeux. En effet, la transformation du code par le préprocesseur a tout de même lieu, et le code devient, suivant la transformation, comme suit :

#include <windows.h>
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
   int a, b;
   if (cin >> a >> b)
      cout << (((a)<(b))?(a):(b)) << endl;
}

... et nous n'avons pas utilisé la fonction std::min() du tout. Cela dit, est-ce grave? Après tout, nous avons les mêmes résultats en fin de compte, du moins avec ce programme... Mais il se trouve que std::min() supporte plusieurs signatures, donc une qui accepte en paramètre une initializer_list. Si nous souhaitions le plus petit de plus de deux nombres, comme dans ce cas-ci :

Ceci fonctionne... ...ceci fonctionne aussi... ...mais ceci ne fonctionne pas
#include <algorithm>
#include <iostream>
int main()
{
   int a, b, c;
   if (std::cin >> a >> b >> c)
      std::cout << std::min({ a, b, c}) << std::endl;
}
#include <windows.h>
#ifdef min
#undef min
#endif
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
   int a, b, c;
   if (cin >> a >> b >> c)
      cout << min({ a, b, c }) << endl;
}
#include <windows.h>
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
   int a, b, c;
   if (cin >> a >> b >> c)
      cout << min({ a, b, c }) << endl;
}

... alors nous verrions que notre manoeuvre n'a rien réglé.

Entrée 01 – Obscure créativité pour définir TRUE ou FALSE

La situation : en langage C, l'existence d'un type pour représenter spécifiquement les booléens est une chose relativement récente, et est moins essentielle qu'en C++ (où le système de types est beaucoup plus riche, et prend plus de place dans le design des programmes). La tradition y est de définir les concepts de vrai et de faux par des macros nommées TRUE et FALSE respectivement. Quelques définitions usuelles suivent (la liste n'est pas exhaustive). Si vous utilisez une bibliothèque supportant le langage C, il est probable que l'une ou l'autre de ces paires de macros y apparaisse :

Implémentations possibles des macros TRUE et FALSE (liste non-exhaustive)
#define FALSE 0
#define TRUE 1
#define FALSE 0
#define TRUE (~0)
#define FALSE 0
#define TRUE (-1)
#define FALSE 0
#define TRUE (!FALSE)
#define FALSE (0!=0)
#define TRUE (0==0)

Ces définitions ont toutes le mérite d'être relativement simples, et partagent la vertu essentielle de faire de TRUE et de FALSE des valeurs distinctes.

La première est sans doute celle qui semblera la plus évidente à la majorité des gens, donnant des valeurs fixes aux deux littéraux. Il se trouve que la valeur entiere de true en C++ est fixée à 1 par le standard, ce qui rend les deux macros compatibles avec leurs cousines plus modernes (faux et zéro sont équivalents dans presque tous les langages ayant été portés à mon attention, mais le concept de vrai est beaucoup plus... fluide).

#define FALSE 0
#define TRUE 1

La deuxième détermine TRUE comme l'inverse bit à bit de FALSE.

#define FALSE 0
#define TRUE (~0)

La troisième, en fixant TRUE comme -1, est probablement équivalente à la deuxième (cela peut varier selon le contexte, les macros n'étant pas typées a priori).

#define FALSE 0
#define TRUE (-1)

La quatrième fait de TRUE l'inverse logique de FALSE.

#define FALSE 0
#define TRUE (!FALSE)

La cinquième, portée à mon attention par mon éminent ami Claude Cardinal, définit à la fois TRUE et FALSE par des expressions qui s'évaluent au résultat booléen correspondant. J'y vois une élégance certaine.

#define FALSE (0!=0)
#define TRUE (0==0)

L'horreur : les macros obscurcissant de prime abord le code en le transformant avant que le compilateur n'y ait accès, mieux vaut ne pas empirer la situation en essayant d'être astucieux. Ainsi, cette définition a été portée à mon attention par l'éminente Kate Gregory :

Ce que vous voyez ici a été rapporté sur un site de soutien par les pairs (StackOverflow si je ne m'abuse) par un programmeur perplexe. Les définitions sont correctes, techniquement, du fait que :

  • La macro FALSE est définie comme le résultat de la soustraction d'un symbole par lui-même, ce qui donne bien 0, alors que
  • La macro TRUE est définie comme le résultat de la division entière d'un symbole (non-nul) par lui-même, ce qui donne bien 1

Cela dit, outre le côté amusant de mystifier les collègues, il n'y a pas d'avantage à obfusquer le concept derrière une astuce. Si vous devez utiliser des macros, visez quelque chose de simple et d'élégant, qui se comprendra sans peine.

#define FALSE ('-'-'-')
#define TRUE  ('/'/'/')

Valid XHTML 1.0 Transitional

CSS Valide !