Les constantes

Si vous faites un #define PI 3.1415927, le compilateur n'aura jamais connaissance de cet identifiant puisque le préprocesseur aura remplacé toutes les occurrences de PI par sa valeur correspondante. Donc si une erreur de compilation a lieu, vous ne verrez que le 3.1415927 et non PI dans le message d'erreur. On n'a donc aucun moyen de savoir d'où provient cette valeur. De même qu'en phase de débogage, vous ne verrez que la valeur et non l'identifiant. Au lieu de cela, définissez une constante : const double PI = 3.1415927;.

Dans le cas où vous souhaiteriez utiliser une chaîne constante, au lieu d'un #define COLOR "red", déclarez un pointeur constant sur un caractère constant comme ceci : const char * const COLOR = "red";. De cette façon, on est certain que la chaîne et son pointeur ne peuvent être changés.

Les fonctions

Une autre mauvaise utilisation des macros est la définition de fonctions. Une des macros les plus utilisée est celle-ci : #define max(a,b) . Elle permet de retourner le maximum de deux valeurs. Ainsi, lorsque le préprocesseur rencontrera une chaine du type max(10, i), il la remplacera par dans le code avant de l'envoyer au compilateur. Ce genre de chose possède de nombreux inconvénients comme la difficulté de mise en oeuvre, notamment en ce qui concerne l'utilisation abusive des parenthèses pour que les priorités des opérateurs se fassent correctement. De plus, la fonction max n'existe pas, donc on en revient aux mêmes problèmes énoncés pour les constantes. Ce genre de fonction marche très bien, sauf lorsqu'on lui passe en argument des expressions et non des variables, il y a alors des effets de bord particulièrement indésirables.

// Fichier main.cpp
#include <iostream>
 
#define max(a,b) ((a) > (b) ? (a) : (b))
 
int main(int argc, char ** argv)
{
  int x = 10;
  int y = 5;
 
  int w = max(x, y); // ok
  std::cout << w << std::endl;
 
  int z = max(++x, y); // pas ok
  // le préprocesseur va remplacer max(++x, y) par ((++x) > (y) ? (++x) : (y))
  // ce qui dans notre cas, devrait retourner 12 et non 10 car ++x est évalué
  // deux fois.
 
  std::cout << z << std::endl;
  return EXIT_SUCCESS;
}

Heureusement, il est possible d'obtenir l'efficacité d'une macro, le comportement d'une fonction et la sécurité du contrôle de type en utilisant le mot clé inline.

inline int max(int a, int b)
{
  return a > b ? a : b;
}

Ainsi chaque appel à max sera remplacé par le code contenu dans le corps de la fonction. On a donc le même fonctionnement qu'avec la macro. Pas tout à fait, telle qu'elle est définie ici, la fonction ne prend en paramètre que des entiers. Pour pallier à ce problème, il suffit d'utiliser les template.

template<class T>
inline const T& max(const T &a, const T &b)
{
  return a > b ? a : b;
}

La fonction max fonctionne désormais pour tout type de données à condition que l'opérateur > soit défini. Les paramètres d'entrée et le retour de la fonction sont tous des références d'un même type. Comme on ne connait pas le type d'objet passé en paramètre, il est très intéressant d'utiliser les références pour des raisons d'efficacité.

Avant d'écrire des fonctions template de ce genre, vérifiez qu'elles n'existent pas déjà dans la STL, c'est le cas ici. Il suffit de faire un #include <algorithm> pour avoir accès aux fonctions std::max et std::min notamment.

Cependant, les macros restent toujours le moyen unique d'inclure un fichier, de ne l'inclure qu'une seule fois, et d'effectuer une compilation conditionnelle en fonction du système d'exploitation, de l'EDI, etc.

Vous pourrez approfondir le sujet avec la sélection des livres suivants : Pour mieux développer avec C++, Standards de programmation en C++ et Le langage C++.