Macro vs Funzione in C

Ho sempre visto esempi e casi in cui l’utilizzo di una macro è migliore rispetto all’utilizzo della funzione.

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

Le macro sono soggette a errori perché si basano sulla sostituzione testuale e non eseguono il controllo dei tipi. 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 se usato con 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 stream di controllo:

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

La solita strategia per risolvere questo problema consiste nel 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 entrambi, con risultati strani:

 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 syntax o errori di runtime che è necessario espandere per comprendere (ad esempio con gcc -E), perché i debugger non possono passare attraverso le macro, come in questo esempio:

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

Le funzioni e le costanti integrate aiutano a evitare molti di questi problemi con le macro, ma non sono sempre applicabili. Dove le macro vengono utilizzate deliberatamente per specificare il comportamento polimorfico, il polimorfismo non intenzionale può essere difficile da evitare. C ++ ha una serie di funzioni 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.

Gli effetti collaterali sono grandi. Ecco un caso tipico:

 #define min(a,b) (a < b ? a : b) min(x++,y) 

si espande a:

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

x viene incrementato due volte nella stessa dichiarazione. (e comportamento non definito)


Anche scrivere macro multi-linea è 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 non si faccia una singola espressione:

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

Non è ansible farlo in una macro a meno che non si usi la dichiarazione di espressione di GCC. (MODIFICA: puoi usare un operatore virgola però ... 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) 

si espande a:

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

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

Funzioni macro :

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

Caratteristiche funzionali :

  • La funzione è compilata
  • Digitare il controllo è fatto
  • La lunghezza del codice rimane uguale
  • Nessun effetto collaterale
  • La velocità di esecuzione è più lenta
  • Durante la chiamata di funzione, ha luogo il trasferimento del controllo
  • Utile dove il codice grande appare molte volte
  • Controllo funzioni Errori di compilazione

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

Non viene ripetuto alcun tipo di controllo dei parametri e del codice che può portare a un aumento di codice. La syntax della macro può anche portare a un numero qualsiasi di casi di edge strani in cui i punti e virgola o l’ordine di precedenza possono interferire. Ecco un link che mostra alcuni macro cattivi

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

Tuttavia, le risposte qui spiegano principalmente i problemi con le macro, invece di avere una visione semplice del fatto che le macro sono malvagie perché sono possibili incidenti sciocchi.
Puoi essere consapevole delle insidie ​​e imparare a evitarli. Quindi utilizzare le macro solo quando c’è una buona ragione per.

Esistono alcuni casi eccezionali in cui sono presenti vantaggi nell’utilizzo delle macro, tra cui:

  • Funzioni generiche, come indicato di seguito, è ansible avere una macro che può essere utilizzata su diversi tipi di argomenti di input.
  • Il numero variabile di argomenti può essere va_args a diverse funzioni invece di utilizzare va_args di va_args
    ad esempio: https://stackoverflow.com/a/24837037/432509 .
  • Possono facoltativamente includere informazioni locali, come le stringhe di debug:
    ( __FILE__ , __LINE__ , __func__ ). controlla le condizioni pre / post, assert in caso di fallimento o anche le assert statiche, quindi il codice non verrà compilato in caso di uso improprio (utile soprattutto per le build di debug).
  • Ispeziona argomenti di input, puoi fare test su argomenti di input come controllare il loro tipo, sizeof, controllare che i membri della struct siano presenti prima del casting
    (può essere utile per i tipi polimorfici) .
    Oppure controlla che un array soddisfi alcune condizioni di lunghezza.
    vedere: https://stackoverflow.com/a/29926435/432509
  • Benché abbia notato che le funzioni eseguono il controllo del tipo, C costringerà anche i valori (ints / floats per esempio). In rari casi questo può essere problematico. È ansible scrivere macro che sono più esigenti di una funzione sui loro argomenti di input. vedere: https://stackoverflow.com/a/25988779/432509
  • Il loro uso come wrapper per le funzioni, in alcuni casi si potrebbe voler evitare di ripetere te stesso, ad esempio … func(FOO, "FOO"); , potresti definire una macro che espande la stringa per te func_wrapper(FOO);
  • Quando si desidera manipolare le variabili nell’oscilloscopio locale dei chiamanti, passare il puntatore a un puntatore funziona normalmente bene, ma in alcuni casi è meno difficile utilizzare ancora una macro.
    (assegnazioni a più variabili, per operazioni per pixel, è un esempio che potresti preferire una macro su una funzione … sebbene dipenda ancora molto dal contesto, poiché le funzioni inline possono essere un’opzione) .

Certo, alcuni di questi si basano su estensioni del compilatore che non sono standard C. Il che significa che si può finire con un codice meno portabile, o ifdef , in modo che vengano sfruttati solo quando il compilatore supporta.


Evitare l’istanza di più argomenti

Notando questo dato che è una delle cause più comuni di errori nelle macro (passando in x++ per esempio, dove una macro può incrementare più volte) .

è ansible scrivere macro che evitino effetti collaterali con più istanze di argomenti.

C11 generico

Se ti piace avere una macro square che funziona con vari tipi e avere il supporto per C11, puoi 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 dichiarazione

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 con i macro è che devi sapere come usarli all’inizio e che non sono supportati come ampiamente.

Un vantaggio è, in questo caso, è ansible utilizzare la stessa funzione square per molti tipi diversi.

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 è ansible impostare un punto di interruzione all’interno di una macro come è ansible con le funzioni.

Aggiungendo a questa risposta ..

Le macro sono sostituite direttamente nel programma dal preprocessore (poiché sono fondamentalmente 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 per restituire risultati, e questo overhead può essere evitato usando macro.

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

Ai macro non è necessario assegnare un tipo di dati per i loro argomenti in contrasto con le funzioni.

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

Le funzioni eseguono il controllo dei tipi. Questo ti dà un ulteriore livello di sicurezza.

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

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

Esempio concreto: vuoi scrivere una versione alternativa della funzione standard “strpbrk” che accetterà, piuttosto che un elenco esplicito di caratteri da cercare all’interno di un’altra stringa, una funzione (puntatore a a) che restituirà 0 fino a quando un personaggio non sarà trovato che passa qualche test (definito dall’utente). Un motivo per cui potresti voler fare questo è che puoi sfruttare altre funzioni di libreria standard: invece di fornire una stringa esplicita piena di punteggiatura, potresti passare invece ‘ispunct’ di ctype.h, ecc. Se ‘ispunct’ è implementato solo come una macro, questo non funzionerebbe.

Ci sono molti altri esempi. Ad esempio, se il confronto viene eseguito mediante macro piuttosto che funzione, non è ansible passarlo a “qsort” di stdlib.h.

Una situazione analoga in Python è “stampa” nella versione 2 rispetto alla versione 3 (istruzione non passabile e funzione passabile).