Les macros sont comme n'importe quel autre outil - un marteau utilisé pour un meurtre n'est pas mauvais parce que c'est un marteau. Il est mauvais dans la façon dont la personne l'utilise de cette manière. Si vous voulez planter des clous, un marteau est un outil parfait.
Il y a quelques aspects des macros qui les rendent "mauvaises" (je développerai chacun d'eux plus tard, et je proposerai des alternatives) :
- Vous ne pouvez pas déboguer les macros.
- La macro-expansion peut entraîner des effets secondaires étranges.
- Les macros n'ont pas d'"espace de noms", donc si vous avez une macro qui entre en conflit avec un nom utilisé ailleurs, vous obtenez des remplacements de macro là où vous ne le vouliez pas, et cela conduit généralement à des messages d'erreur étranges.
- Les macros peuvent affecter des choses dont vous ne vous rendez pas compte.
Alors développons un peu ici :
1) Les macros ne peuvent pas être déboguées. Lorsque vous avez une macro qui se traduit par un nombre ou une chaîne de caractères, le code source contient le nom de la macro et de nombreux débogueurs ne peuvent pas "voir" ce que la macro traduit. Vous ne savez donc pas réellement ce qui se passe.
Remplacement : Utiliser enum
ou const T
Pour les macros de type "fonction", comme le débogueur fonctionne au niveau "par ligne de source où vous vous trouvez", votre macro se comportera comme une seule déclaration, qu'il s'agisse d'une déclaration ou de cent. Il est donc difficile de comprendre ce qui se passe.
Remplacement : Utiliser des fonctions - en ligne si cela doit être "rapide" (mais attention, trop d'inline n'est pas une bonne chose).
2) Les macro-expansions peuvent avoir des effets secondaires étranges.
Le plus célèbre est #define SQUARE(x) ((x) * (x))
et l'utilisation x2 = SQUARE(x++)
. Cela conduit à x2 = (x++) * (x++);
qui, même s'il s'agissait d'un code valide [1], ne serait presque certainement pas ce que le programmeur voulait. S'il s'agissait d'une fonction, il serait possible de faire x++, et x ne s'incrémenterait qu'une fois.
Un autre exemple est "if else" dans les macros, disons que nous avons ceci :
#define safe_divide(res, x, y) if (y != 0) res = x/y;
et ensuite
if (something) safe_divide(b, a, x);
else printf("Something is not set...");
En fait, ça devient complètement la mauvaise chose. ....
Remplacement : fonctions réelles.
3) Les macros n'ont pas d'espace de nom
Si nous avons une macro :
#define begin() x = 0
et nous avons du code en C++ qui utilise begin :
std::vector<int> v;
... stuff is loaded into v ...
for (std::vector<int>::iterator it = myvector.begin() ; it != myvector.end(); ++it)
std::cout << ' ' << *it;
Maintenant, quel message d'erreur pensez-vous obtenir, et où chercher une erreur [en supposant que vous ayez complètement oublié - ou que vous ne connaissiez même pas - la macro begin qui se trouve dans un fichier d'en-tête que quelqu'un d'autre a écrit ? [et encore plus amusant si vous avez inclus cette macro avant l'include - vous vous noieriez dans des erreurs étranges qui n'ont absolument aucun sens lorsque vous regardez le code lui-même.
Remplacement : Eh bien, il ne s'agit pas tant d'un remplacement que d'une "règle" - n'utilisez que des noms en majuscules pour les macros, et n'utilisez jamais de noms en majuscules pour les autres choses.
4) Les macros ont des effets que vous ne soupçonnez pas.
Prenez cette fonction :
#define begin() x = 0
#define end() x = 17
... a few thousand lines of stuff here ...
void dostuff()
{
int x = 7;
begin();
... more code using x ...
printf("x=%d\n", x);
end();
}
Maintenant, sans regarder la macro, on pourrait penser que begin est une fonction, qui ne devrait pas affecter x.
Ce genre de chose, et j'ai vu des exemples bien plus complexes, peut VRAIMENT vous gâcher la journée !
Remplacement : Soit vous n'utilisez pas de macro pour définir x, soit vous passez x en argument.
Il y a des moments où l'utilisation de macros est vraiment bénéfique. Par exemple, on peut envelopper une fonction avec des macros pour transmettre des informations sur le fichier ou la ligne :
#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x) my_debug_free(x, __FILE__, __LINE__)
Nous pouvons maintenant utiliser my_debug_malloc
comme le malloc normal dans le code, mais il a des arguments supplémentaires, donc quand il arrive à la fin et que nous analysons le "quels éléments de mémoire n'ont pas été libérés", nous pouvons imprimer l'endroit où l'allocation a été faite pour que le programmeur puisse retrouver la fuite.
[1] Il est indéfini de mettre à jour une variable plus d'une fois "dans un point de séquence". Un point de séquence n'est pas exactement la même chose qu'une instruction, mais dans la plupart des cas, c'est ce que nous devons considérer comme tel. Ainsi, en faisant x++ * x++
mettra à jour x
deux fois, ce qui n'est pas défini et conduira probablement à des valeurs différentes sur différents systèmes, et à une valeur de résultat différente en x
également.