127 votes

Pourquoi les macros du préprocesseur sont-elles mauvaises et quelles sont les alternatives ?

J'ai toujours posé cette question mais je n'ai jamais reçu de bonne réponse ; je pense que presque tous les programmeurs, avant même d'écrire le premier "Hello World", ont rencontré une phrase comme "les macros ne devraient jamais être utilisées", "les macros sont diaboliques", etc. Avec le nouveau C++11, y a-t-il une réelle alternative après tant d'années ?

La partie facile concerne les macros comme #pragma qui sont spécifiques à la plate-forme et au compilateur, et la plupart du temps, ils présentent de graves défauts tels que #pragma once qui est source d'erreurs dans au moins deux situations importantes : même nom dans des chemins différents et avec certaines configurations de réseau et systèmes de fichiers.

Mais en général, qu'en est-il des macros et des alternatives à leur utilisation ?

209voto

Mats Petersson Points 70074

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) :

  1. Vous ne pouvez pas déboguer les macros.
  2. La macro-expansion peut entraîner des effets secondaires étranges.
  3. 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.
  4. 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.

26voto

utnapistim Points 12060

L'expression "les macros sont diaboliques" fait généralement référence à l'utilisation de #define, et non de #pragma.

Plus précisément, l'expression fait référence à ces deux cas :

  • définir les nombres magiques comme des macros

  • utilisation de macros pour remplacer des expressions

Avec le nouveau C++ 11, existe-t-il une réelle alternative après tant d'années ?

Oui, pour les éléments de la liste ci-dessus (les nombres magiques doivent être définis avec const/constexpr et les expressions doivent être définies avec des fonctions [normal/inline/template/inline template].

Voici quelques-uns des problèmes introduits par la définition de nombres magiques en tant que macros et le remplacement d'expressions par des macros (au lieu de définir des fonctions pour évaluer ces expressions) :

  • lors de la définition de macros pour les nombres magiques, le compilateur ne conserve aucune information de type pour les valeurs définies. Cela peut provoquer des avertissements de compilation (et des erreurs) et perturber les personnes qui déboguent le code.

  • en définissant des macros au lieu de fonctions, les programmeurs qui utilisent ce code s'attendent à ce qu'elles fonctionnent comme des fonctions, ce qui n'est pas le cas.

Considérez ce code :

#define max(a, b) ( ((a) > (b)) ? (a) : (b) )

int a = 5;
int b = 4;

int c = max(++a, b);

On s'attendrait à ce que a et c soient 6 après l'affectation à c (comme ce serait le cas, en utilisant std::max au lieu de la macro). Au lieu de cela, le code s'exécute :

int c = ( ((++a) ? (b)) ? (++a) : (b) ); // after this, c = a = 7

En outre, les macros ne prennent pas en charge les espaces de noms, ce qui signifie que la définition de macros dans votre code limitera le code client quant aux noms qu'il peut utiliser.

Cela signifie que si vous définissez la macro ci-dessus (pour max), vous ne serez plus en mesure de #include <algorithm> dans le code ci-dessous, sauf si vous l'écrivez explicitement :

#ifdef max
#undef max
#endif
#include <algorithm>

Le fait d'avoir des macros au lieu de variables/fonctions signifie également que vous ne pouvez pas prendre leur adresse :

  • si une macro en tant que constante s'évalue à un nombre magique, vous ne pouvez pas la passer par adresse

  • pour une macro en tant que fonction, vous ne pouvez pas l'utiliser comme prédicat ou prendre l'adresse de la fonction ou la traiter comme un foncteur.

Edit : A titre d'exemple, l'alternative correcte à l'option #define max ci-dessus :

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

Elle fait tout ce que fait la macro, avec une limitation : si les types des arguments sont différents, la version template vous oblige à être explicite (ce qui conduit en fait à un code plus sûr et plus explicite) :

int a = 0;
double b = 1.;
max(a, b);

Si ce max est défini comme une macro, le code sera compilé (avec un avertissement).

Si ce max est défini comme une fonction template, le compilateur signalera l'ambiguïté, et vous devrez dire soit max<int>(a, b) ou max<double>(a, b) (et ainsi indiquer explicitement votre intention).

14voto

phaazon Points 1062

Un problème courant est le suivant :

#define DIV(a,b) a / b

printf("25 / (3+2) = %d", DIV(25,3+2));

Il imprimera 10, et non 5, parce que le préprocesseur le développera de cette façon :

printf("25 / (3+2) = %d", 25 / 3 + 2);

Cette version est plus sûre :

#define DIV(a,b) (a) / (b)

4voto

axeoth Points 1624

Les macros sont précieuses notamment pour créer du code générique (les paramètres de la macro peuvent être n'importe quoi), parfois avec des paramètres.

De plus, ce code est placé (c'est-à-dire inséré) à l'endroit où la macro est utilisée.

En revanche, des résultats similaires peuvent être obtenus avec :

  • fonctions surchargées (différents types de paramètres)

  • les modèles, en C++ (types et valeurs de paramètres génériques)

  • les fonctions inline (placer le code là où elles sont appelées, au lieu de sauter à une définition unique -- cependant, il s'agit plutôt d'une recommandation pour le compilateur).

edit : quant à savoir pourquoi les macros sont mauvaises :

1) pas de vérification du type des arguments (ils n'ont pas de type), donc ils peuvent être facilement mal utilisés 2) s'étendent parfois en code très complexe, qui peut être difficile à identifier et à comprendre dans le fichier prétraité 3) il est facile de faire du code sujet à des erreurs dans les macros, comme par exemple :

#define MULTIPLY(a,b) a*b

et ensuite appeler

MULTIPLY(2+3,4+5)

qui s'étend en

2+3*4+5 (et non en : (2+3)*(4+5)).

Pour avoir ce dernier, vous devez définir :

#define MULTIPLY(a,b) ((a)*(b))

3voto

Sandi Hrvić Points 191

Je ne pense pas qu'il y ait quelque chose de mal à utiliser les définitions du préprocesseur ou les macros comme vous les appelez.

Il s'agit d'un concept de (méta)langage que l'on trouve en c/c++ et, comme tout autre outil, ils peuvent vous faciliter la vie si vous savez ce que vous faites. Le problème avec les macros est qu'elles sont traitées avant votre code c/c++ et génèrent un nouveau code qui peut être défectueux et causer des erreurs de compilation qui sont toutes évidentes. D'un autre côté, elles peuvent vous aider à garder votre code propre et vous épargner beaucoup de frappe si elles sont utilisées correctement, ce qui revient à une question de préférence personnelle.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X