Macro vs fonction en C

101

J'ai toujours vu des exemples et des cas où l'utilisation d'une macro est meilleure que l'utilisation d'une fonction.

Quelqu'un pourrait-il m'expliquer avec un exemple l'inconvénient d'une macro par rapport à une fonction?

Kyrol
la source
21
Tournez la question sur sa tête. Dans quelle situation une macro est-elle meilleure? Utilisez une fonction réelle, sauf si vous pouvez démontrer qu'une macro est meilleure.
David Heffernan

Réponses:

114

Les macros sont sujettes aux erreurs car elles reposent sur la substitution textuelle et n'effectuent pas de vérification de type. Par exemple, cette macro:

#define square(a) a * a

fonctionne bien lorsqu'il est utilisé avec un entier:

square(5) --> 5 * 5 --> 25

mais fait des choses très étranges lorsqu'il est utilisé avec des expressions:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

Mettre des parenthèses autour des arguments aide mais n'élimine pas complètement ces problèmes.

Lorsque les macros contiennent plusieurs instructions, vous pouvez avoir des problèmes avec les constructions de flux de contrôle:

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

La stratégie habituelle pour résoudre ce problème est de placer les instructions dans une boucle "do {...} while (0)".

Si vous avez deux structures qui contiennent un champ avec le même nom mais une sémantique différente, la même macro peut fonctionner sur les deux, avec des résultats étranges:

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

Enfin, les macros peuvent être difficiles à déboguer, produisant des erreurs de syntaxe étranges ou des erreurs d'exécution que vous devez développer pour comprendre (par exemple avec gcc -E), car les débogueurs ne peuvent pas parcourir les macros, comme dans cet exemple:

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

Les fonctions et les constantes en ligne permettent d'éviter beaucoup de ces problèmes avec les macros, mais ne sont pas toujours applicables. Lorsque des macros sont délibérément utilisées pour spécifier un comportement polymorphe, le polymorphisme involontaire peut être difficile à éviter. C ++ a un certain nombre de fonctionnalités telles que des modèles pour aider à créer des constructions polymorphes complexes d'une manière sécurisée sans l'utilisation de macros; voir le langage de programmation C ++ de Stroustrup pour plus de détails.

D Coetzee
la source
43
Qu'est-ce que la publicité C ++?
Pacerier le
4
D'accord, c'est une question C, pas besoin d'ajouter de biais.
ideasman42
16
C ++ est une extension de C qui ajoute (entre autres) des fonctionnalités destinées à résoudre cette limitation spécifique de C. Je ne suis pas fan de C ++, mais je pense que c'est sur le sujet ici.
D Coetzee
1
Les macros, les fonctions en ligne et les modèles sont souvent utilisés pour tenter d'améliorer les performances. Ils sont surutilisés et ont tendance à nuire aux performances en raison de la surcharge du code, ce qui réduit l'efficacité du cache d'instructions du processeur. Nous pouvons créer des structures de données génériques rapides en C sans utiliser ces techniques.
Sam Watkins
1
Selon la norme ISO / CEI 9899: 1999 §6.5.1, "Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression." (Une formulation similaire existe dans les normes C précédentes et suivantes.) On x++*x++ne peut donc pas dire que l'expression s'incrémente xdeux fois; il invoque en fait un comportement indéfini , ce qui signifie que le compilateur est libre de faire tout ce qu'il veut - il peut s'incrémenter xdeux fois, une fois ou pas du tout; il pourrait avorter avec une erreur ou même faire voler des démons hors de votre nez .
Psychonaut
40

Caractéristiques macro :

  • La macro est prétraitée
  • Aucune vérification de type
  • Augmentation de la longueur du code
  • L'utilisation de macro peut entraîner des effets secondaires
  • La vitesse d'exécution est plus rapide
  • Avant la compilation, le nom de la macro est remplacé par la valeur de la macro
  • Utile là où le petit code apparaît plusieurs fois
  • La macro ne vérifie pas les erreurs de compilation

Caractéristiques de la fonction :

  • La fonction est compilée
  • La vérification de type est terminée
  • La longueur du code reste la même
  • Aucun effet secondaire
  • La vitesse d'exécution est plus lente
  • Pendant l'appel de fonction, le transfert de contrôle a lieu
  • Utile lorsque du code volumineux apparaît plusieurs fois
  • Vérifie les fonctions Compile les erreurs
zangw
la source
2
référence "la vitesse d'exécution est plus rapide" requise. Tout compilateur, même quelque peu compétent, de la dernière décennie fonctionnera parfaitement s'il pense que cela offrira un avantage en termes de performances.
Voo
1
N'est-ce pas, dans le contexte de l'informatique MCU de bas niveau (AVR, c'est-à-dire ATMega32), les macros sont un meilleur choix, car elles n'augmentent pas la pile d'appels, comme le font les appels de fonction?
hardyVeles
1
@hardyVeles Pas si. Les compilateurs, même pour un AVR, peuvent coder en ligne très intelligemment. Voici un exemple: godbolt.org/z/Ic21iM
Edward
34

Les effets secondaires sont importants. Voici un cas typique:

#define min(a, b) (a < b ? a : b)

min(x++, y)

est étendu à:

(x++ < y ? x++ : y)

xest incrémenté deux fois dans la même instruction. (et comportement indéfini)


L'écriture de macros multilignes est également une douleur:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

Ils nécessitent un \à la fin de chaque ligne.


Les macros ne peuvent rien "renvoyer" à moins que vous n'en fassiez une seule expression:

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

Vous ne pouvez pas faire cela dans une macro à moins que vous n'utilisiez l'instruction d'expression de GCC. (EDIT: Vous pouvez cependant utiliser un opérateur virgule ... j'ai oublié cela ... Mais cela pourrait encore être moins lisible.)


Ordre des opérations: (avec l'aimable autorisation de @ouah)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

est étendu à:

(x & 0xFF < 42 ? x & 0xFF : 42)

Mais &a une priorité inférieure à <. Donc 0xFF < 42est évalué en premier.

Mysticial
la source
5
et ne pas mettre de parenthèses avec des arguments de macro dans la définition de macro peut entraîner des problèmes de priorité: par exemple,min(a & 0xFF, 42)
ouah
Ah oui. Je n'ai pas vu votre commentaire pendant que je mettais à jour le message. Je suppose que je vais le mentionner aussi.
Mysticial
14

Exemple 1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

tandis que:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

Exemple 2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

Par rapport à:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}
Flexo
la source
13

En cas de doute, utilisez des fonctions (ou des fonctions en ligne).

Cependant, les réponses ici expliquent principalement les problèmes avec les macros, au lieu d'avoir une idée simple que les macros sont mauvaises parce que des accidents stupides sont possibles.
Vous pouvez être conscient des pièges et apprendre à les éviter. Utilisez ensuite les macros uniquement lorsqu'il y a une bonne raison de le faire.

Il existe certains cas exceptionnels où l'utilisation de macros présente des avantages, notamment:

  • Fonctions génériques, comme indiqué ci-dessous, vous pouvez avoir une macro qui peut être utilisée sur différents types d'arguments d'entrée.
  • Nombre variable d'arguments peut mapper à différentes fonctions au lieu d'utiliser C de va_args.
    Par exemple: https://stackoverflow.com/a/24837037/432509 .
  • Ils peuvent éventuellement inclure des informations locales, telles que les chaînes de débogage:
    ( __FILE__, __LINE__, __func__). vérifier les conditions pré / post, asserten cas d'échec ou même d'assertions statiques pour que le code ne se compile pas en cas d'utilisation incorrecte (surtout utile pour les versions de débogage).
  • Inspecter les arguments d'entrée, vous pouvez faire des tests sur les arguments d'entrée tels que vérifier leur type, sizeof, vérifier que les structmembres sont présents avant la conversion
    (peut être utile pour les types polymorphes) .
    Ou vérifiez qu'un tableau remplit une condition de longueur.
    voir: https://stackoverflow.com/a/29926435/432509
  • Bien qu'il soit noté que les fonctions vérifient le type, C contraindra également les valeurs (ints / floats par exemple). Dans de rares cas, cela peut être problématique. Il est possible d'écrire des macros qui sont plus exigeantes qu'une fonction sur leurs arguments d'entrée. voir: https://stackoverflow.com/a/25988779/432509
  • Leur utilisation comme wrappers pour les fonctions, dans certains cas, vous voudrez peut-être éviter de vous répéter, par exemple ... func(FOO, "FOO");, vous pouvez définir une macro qui développe la chaîne pour vousfunc_wrapper(FOO);
  • Lorsque vous souhaitez manipuler des variables dans la portée locale des appelants, passer le pointeur vers un pointeur fonctionne très bien normalement, mais dans certains cas, il est encore moins difficile d'utiliser une macro.
    (les affectations à plusieurs variables, pour une opération par pixel, est un exemple que vous pourriez préférer une macro à une fonction ... même si cela dépend toujours beaucoup du contexte, car les inlinefonctions peuvent être une option) .

Certes, certains d'entre eux reposent sur des extensions de compilateur qui ne sont pas standard C. Cela signifie que vous pouvez vous retrouver avec moins de code portable, ou en avoir besoin ifdef, donc ils ne sont exploités que lorsque le compilateur prend en charge.


Éviter l'instanciation d'arguments multiples

Noter cela car c'est l'une des causes les plus courantes d'erreurs dans les macros (en passant x++par exemple, où une macro peut s'incrémenter plusieurs fois) .

il est possible d'écrire des macros qui évitent les effets secondaires avec plusieurs instanciations d'arguments.

Générique C11

Si vous aimez avoir une squaremacro qui fonctionne avec différents types et qui prend en charge C11, vous pouvez le faire ...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

Expressions de déclaration

Il s'agit d'une extension de compilateur supportée par GCC, Clang, EKOPath et Intel C ++ (mais pas MSVC) ;

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

Donc, l'inconvénient des macros est que vous devez savoir comment les utiliser pour commencer et qu'elles ne sont pas prises en charge aussi largement.

Un avantage est que, dans ce cas, vous pouvez utiliser la même squarefonction pour de nombreux types différents.

ideasman42
la source
1
"... supporté aussi largement ..." Je parie que l'expression de déclaration que vous avez mentionnée n'est pas prise en charge par cl.exe? (MS's Compiler)
gideon
1
@gideon, bonne réponse éditée, mais pour chaque fonctionnalité mentionnée, je ne suis pas sûr qu'il soit nécessaire d'avoir une matrice de prise en charge des fonctionnalités du compilateur.
ideasman42
12

Aucune vérification de type des paramètres et du code n'est répétée, ce qui peut entraîner un gonflement du code. La syntaxe de macro peut également conduire à n'importe quel nombre de cas de bord étranges où les points-virgules ou l'ordre de priorité peuvent gêner. Voici un lien qui démontre un mal macro

Michael Dorgan
la source
6

un inconvénient des macros est que les débogueurs lisent le code source, qui n'a pas de macros développées, donc exécuter un débogueur dans une macro n'est pas nécessairement utile. Inutile de dire que vous ne pouvez pas définir un point d'arrêt dans une macro comme vous le pouvez avec des fonctions.

Jim Mcnamara
la source
Le point d'arrêt est une affaire très importante ici, merci de l'avoir signalé.
Hans
6

Les fonctions vérifient le type. Cela vous donne une couche supplémentaire de sécurité.

ncmathsadist
la source
6

Ajout à cette réponse…

Les macros sont remplacées directement dans le programme par le préprocesseur (car ce sont essentiellement des directives de préprocesseur). Ils utilisent donc inévitablement plus d'espace mémoire qu'une fonction respective. D'autre part, une fonction nécessite plus de temps pour être appelée et pour renvoyer des résultats, et cette surcharge peut être évitée en utilisant des macros.

Les macros ont également des outils spéciaux qui peuvent aider à la portabilité des programmes sur différentes plates-formes.

Les macros n'ont pas besoin de se voir attribuer un type de données pour leurs arguments contrairement aux fonctions.

Dans l'ensemble, ils sont un outil utile dans la programmation. Et les macro-instructions et les fonctions peuvent être utilisées selon les circonstances.

Nikos
la source
3

Je n'ai pas remarqué, dans les réponses ci-dessus, un avantage des fonctions par rapport aux macros que je pense être très important:

Les fonctions peuvent être passées en arguments, les macros ne le peuvent pas.

Exemple concret: vous souhaitez écrire une version alternative de la fonction standard 'strpbrk' qui acceptera, plutôt qu'une liste explicite de caractères à rechercher dans une autre chaîne, une fonction (pointeur vers une) qui renverra 0 jusqu'à ce qu'un caractère soit trouvé qui passe un certain test (défini par l'utilisateur). Une des raisons pour lesquelles vous voudrez peut-être faire cela est de pouvoir exploiter d'autres fonctions de bibliothèque standard: au lieu de fournir une chaîne explicite pleine de ponctuation, vous pouvez passer 'ispunct' de ctype.h à la place, etc. Si 'ispunct' a été implémenté uniquement comme une macro, cela ne fonctionnerait pas.

Il existe de nombreux autres exemples. Par exemple, si votre comparaison est effectuée par macro plutôt que par fonction, vous ne pouvez pas la passer au 'qsort' de stdlib.h.

Une situation analogue en Python est «imprimer» dans la version 2 par rapport à la version 3 (instruction non passable vs fonction passable).

Sean Rostami
la source
1
Merci pour cette réponse
Kyrol
1

Si vous passez la fonction en tant qu'argument à la macro, elle sera évaluée à chaque fois. Par exemple, si vous appelez l'une des macros les plus populaires:

#define MIN(a,b) ((a)<(b) ? (a) : (b))

comme ça

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTime sera évalué 5 fois, ce qui peut réduire considérablement les performances

Safran
la source