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


92

J'ai toujours posé cette question mais je n'ai jamais reçu de très bonne réponse; Je pense que presque tous les programmeurs avant même d'écrire le premier "Hello World" avaient rencontré une phrase comme "macro ne devrait jamais être utilisée", "macro are evil" et ainsi de suite, ma question est: pourquoi? Avec le nouveau C ++ 11, y a-t-il une vraie alternative après tant d'années?

La partie la plus simple concerne les macros telles #pragmaque, qui sont spécifiques à la plate-forme et au compilateur, et la plupart du temps, elles ont de graves défauts comme #pragma oncecelui-ci est sujet aux erreurs dans au moins 2 situations importantes: le même nom dans des chemins différents et avec certaines configurations réseau et systèmes de fichiers.

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


19
#pragman'est pas macro.
FooF

1
@foof directive de préprocesseur?
user1849534

6
@ user1849534: Oui, c'est ce que c'est ... et les conseils concernant les macros ne parlent pas #pragma.
Ben Voigt

1
Vous pouvez faire beaucoup avec constexpr, les inlinefonctions et templates, mais boost.preprocessoret chaosmontrer que les macros ont leur place. Sans parler des macros de configuration pour les compilateurs de différence, les plates-formes, etc.
Brandon

Réponses:


161

Les macros sont comme n'importe quel autre outil - un marteau utilisé dans un meurtre n'est pas mauvais parce que c'est un marteau. C'est mauvais dans la manière dont la personne l'utilise de cette manière. Si vous voulez enfoncer des clous, un marteau est un outil parfait.

Il y a quelques aspects des macros qui les rendent "mauvaises" (je les développerai plus tard et suggérerai des alternatives):

  1. Vous ne pouvez pas déboguer les macros.
  2. L'expansion des macros peut entraîner d'étranges effets secondaires.
  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, ce qui conduit généralement à des messages d'erreur étranges.
  4. Les macros peuvent affecter des choses que vous ne réalisez pas.

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, le code source aura le nom de la macro et de nombreux débogueurs, vous ne pouvez pas "voir" ce que la macro se traduit. Donc, vous ne savez pas vraiment ce qui se passe.

Remplacement : utilisez enumouconst T

Pour les macros «fonctionnelles», comme le débogueur fonctionne au niveau «par ligne source où vous êtes», votre macro agira comme une seule instruction, qu'il s'agisse d'une instruction ou d'une centaine. Rend difficile de comprendre ce qui se passe.

Remplacement : utilisez des fonctions - en ligne si cela doit être "rapide" (mais attention, trop de fonctions en ligne n'est pas une bonne chose)

2) Les extensions de macros peuvent avoir des effets secondaires étranges.

Le célèbre est #define SQUARE(x) ((x) * (x))et l'utilisation x2 = SQUARE(x++). Cela conduit à x2 = (x++) * (x++);ce qui, même s'il s'agissait de code valide [1], ne serait certainement pas ce que le programmeur voulait. Si c'était une fonction, ce serait bien de faire x ++, et x n'incrémenterait qu'une seule 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;

puis

if (something) safe_divide(b, a, x);
else printf("Something is not set...");

Cela devient en fait complètement faux ...

Remplacement : de vraies fonctions.

3) Les macros n'ont pas d'espace de noms

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ù recherchez-vous une erreur [en supposant que vous avez complètement oublié - ou même pas au courant - la macro de début qui se trouve dans un fichier d'en-tête écrit par quelqu'un d'autre? [et encore plus amusant si vous incluiez cette macro avant l'inclusion - vous seriez noyé dans des erreurs étranges qui n'ont absolument aucun sens quand vous regardez le code lui-même.

Remplacement : Eh bien, il n'y a pas tant un remplacement qu'une «règle» - n'utilisez que des noms en majuscules pour les macros, et n'utilisez jamais tous les noms en majuscules pour d'autres choses.

4) Les macros ont des effets que vous ne réalisez 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, vous penseriez que begin est une fonction, qui ne devrait pas affecter x.

Ce genre de chose, et j'ai vu des exemples beaucoup plus complexes, peut VRAIMENT gâcher votre journée!

Remplacement : n'utilisez pas de macro pour définir x ou transmettez x comme argument.

Il y a des moments où l'utilisation de macros est vraiment bénéfique. Un exemple consiste à envelopper une fonction avec des macros pour transmettre des informations de fichier / ligne:

#define malloc(x) my_debug_malloc(x, __FILE__, __LINE__)
#define free(x)  my_debug_free(x, __FILE__, __LINE__)

Maintenant, nous pouvons utiliser my_debug_malloccomme malloc régulier dans le code, mais il a des arguments supplémentaires, donc quand il s'agit de la fin et que nous analysons les "éléments de mémoire qui n'ont pas été libérés", nous pouvons afficher où l'allocation a été faite afin que le le programmeur peut localiser 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 devrions considérer comme. Cela x++ * x++entraînera une mise à jour xdeux fois, ce qui n'est pas défini et entraînera probablement des valeurs différentes sur différents systèmes, xainsi qu'une valeur de résultat différente .


6
Les if elseproblèmes peuvent être résolus en enveloppant le corps de la macro à l'intérieur do { ... } while(0). Cela se comporte comme on pourrait s'y attendre en ce qui concerne ifet foret d'autres problèmes de flux de contrôle potentiellement risqués. Mais oui, une vraie fonction est généralement une meilleure solution. #define macro(arg1) do { int x = func(arg1); func2(x0); } while(0)
Aaron McDaid

11
@AaronMcDaid: Oui, il existe des solutions de contournement qui résolvent certains des problèmes exposés dans ces macros. Le but de mon article n'était pas de montrer comment bien faire des macros, mais "comment il est facile de se tromper de macros", là où il existe une bonne alternative. Cela dit, il y a des choses que les macros résolvent très facilement, et il y a des moments où les macros sont également la bonne chose à faire.
Mats Petersson du

1
Au point 3, les erreurs ne sont plus vraiment un problème. Les compilateurs modernes tels que Clang diront quelque chose comme note: expanded from macro 'begin'et montreront où beginest défini.
kirbyfan64sos

5
Les macros sont difficiles à traduire dans d'autres langues.
Marco van de Voort

1
@FrancescoDondi: stackoverflow.com/questions/4176328/… (un peu plus bas dans cette réponse, il parle de i ++ * i ++ et autres.
Mats Petersson

21

Le dicton «les macros sont mauvaises» se réfère généralement à l'utilisation de #define, pas de #pragma.

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

  • définition des nombres magiques sous forme de macros

  • utilisation de macros pour remplacer des expressions

avec le nouveau C ++ 11, il y a une vraie 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 les fonctions [normal / inline / template / inline template].

Voici quelques-uns des problèmes introduits en définissant des nombres magiques comme des macros et en remplaçant des 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 dérouter les personnes qui déboguent le code.

  • lors de la définition de macros au lieu de fonctions, les programmeurs qui utilisent ce code s'attendent à ce qu'elles fonctionnent comme des fonctions et ce 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);

Vous vous attendez à ce que a et c soient 6 après l'affectation à c (comme il le ferait, en utilisant std :: max au lieu de la macro). Au lieu de cela, le code effectue:

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

En plus de cela, 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 dans les noms qu'ils peuvent utiliser.

Cela signifie que si vous définissez la macro ci-dessus (pour max), vous ne pourrez plus le faire #include <algorithm>dans aucun des codes ci-dessous, sauf si vous écrivez explicitement:

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

Avoir des macros au lieu de variables / fonctions signifie également que vous ne pouvez pas prendre leur adresse:

  • si une macro-constante est évaluée à un nombre magique, vous ne pouvez pas la passer par adresse

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

Edit: À titre d'exemple, la bonne alternative à ce qui #define maxprécède:

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

Cela fait tout ce que fait la macro, avec une limitation: si les types d'arguments sont différents, la version du modèle 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 se compilera (avec un avertissement).

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


1
Il n'est pas nécessaire que ce soit spécifique à C ++ 11; vous pouvez simplement utiliser des fonctions pour remplacer l'utilisation des macros-as-expressions et [static] const / constexpr pour remplacer l'utilisation des macros-as-constantes.
utnapistim

1
Même C99 autorise l'utilisation de const int someconstant = 437;, et il peut être utilisé presque de toutes les manières dont une macro serait utilisée. De même pour les petites fonctions. Il y a quelques choses où vous pouvez écrire quelque chose comme une macro qui ne fonctionnera pas dans une expression régulière en C (vous pouvez faire quelque chose qui fait la moyenne d'un tableau de n'importe quel type de nombre, ce que C ne peut pas faire - mais C ++ a des modèles pour ça). Alors que C ++ 11 ajoute quelques autres choses que "vous n'avez pas besoin de macros pour cela", il est pour la plupart déjà résolu dans le C / C ++ antérieur.
Mats Petersson

Faire un pré-incrément en passant un argument est une pratique de codage terrible. Et quiconque codant en C / C ++ ne doit pas présumer qu'un appel de type fonction n'est pas une macro.
StephenG

De nombreuses implémentations mettent volontairement les identifiants entre parenthèses maxet mins'ils sont suivis d'une parenthèse gauche. Mais vous ne devriez pas définir de telles macros ...
LF

14

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, pas 5, car le préprocesseur l'étendra de cette façon:

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

Cette version est plus sûre:

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

2
exemple intéressant, en gros, ce ne sont que des jetons sans sémantique
user1849534

Oui. Ils sont développés comme ils sont donnés à la macro. La DIVmacro peut être réécrite avec une paire de () autour b.
phaazon

2
Vous voulez dire #define DIV(a,b)non #define DIV (a,b), ce qui est très différent.
rici

6
#define DIV(a,b) (a) / (b)n'est pas assez bon; en #define DIV(a,b) ( (a) / (b) )
règle

3

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

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

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

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

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

  • fonctions en ligne (placez le code là où elles sont appelées, au lieu de passer à une définition en un seul point - cependant, c'est plutôt une recommandation pour le compilateur).

edit: quant à la raison pour laquelle les macro sont mauvaises:

1) pas de vérification de type des arguments (ils n'ont pas de type), donc peuvent être facilement mal utilisés 2) parfois se développer dans un code très complexe, qui peut être difficile à identifier et à comprendre dans le fichier prétraité 3) il est facile de faire une erreur -prone code dans les macros, comme:

#define MULTIPLY(a,b) a*b

puis appelez

MULTIPLY(2+3,4+5)

qui se développe en

2 + 3 * 4 + 5 (et pas dans: (2 + 3) * (4 + 5)).

Pour avoir ce dernier, vous devez définir:

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

3

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

C'est un concept de (méta) langage trouvé dans 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 provoquer des erreurs de compilation qui sont tout sauf évidentes. Du bon côté, ils peuvent vous aider à garder votre code propre et vous faire économiser beaucoup de frappe s'il est utilisé correctement, donc cela dépend de vos préférences personnelles.


De plus, comme indiqué par d'autres réponses, des définitions de préprocesseur mal conçues peuvent produire du code avec une syntaxe valide mais une signification sémantique différente, ce qui signifie que le compilateur ne se plaindra pas et que vous avez introduit un bogue dans votre code qui sera encore plus difficile à trouver.
Sandi Hrvić

3

Les macros en C / C ++ peuvent servir d'outil important pour le contrôle de version. Le même code peut être livré à deux clients avec une configuration mineure de macros. J'utilise des choses comme

#define IBM_AS_CLIENT
#ifdef IBM_AS_CLIENT 
  #define SOME_VALUE1 X
  #define SOME_VALUE2 Y
#else
  #define SOME_VALUE1 P
  #define SOME_VALUE2 Q
#endif

Ce type de fonctionnalité n'est pas si facilement possible sans macros. Les macros sont en fait un excellent outil de gestion de la configuration logicielle et pas seulement un moyen de créer des raccourcis pour la réutilisation du code. La définition de fonctions à des fins de réutilisation dans les macros peut certainement créer des problèmes.


Définir des valeurs de macro sur la ligne de commande lors de la compilation pour créer deux variantes à partir d'une base de code est vraiment bien. avec moderation.
kevinf

1
D'un certain point de vue, cet usage est le plus dangereux: les outils (IDE, analyseurs statiques, refactoring) auront du mal à trouver les chemins de code possibles.
erenon

1

Je pense que le problème est que les macros ne sont pas bien optimisées par le compilateur et sont "moche" à lire et à déboguer.

Les fonctions génériques et / ou les fonctions en ligne constituent souvent de bonnes alternatives.


2
Qu'est-ce qui vous porte à croire que les macros ne sont pas bien optimisées? Il s'agit d'une simple substitution de texte, et le résultat est optimisé tout autant que du code écrit sans macros.
Ben Voigt

@BenVoigt mais ils ne considèrent pas la sémantique et cela peut conduire à quelque chose qui peut être considéré comme "non optimal" ... au moins c'est ma première réflexion sur ce stackoverflow.com/a/14041502/1849534
user1849534

1
@ user1849534: Ce n'est pas ce que signifie le mot "optimisé" dans le contexte de la compilation.
Ben Voigt

1
@BenVoigt Exactement, les macros ne sont que des substitutions de texte. Le compilateur ne fait que dupliquer le code, ce n'est pas un problème de performances mais peut augmenter la taille du programme. Particulièrement vrai dans certains contextes où vous avez des limitations de taille de programme. Certains codes sont tellement remplis de macros que la taille du programme est double.
Davide Icardi

1

Les macros de préprocesseur ne sont pas mauvaises lorsqu'elles sont utilisées à des fins prévues telles que:

  • Création de différentes versions du même logiciel en utilisant des constructions de type #ifdef, par exemple la publication de fenêtres pour différentes régions.
  • Pour définir les valeurs liées aux tests de code.

Alternatives - On peut utiliser une sorte de fichiers de configuration au format ini, xml, json à des fins similaires. Mais leur utilisation aura des effets d'exécution sur le code qu'une macro de préprocesseur peut éviter.


1
depuis C ++ 17 constexpr if + un fichier d'en-tête contenant des variables constexpr "config" peut remplacer les # ifdef.
Enhex
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.