web-dev-qa-db-fra.com

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

J'ai toujours posé cette question mais je n'ai jamais reçu de très bonne réponse; Je pense que presque n'importe quel programmeur avant même d'écrire le premier "Hello World" avait rencontré une phrase comme "les macros ne devraient jamais être utilisées", "les macros sont mauvaises" 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 la plus simple 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 comme #pragma once qui est sujet aux erreurs dans au moins 2 situations importantes: 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?

81
user1849534

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

Il y a quelques aspects des 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 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 se heurte à 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 que vous ne réalisez pas.

Développons donc 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. Vous ne savez donc pas vraiment ce qui se passe.

remplacement: utilisez enum ou const T

Pour les macros "fonctionnelles", car le débogueur fonctionne au niveau "par ligne source où vous êtes", votre macro se comportera comme une seule instruction, que ce soit une instruction ou 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 en ligne n'est pas une bonne chose)

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

Le plus connu 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 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 "sinon" 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...");

Cela devient en fait complètement la mauvaise chose ...

Remplacement: fonctions réelles.

) 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 que vous obtenez, et où recherchez-vous une erreur [en supposant que vous avez complètement oublié - ou ne connaissiez même pas - la macro de début qui réside dans un fichier d'en-tête que quelqu'un d'autre a écrit? [et encore plus amusant si vous incluez cette macro avant l'inclusion - vous vous noyeriez dans d'étranges erreurs qui n'ont absolument aucun sens lorsque vous regardez le code lui-même.

Remplacement: Eh bien, il n'y a pas autant de remplacement qu'une "règle" - utilisez uniquement des noms en majuscules pour les macros, et n'utilisez jamais tous les noms en majuscules pour autre chose.

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 en argument.

Il y a des moments où l'utilisation de macros est certainement bénéfique. Un exemple est d'encapsuler 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 malloc normal dans le code, mais il a des arguments supplémentaires, donc quand il s'agit de la fin et que nous analysons "quels éléments de mémoire n'ont pas été libérés", nous pouvons imprimer où l'allocation a été faite pour que le programmeur puisse localiser la fuite.

[1] Il n'est pas dé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 le même qu'une déclaration, mais pour la plupart des intentions, c'est ce que nous devrions considérer comme. Ainsi, 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 à des valeurs de résultat différentes dans x également.

141
Mats Petersson

Le dicton "les macros sont mauvaises" 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éfinition de nombres magiques comme 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 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 (et des erreurs) de compilation et perturber le débogage du code.

  • lors de la définition de macros au lieu de fonctions, les programmeurs utilisant ce code s'attendent à ce qu'ils fonctionnent comme des fonctions et ils ne le font pas.

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

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 <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 en un nombre magique, vous ne pouvez pas la 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, l'alternative correcte au #define max Ci-dessus:

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 des 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 sera compilé (avec un avertissement).

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

21
utnapistim

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 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)
11
phaazon

Les macros sont particulièrement utiles pour créer du code générique (les paramètres des macros 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)

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

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

edit: quant à la raison pour laquelle la macro est mauvaise:

1) pas de vérification de type des arguments (ils n'ont pas de type), donc peuvent être facilement mal utilisés 2) se développent parfois en 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 appeler

MULTIPLY(2+3,4+5)

qui se développe dans

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

Pour avoir ce dernier, vous devez définir:

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

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

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

3
Sandi Hrvić

Les macros en C/C++ peuvent servir d'outil important pour le contrôle de version. Le même code peut être fourni à 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éutilisabilité dans les macros peut certainement créer des problèmes.

1
indiangarg

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.

Souvent, de bonnes alternatives sont les fonctions génériques et/ou les fonctions en ligne.

1
Davide Icardi

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

  • Création de différentes versions du même logiciel à l'aide de constructions de type #ifdef, par exemple la publication de fenêtres pour différentes régions.
  • Pour définir des valeurs liées au test 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.

0
indiangarg