Macro vs funzione in C


101

Ho sempre visto esempi e casi in cui usare una macro è meglio che usare la funzione.

Qualcuno potrebbe spiegarmi con un esempio lo svantaggio di una macro rispetto ad una funzione?


21
Capovolgi la domanda. In quale situazione è migliore una macro? Usa una funzione reale a meno che tu non possa dimostrare che una macro è migliore.
David Heffernan

Risposte:


114

Le macro sono soggette a errori perché si basano sulla sostituzione testuale e non eseguono il controllo del tipo. Ad esempio, questa macro:

#define square(a) a * a

funziona bene se usato con un numero intero:

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

ma fa cose molto strane quando viene usato con le espressioni:

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

Mettere le parentesi attorno agli argomenti aiuta ma non elimina completamente questi problemi.

Quando le macro contengono più istruzioni, puoi avere problemi con i costrutti del flusso di controllo:

#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 strategia usuale per risolvere questo problema è mettere le istruzioni all'interno di un ciclo "do {...} while (0)".

Se hai due strutture che contengono un campo con lo stesso nome ma semantica diversa, la stessa macro potrebbe funzionare su entrambe, con strani risultati:

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

Infine, le macro possono essere difficili da eseguire il debug, producendo strani errori di sintassi o errori di runtime che devi espandere per comprendere (ad esempio con gcc -E), perché i debugger non possono passare da una macro all'altra, come in questo esempio:

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

Le funzioni e le costanti inline aiutano a evitare molti di questi problemi con le macro, ma non sono sempre applicabili. Quando le macro vengono utilizzate deliberatamente per specificare il comportamento polimorfico, può essere difficile evitare il polimorfismo involontario. Il C ++ ha una serie di caratteristiche come i modelli per aiutare a creare costrutti polimorfici complessi in modo tipicamente sicuro senza l'uso di macro; vedere Il linguaggio di programmazione C ++ di Stroustrup per i dettagli.


43
Che cos'è la pubblicità in C ++?
Pacerier,

4
D'accordo, questa è una domanda C, non c'è bisogno di aggiungere pregiudizi.
ideasman42

16
C ++ è un'estensione di C che aggiunge (tra le altre cose) funzionalità intese ad affrontare questa specifica limitazione di C. Non sono un fan di C ++, ma penso che sia in tema qui.
D Coetzee

1
Macro, funzioni inline e modelli vengono spesso utilizzati nel tentativo di migliorare le prestazioni. Sono abusati e tendono a danneggiare le prestazioni a causa del sovraccarico del codice, che riduce l'efficacia della cache delle istruzioni della CPU. Possiamo creare strutture dati generiche veloci in C senza utilizzare queste tecniche.
Sam Watkins

1
Secondo la ISO / IEC 9899: 1999 §6.5.1, "Tra il punto della sequenza precedente e successivo un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione." (Una formulazione simile esiste negli standard C precedenti e successivi.) Quindi x++*x++non si può dire che l'espressione incrementa xdue volte; in realtà invoca un comportamento indefinito , il che significa che il compilatore è libero di fare tutto ciò che vuole: può incrementare xdue volte, o una volta, o per niente; potrebbe abortire con un errore o persino far volare i demoni fuori dal naso .
Psychonaut

40

Funzionalità macro :

  • La macro è preelaborata
  • Nessun controllo del tipo
  • La lunghezza del codice aumenta
  • L'uso della macro può portare a effetti collaterali
  • La velocità di esecuzione è più veloce
  • Prima che il nome della macro della compilazione venga sostituito dal valore della macro
  • Utile quando il codice piccolo appare molte volte
  • La macro non controlla gli errori di compilazione

Caratteristiche della funzione :

  • La funzione è compilata
  • Il controllo del tipo è stato eseguito
  • La lunghezza del codice rimane la stessa
  • Nessun effetto collaterale
  • La velocità di esecuzione è più lenta
  • Durante la chiamata di funzione, ha luogo il trasferimento del controllo
  • Utile quando il codice di grandi dimensioni appare molte volte
  • Errori di compilazione dei controlli di funzione

2
"la velocità di esecuzione è più veloce" riferimento richiesto. Qualsiasi compilatore anche un po 'competente dell'ultimo decennio funzionerà perfettamente se pensa che fornirà un vantaggio in termini di prestazioni.
Voo

1
Non è che, nel contesto dell'elaborazione MCU di basso livello (AVR, cioè ATMega32), le macro sono una scelta migliore, poiché non aumentano lo stack di chiamate, come fanno le chiamate di funzione?
hardyVeles

1
@hardyVeles Non così. I compilatori, anche per un AVR, possono incorporare codice in modo molto intelligente. Ecco un esempio: godbolt.org/z/Ic21iM
Edward

34

Gli effetti collaterali sono importanti. Ecco un caso tipico:

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

min(x++, y)

viene espanso a:

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

xviene incrementato due volte nella stessa istruzione. (e comportamento indefinito)


Anche la scrittura di macro su più righe è un problema:

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

Richiedono un \alla fine di ogni riga.


Le macro non possono "restituire" nulla a meno che tu non ne faccia una singola espressione:

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

Non puoi farlo in una macro a meno che non usi l'istruzione di espressione di GCC. (EDIT: Puoi usare un operatore virgola anche se ... trascurato che ... Ma potrebbe essere ancora meno leggibile.)


Ordine delle operazioni: (per gentile concessione di @ouah)

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

min(x & 0xFF, 42)

viene espanso a:

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

Ma &ha una precedenza inferiore a <. Quindi 0xFF < 42viene valutato prima.


5
e non mettere parentesi con argomenti macro nella definizione macro può portare a problemi di precedenza: ad esempio,min(a & 0xFF, 42)
ouah

Ah sì. Non ho visto il tuo commento mentre aggiornavo il post. Immagino che menzionerò anche questo.
Mysticial

14

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

mentre:

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

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

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

Rispetto a:

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

13

In caso di dubbio, utilizzare le funzioni (o le funzioni inline).

Tuttavia le risposte qui spiegano principalmente i problemi con le macro, invece di avere una semplice visione che le macro sono dannose perché sono possibili incidenti stupidi.
Puoi essere consapevole delle insidie ​​e imparare a evitarle. Quindi usa le macro solo quando c'è una buona ragione per farlo.

Ci sono alcuni casi eccezionali in cui ci sono vantaggi nell'utilizzo di macro, questi includono:

  • Funzioni generiche, come indicato di seguito, è possibile avere una macro che può essere utilizzata su diversi tipi di argomenti di input.
  • Il numero variabile di argomenti può essere mappato a funzioni diverse invece di utilizzare C's va_args.
    ad esempio: https://stackoverflow.com/a/24837037/432509 .
  • Essi possono opzionalmente includere informazioni locali, come ad esempio stringhe di debug:
    ( __FILE__, __LINE__, __func__). controllare le condizioni pre / post, assertin caso di errore o anche statiche-asserisce in modo che il codice non venga compilato in caso di uso improprio (utile soprattutto per le build di debug).
  • Ispeziona gli argomenti di input, puoi eseguire test sugli argomenti di input come controllarne il tipo, la dimensione, controllare che i structmembri siano presenti prima del casting
    (può essere utile per i tipi polimorfici) .
    Oppure controlla che un array soddisfi una condizione di lunghezza.
    vedere: https://stackoverflow.com/a/29926435/432509
  • Mentre è noto che le funzioni eseguono il controllo del tipo, C forzerà anche i valori (int / float per esempio). In rari casi questo può essere problematico. È possibile scrivere macro più precise di una funzione sugli argomenti di input. vedere: https://stackoverflow.com/a/25988779/432509
  • Il loro uso come wrapper per le funzioni, in alcuni casi potresti voler evitare di ripeterti, ad esempio ... func(FOO, "FOO");, potresti definire una macro che espande la stringa per tefunc_wrapper(FOO);
  • Quando si desidera manipolare le variabili nell'ambito locale del chiamante, il passaggio del puntatore a un puntatore funziona normalmente, ma in alcuni casi è meno problematico utilizzare una macro.
    (l'assegnazione a più variabili, per operazioni per pixel, è un esempio in cui potresti preferire una macro a una funzione ... sebbene dipenda ancora molto dal contesto, poiché le inlinefunzioni potrebbero essere un'opzione) .

Certo, alcuni di questi si basano su estensioni del compilatore che non sono standard C. Significa che potresti ritrovarti con codice meno portabile, o doverlo inserire ifdef, quindi vengono sfruttati solo quando il compilatore supporta.


Evitare la creazione di istanze di più argomenti

Notando questo poiché è una delle cause più comuni di errori nelle macro (passando x++ad esempio, dove una macro può aumentare più volte) .

è possibile scrivere macro che evitano effetti collaterali con più istanze di argomenti.

C11 Generic

Se ti piace avere squaremacro che funzioni con vari tipi e avere il supporto C11, potresti farlo ...

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

Espressioni di istruzioni

Questa è un'estensione del compilatore supportata da GCC, Clang, EKOPath e Intel C ++ (ma non MSVC) ;

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

Quindi lo svantaggio delle macro è che devi sapere come usarle per cominciare e che non sono supportate così ampiamente.

Un vantaggio è che, in questo caso, puoi utilizzare la stessa squarefunzione per molti tipi diversi.


1
"... supportato ampiamente .." Scommetto che l'espressione dell'istruzione che hai citato non è supportata da cl.exe? (MS's Compiler)
gideon

1
@gideon, risposta corretta, anche se per ogni caratteristica menzionata, non sono sicuro che sia necessario avere una matrice di supporto delle funzionalità del compilatore.
ideasman42

12

Nessun controllo del tipo di parametri e codice viene ripetuto, il che può causare un aumento del codice. La sintassi della macro può anche portare a un numero qualsiasi di casi limite strani in cui i punti e virgola o l'ordine di precedenza possono intralciare. Ecco un link che dimostra alcune macro male


6

uno svantaggio delle macro è che i debugger leggono il codice sorgente, che non ha macro espanse, quindi eseguire un debugger in una macro non è necessariamente utile. Inutile dire che non puoi impostare un punto di interruzione all'interno di una macro come puoi fare con le funzioni.


Il punto di interruzione è un affare molto importante qui, grazie per averlo sottolineato.
Hans

6

Le funzioni eseguono il controllo del tipo. Questo ti dà un ulteriore livello di sicurezza.


6

Aggiungendo a questa risposta ..

Le macro vengono sostituite direttamente nel programma dal preprocessore (dato che fondamentalmente sono direttive del preprocessore). Quindi usano inevitabilmente più spazio di memoria di una rispettiva funzione. D'altra parte, una funzione richiede più tempo per essere chiamata e restituire risultati e questo sovraccarico può essere evitato utilizzando le macro.

Anche le macro hanno alcuni strumenti speciali che possono aiutare con la portabilità del programma su piattaforme diverse.

A differenza delle funzioni, non è necessario assegnare alle macro un tipo di dati per i loro argomenti.

Nel complesso sono uno strumento utile nella programmazione. E sia le macroistruzioni che le funzioni possono essere utilizzate a seconda delle circostanze.


3

Non ho notato, nelle risposte sopra, un vantaggio delle funzioni rispetto alle macro che penso sia molto importante:

Le funzioni possono essere passate come argomenti, le macro no.

Esempio concreto: si desidera scrivere una versione alternativa della funzione standard 'strpbrk' che accetti, piuttosto che un elenco esplicito di caratteri da cercare all'interno di un'altra stringa, una funzione (puntatore a una) che restituirà 0 finché un carattere non è ha rilevato che supera alcuni test (definito dall'utente). Uno dei motivi per cui potresti volerlo fare è che puoi sfruttare altre funzioni di libreria standard: invece di fornire una stringa esplicita piena di punteggiatura, potresti invece passare "ispunct" a ctype.h, ecc. Se "ispunct" è stato implementato solo come una macro, questo non funzionerebbe.

Ci sono molti altri esempi. Ad esempio, se il confronto viene eseguito da macro anziché da funzione, non è possibile passarlo a "qsort" di stdlib.h.

Una situazione analoga in Python è "print" nella versione 2 rispetto alla versione 3 (istruzione non passabile contro funzione passabile).


1
Grazie per questa risposta
Kyrol

1

Se passi la funzione come argomento alla macro, verrà valutata ogni volta. Ad esempio, se chiami una delle macro più popolari:

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

come quello

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

functionThatTakeLongTime verrà valutata 5 volte, il che può far diminuire in modo significativo le prestazioni

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.