112 votes

Macro et fonction en C

Je vois souvent des cas où l'utilisation d'une macro est préférable à l'utilisation d'une fonction.

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

23 votes

Retournez la question dans 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.

121voto

Derrick Coetzee Points 1912

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 nombre 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 consiste à placer les instructions à l'intérieur d'une lignée "do { ... } while (0)".

Si vous avez deux structures qui contiennent un champ portant le même nom mais ayant 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 bizarres ou des erreurs d'exécution que vous devez développer pour les comprendre (par exemple avec gcc -E), car les débogueurs ne peuvent pas passer par 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 constantes en ligne permettent d'éviter bon nombre de ces problèmes avec les macros, mais ne sont pas toujours applicables. Lorsque les macros sont délibérément utilisées pour spécifier un comportement polymorphe, le polymorphisme involontaire peut être difficile à éviter. Le C++ dispose d'un certain nombre de fonctionnalités, telles que les modèles, qui permettent de créer des constructions polymorphes complexes de manière sûre, sans avoir recours aux macros. Le langage de programmation C++ pour les détails.

48 votes

C'est quoi cette publicité pour le C++ ?

4 votes

Je suis d'accord, il s'agit d'une question C, pas besoin d'y ajouter un parti pris.

24 votes

Le C++ est une extension du C qui ajoute (entre autres choses) des fonctionnalités destinées à remédier à cette limitation spécifique du C. Je ne suis pas un fan du C++, mais je pense que c'est le sujet ici.

36voto

Mysticial Points 180300

Les effets secondaires en sont un gros. Voici un cas typique :

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

min(x++, y)

est étendu à :

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

x est incrémenté deux fois dans la même déclaration. (et un comportement indéfini)


L'écriture de macros de plusieurs lignes est également un problème :

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

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


Les macros ne peuvent pas "renvoyer" quoi que ce soit, sauf si vous en faites une expression unique :

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

On ne peut pas le faire dans une macro, à moins d'utiliser la méthode GCC expressions de déclaration . (EDIT : Vous pouvez utiliser un opérateur de virgule cependant... oublié que... Mais cela pourrait quand même être moins lisible).


Ordre des opérations : (courtoisie 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 à celle de < . Donc 0xFF < 42 est évalué en premier.

6 votes

Et le fait de ne pas mettre de parenthèses avec les arguments de la macro dans la définition de la macro peut entraîner des problèmes de précédence : par ex, min(a & 0xFF, 42)

0 votes

Ah oui. Je n'ai pas vu ton commentaire pendant que je mettais à jour le post. Je suppose que je vais le mentionner aussi.

15voto

ideasman42 Points 1682

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

Cependant, les réponses données ici expliquent surtout les problèmes liés aux macros, au lieu de dire simplement que les macros sont mauvaises parce que des accidents stupides sont possibles.
Vous pouvez être conscient des pièges et apprendre à les éviter. N'utilisez ensuite les macros que lorsque vous avez une bonne raison de le faire.

Il y a certains exceptionnel Il existe des cas 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.
  • Un nombre variable d'arguments peut correspondre à différentes fonctions au lieu d'utiliser la fonction C va_args .
    eg : 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, assert en cas d'échec, ou même des affirmations statiques pour que le code ne compile pas en cas de mauvaise utilisation (surtout utile pour les constructions de débogage).
  • Inspecter les args d'entrée, Vous pouvez effectuer des tests sur les args d'entrée tels que la vérification de leur type, sizeof, check struct les membres sont présents avant le casting
    (peut être utile pour les types polymorphes) .
    Ou vérifier qu'un tableau répond à une certaine condition de longueur.
    voir : https://stackoverflow.com/a/29926435/432509
  • Bien qu'il soit noté que les fonctions vérifient les types, le C peut aussi imposer des 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 en ce qui concerne les arguments d'entrée : https://stackoverflow.com/a/25988779/432509
  • Leur utilisation comme enveloppes de fonctions, dans certains cas vous voudrez éviter de vous répéter, par exemple... func(FOO, "FOO"); vous pouvez définir une macro qui développe la chaîne de caractères à votre place. func_wrapper(FOO);
  • Lorsque vous souhaitez manipuler des variables dans la portée locale de l'appelant, le passage d'un pointeur à un pointeur fonctionne normalement très bien, mais dans certains cas, il est plus simple d'utiliser une macro.
    (les assignations à des variables multiples, pour des opérations par pixel, sont un exemple où vous pourriez préférer une macro à une fonction... bien que cela dépende encore beaucoup du contexte, puisque inline peut être une option) .

Il est vrai que certaines d'entre elles s'appuient sur des extensions du compilateur qui ne font pas partie du langage C standard. ifdef de sorte qu'ils ne sont utilisés que lorsque le compilateur les prend en charge.


Éviter l'instanciation d'arguments multiples

Il faut noter que c'est l'une des causes les plus courantes d'erreurs dans les macros. (en passant dans x++ par exemple, lorsqu'une macro peut s'incrémenter plusieurs fois) .

il est possible d'écrire des macros qui évitent les effets secondaires liés à l'instanciation multiple des arguments.

C11 Générique

Si vous aimez avoir square qui fonctionne avec différents types et qui a un support C11, vous pourriez faire ceci...

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

Expression des déclarations

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); })

L'inconvénient des macros est donc que vous devez savoir 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 le même square pour de nombreux types différents.

1 votes

"...soutenu comme largement..." Je parie que l'expression que vous avez mentionnée n'est pas supportée par cl.exe ? (Compilateur de MS)

1 votes

@gideon, bonne réponse éditée, bien que pour chaque fonctionnalité mentionnée, pas sûr qu'il soit nécessaire d'avoir une matrice compilateur-fonctionnalité-support.

14voto

Flexo Points 39273

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

alors 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;
}

12voto

Michael Dorgan Points 7849

Il n'y a pas de vérification du type des paramètres et le code est répété, ce qui peut entraîner un gonflement du code. La syntaxe des macros peut également conduire à un certain nombre de cas limites étranges où les points-virgules ou l'ordre de préséance peuvent s'interposer. Voici un lien qui présente quelques macros maléfique

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