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

J’ai toujours demandé cela mais je n’ai jamais reçu de réponse vraiment bonne. Je pense que presque tous les programmeurs avant même d’écrire le premier “Hello World” avaient rencontré une phrase comme “la macro ne devrait jamais être utilisée”, “la macro est mauvaise” et ainsi de suite, ma question est: pourquoi? Avec le nouveau C ++ 11, existe-t-il une véritable 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, elles ont de graves défauts comme #pragma once qui est sujet aux erreurs dans au moins deux situations importantes: même nom dans différents chemins configurations réseau et systèmes de fichiers.

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

Les macros sont comme n’importe quel autre outil – un marteau utilisé dans un meurtre n’est pas un mal parce que c’est un marteau. C’est le mal dans la façon 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 aux macros qui les rendent “mauvais” (je les développerai plus tard et proposerai des alternatives):

  1. Vous ne pouvez pas déboguer les macros.
  2. L’expansion de la macro 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 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 élargissons 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” à quoi la macro se traduit. Donc, vous ne savez pas réellement ce qui se passe.

Remplacement : Utilisez enum ou const T

Pour les macros “de type fonction”, parce que le débogueur fonctionne au niveau “par ligne source où vous êtes”, votre macro agira comme une seule instruction, peu importe qu’il s’agisse d’une instruction ou d’une centaine. Il est difficile de comprendre ce qui se passe.

Remplacement : Utilisez les fonctions – en ligne si cela doit être “rapide” (mais attention, trop d’inline n’est pas une bonne chose)

2) Les extensions de macro 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++); , qui, même si c’était du 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; 

et alors

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

En fait, cela devient complètement la mauvaise chose ….

Remplacement : fonctions réelles.

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 commence par:

 std::vector v; ... stuff is loaded into v ... for (std::vector::iterator it = myvector.begin() ; it != myvector.end(); ++it) std::cout << ' ' << *it; 

À votre avis, quel message d'erreur obtenez-vous, et où recherchez-vous une erreur [en supposant que vous avez complètement oublié - ou même ignoré - 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 avez inclus 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 beaucoup de remplacement en tant que "règle" - n'utilisez que des noms de majuscules pour les macros, et n'utilisez jamais tous les noms de 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 passez x in en argument.

Il y a des moments où l'utilisation de macros est définitivement 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_malloc comme le malloc normal dans le code, mais il a des arguments supplémentaires, donc à la fin et nous analysons le "quels éléments de la mémoire n'ont pas été libérés", nous pouvons imprimer où l'allocation a été faite le programmeur peut détecter la fuite.

[1] Il s'agit d'un comportement non défini pour mettre à jour une variable plus d'une fois "dans un sharepoint séquence". Un sharepoint séquence n'est pas exactement la même chose qu'un énoncé, mais pour l'essentiel, c'est ce que nous devrions considérer comme tel. Faire x++ * x++ mettra donc x jour deux fois, ce qui est indéfini et entraînera probablement différentes valeurs sur différents systèmes, ainsi que des valeurs de résultat différentes dans x .

Le dicton “macros are evil” fait généralement référence à l’utilisation de #define, pas de #pragma.

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

  • définir des nombres magiques comme des macros

  • utiliser des 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 certains des problèmes introduits en définissant des nombres magiques comme des macros et des expressions de remplacement avec 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 induire en erreur les utilisateurs qui déboguent le code.

  • Lors de la définition des macros à la place des fonctions, les programmeurs utilisant ce code s’attendent à ce qu’ils fonctionnent comme des fonctions et non.

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 attendriez à 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 exécute:

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

De plus, 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 #include dans le code ci-dessous, sauf si vous écrivez explicitement:

 #ifdef max #undef max #endif #include  

Avoir des macros à la place de variables / fonctions signifie également que vous ne pouvez pas prendre leur adresse:

  • si une macro en constante évalue un nombre magique, vous ne pouvez pas le transmettre 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: Par exemple, la bonne alternative à la #define max ci-dessus:

 template 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 des arguments sont différents, la version du modèle vous oblige à être explicite (ce qui conduit à un code plus sûr et plus explicite):

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

Si ce max est défini en tant que macro, le code sera compilé (avec un avertissement).

Si ce max est défini comme une fonction de modèle, le compilateur indiquera l’ambiguïté et vous devrez dire soit max(a, b) ou max(a, b) (et donc explicitement votre intention ).

Un problème commun 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 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) 

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

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

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

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

edit: pourquoi la macro est mauvaise:

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

 #define MULTIPLY(a,b) a*b 

et ensuite appeler

 MULTIPLY(2+3,4+5) 

qui se développe dans

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

Pour avoir ce dernier, vous devez définir:

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

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

Les fonctions génériques et / ou les fonctions intégrées sont souvent une bonne alternative.

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

Ils sont un concept de (méta) langage trouvé dans c / c ++ et, comme tout autre outil, ils peuvent vous simplifier 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. Sur le plan positif, ils peuvent vous aider à garder votre code propre et vous faire économiser beaucoup de saisie si vous l’utilisez correctement. Cela dépend donc de vos préférences personnelles.

Les macros en C / C ++ peuvent constituer un outil important pour le contrôle de version. Le même code peut être dé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 non simplement un moyen de créer des raccourcis pour la réutilisation du code. Définir des fonctions dans le but de les réutiliser dans les macros peut définitivement créer des problèmes.