Macro Parametriche in Linguaggio C

Nella lezione precedente abbiamo analizzato le macro semplici e visto il loro meccanismo di funzionamento: la sostituzione testuale.

In linguaggio C, tuttavia, le macro sono in grado anche di accettare parametri in ingresso, proprio come una funzione. Per tal motivo, vengono chiamate macro parametriche o macro funzionali. Attraverso il passaggio di parametri, possiamo creare macro più flessibili e potenti.

In questa lezione vedremo la sintassi delle macro parametriche. Inoltre vedremo quali sono i vantaggi ed anche gli svantaggi del loro utilizzo rispetto alle funzioni.

Macro Parametriche

Nella lezione precedente abbiamo visto la sintassi per definire macro in linguaggio C. Ci siamo soffermati sulle macro semplici e ne abbiamo studiato il funzionamento.

Adesso ci concentriamo sul caso delle macro parametriche, chiamate anche macro funzionali, la cui caratteristica principale è quella di poter accettare dei parametri in ingresso proprio come le funzioni.

La sintassi generale per definire una macro parametrica è riportata di seguito:

Definizione

Macro Parametrica in C

Una Macro Parametrica, in linguaggio C, è una macro che accetta in ingresso dei parametri. La sintassi generale per definire una macro parametrica è la seguente:

#define NOME_MACRO(parametro1, parametro2, ..., parametroN) corpo_macro

Quindi, proprio come le funzioni, una macro parametrica può accettare uno o più parametri separati da virgole. Questi parametri possono essere utilizzati all'interno del corpo della macro per generare del codice personalizzato.

Vediamo nel dettaglio. Quando il preprocessore incontra la definizione di una macro con parametri, analogamente al caso delle macro semplici, esso memorizza il nome della macro e il corpo associato.

Quando, successivamente, la macro viene invocata (in altre parole il preprocessore ne incontra un'occorrenza) nella forma NOME_MACRO(Y1, Y2, ..., YN) il preprocessore sostituisce il nome della macro con il corpo associato sostituendo, inoltre, i parametri formali con i valori effettivi passati in ingresso.

La differenza sostanziale rispetto ad una funzione è che gli argomenti passati ad una macro possono essere token qualsiasi, ossia non devono rispettare una sintassi predefinita e tantomeno essere di un tipo specifico.

Chiariamo con un esempio. Supponiamo di voler realizzare una macro parametrica che ci fornisca il massimo tra due elementi. Possiamo scrivere questa macro così:

#define MAX(x, y) ((x) > (y) ? (x) : (y))

Guardando la definizione della macro potrebbe apparire strano il numero di parentesi presenti. In realtà esiste una motivazione che vedremo dopo. Per ora concentriamoci sull'utilizzo della macro.

Successivamente, dopo averla definita, possiamo usare la macro nel nostro codice in questo modo:

int a = 10;
int b = 20;

int massimo = MAX(a, b);

Quando il preprocessore incontra l'invocazione della macro MAX(a, b) sostituirà il nome della macro con il corpo associato e sostituirà i parametri formali (x) e (y) con i valori effettivi a e b. Quindi, il codice precedente diventerà:

int massimo = ((a) > (b) ? (a) : (b));

Questo esempio mostra un possibile utilizzo delle macro parametriche come semplici funzioni. In questo caso MAX accetta due parametri e restituisce il massimo tra i due.

Nell'esempio, però, ci siamo limitati a passare due interi come parametri. Come già detto, le macro parametriche possono accettare qualsiasi tipo di token. Quindi, possiamo passare anche espressioni più complesse come parametri. Ad esempio:

int massimo = MAX(a + 5, b * 2);

In tal caso il risultato, dopo la sostituzione, sarà:

int massimo = ((a + 5) > (b * 2) ? (a + 5) : (b * 2));

Oppure, possiamo anche passare tipi diversi come parametri:

int massimo = MAX('c', 'b');

Il risultato, dopo la sostituzione, sarà:

int massimo = (('c') > ('b') ? ('c') : ('b'));

In questo caso, la macro restituirà il carattere 'c' poiché il carattere 'c' ha un valore ASCII maggiore di 'b'.

Da tenere presente che la sostituzione della macro non genererà errori fintanto che sul tipo dei parametri passati sia definito l'operatore di confronto >. Per i tipi numerici, int, float e così via, e per i char che abbiamo usato sopra l'operatore > è definito.

Se, invece, si passano due stringhe come parametri, il preprocessore genererà un errore in quanto non è definito l'operatore di confronto tra stringhe.

Tutto ciò deriva dal fatto, come abbiamo spiegato nella lezione precedente, che le macro sono sostituzioni di testo e non funzioni vere e proprie. Quindi, il preprocessore non esegue alcun controllo sui tipi dei parametri passati.

Nota

Attenzione alle parentesi nella definizione delle macro parametriche

Sebbene la sintassi per definire una macro parametrica sia molto simile a quella per definire una funzione esiste un dettaglio a cui prestare la massima attenzione: non bisogna mai inserire spazi tra il nome della macro e la parentesi di sinistra.

Infatti, il codice seguente presenta dei problemi:

/* CODICE ERRATO */
#define SOMMA (x, y) ((x) + (y))

In tal caso, infatti, il precompilatore considererà come corpo della macro la stringa (x, y) ((x) + (y)) e non ((x) + (y)) come ci si aspetterebbe.

La conseguenza di questo errore è che, quando si utilizzerà la macro SOMMA, il precompilatore sostituirà SOMMA(3, 4) con ((3) + (4))(3, 4) e non con ((3) + (4)) come ci si aspetterebbe.

La versione corretta è, invece, la seguente:

/* CODICE CORRETTO */
#define SOMMA(x, y) ((x) + (y))

Si noti bene che, per definire le macro parametriche, non è necessario specificare una lista di parametri. Ad esempio, una definizione del genere è perfettamente legale:

#define STAMPA_MESSAGGIO() (printf("Ciao, come si va?"))

Quando vogliamo richiamare questa macro nel nostro codice, possiamo scrivere semplicemente:

STAMPA_MESSAGGIO();

A prima vista, l'introduzione di una sintassi che permetta di definire una macro parametrica senza parametri può sembrare inutile. Avremmo potuto, ad esempio, usare una macro semplice. In realtà, si tratta di una scelta di consistenza con la sintassi di una funzione; dal punto di vista dell'utilizzatore, rende chiaro il fatto che si tratta di una macro che esegue operazioni, come una funzione, se la sintassi è simile a quella di una funzione.

Definizione

Macro Parametrica senza Parametri

Una macro parametrica può avere la lista dei parametri vuota:

#define NOME_MACRO() corpo_macro

In tal caso, quando si vuole invocare la macro, è comunque necessario inserire le parentesi tonde vuote:

NOME_MACRO();

Esempi

Vediamo qualche esempio di macro parametrica per fissare meglio i concetti.

Supponiamo di voler definire una macro per verificare se un numero è pari. Possiamo scrivere la macro in questo modo:

#define NUMERO_PARI(x) ((x) % 2 == 0)

In questo caso, la macro restituirà 1 se il numero passato come parametro è pari, 0 altrimenti.

Analogamente, possiamo definire una macro per verificare se un numero è dispari:

#define NUMERO_DISPARI(x) ((x) % 2 != 0)

Possiamo definire una macro per il calcolo del cubo di un numero:

#define CUBO(x) ((x) * (x) * (x))

Infine, possiamo definire una macro per il calcolo della somma dei primi n numeri interi:

#define SOMMA_PRIMI_N(n) ((n) * ((n) + 1) / 2)

Vantaggi delle macro parametriche

Una macro parametrica, abbiamo visto, è molto simile ad una funzione. Quello che ci chiediamo, adesso, è quali sono i vantaggi di utilizzarle rispetto ad una normale funzione?

Vediamo nel dettaglio.

Si possono ottenere delle performance migliori

Generalmente, l'operazione di invocazione di una funzione non è un'operazione a costo zero. Esisteva, e spesso esiste tuttora nei sistemi moderni, un overhead o costo aggiuntivo associato al chiamare una funzione. Senza entrare nel dettaglio tecnico, possiamo dire che, in generale, l'invocazione di una funzione comporta l'allocazione di spazio in memoria per i parametri, il salvataggio dell'indirizzo di ritorno, il salto all'indirizzo della funzione e il ripristino dello stato precedente al termine della funzione.

Tutte queste operazioni, seppur veloci, comportano un costo che, in alcuni casi, può essere evitato. Le macro parametriche, essendo sostituzioni di testo, non comportano alcun costo aggiuntivo. Infatti, il corpo delle macro parametriche viene sostituito direttamente nel punto in cui viene invocata. Il risultato finale è che il codice generato è equivalente a quello che si otterrebbe scrivendo direttamente il codice al posto della macro.

Ritorniamo all'esempio della macro MAX. Il codice che segue:

int massimo1 = MAX(a, b);
int massimo2 = MAX(c, d);
int massimo3 = MAX(e, f);

si trasforma nel codice seguente:

int massimo1 = ((a) > (b) ? (a) : (b));
int massimo2 = ((c) > (d) ? (c) : (d));
int massimo3 = ((e) > (f) ? (e) : (f));

In altre parole, è come se avessimo ripetuto il codice per calcolare il massimo per 3 volte.

Risulta anche vero, però, che i moderni processori e i moderni compilatori sono in grado di ottimizzare le chiamate a funzione in modo tale da ridurre al minimo l'overhead. Quindi, in generale, l'uso delle macro parametriche per ottenere performance migliori è un vantaggio che si è ridotto nel tempo.

Inoltre, nello standard C99, sono state introdotte le cosiddette inline function o funzioni in linea che permettono di ottenere le stesse performance delle macro parametriche con la sintassi di una funzione. Vedremo di cosa si tratta in una lezione successiva.

Le macro sono generiche

Come abbiamo accennato anche sopra, i parametri di una macro non hanno un tipo associato. Il precompilatore, a differenza del caso di una funzione, non controlla se gli argomenti passati alla macro sono di tipo intero, virgola mobile e così via.

Del resto, nemmeno nella definizione di una macro parametrica abbiamo specificato il tipo dei parametri.

La conseguenza di questa caratteristica è che le macro parametriche possono essere utilizzate con qualsiasi tipo di dato. La condizione da rispettare, tuttavia, è che il programma risultante dopo la sostituzione sia sintatticamente corretto.

Ritorniamo ancora all'esempio di MAX. Possiamo utilizzare, come visto sopra, MAX con tipi int, float, double e char:

int massimo = MAX(10, 20);
float massimo_f = MAX(10.5, 20.5);
double massimo_d = MAX(10.5, 20.5);
char massimo_c = MAX('c', 'b');

Questo lo possiamo fare scrivendo una e una sola volta la definizione della macro. Se avessimo voluto utilizzare una funzione al posto della macro, avremmo dovuto scrivere una funzione per ogni tipo di dato:

int massimo_int(int x, int y) {
    return x > y ? x : y;
}

float massimo_float(float x, float y) {
    return x > y ? x : y;
}

double massimo_double(double x, double y) {
    return x > y ? x : y;
}

char massimo_char(char x, char y) {
    return x > y ? x : y;
}

Il vincolo è che dopo la sostituzione testuale della macro, il programma continui ad essere valido. Se avessimo utilizzato due array, ad esempio, con la macro MAX avremmo ottenuto un errore in compilazione. Questo perché l'operatore >, utilizzato nel corpo della macro, non è definito per gli array.

Svantaggi delle macro parametriche

Abbiamo visto i vantaggi delle macro parametriche. Vediamo, adesso, quali sono gli svantaggi principali rispetto alle funzioni.

Il codice generato è di dimensioni maggiori

Invocare una macro, nella pratica, consiste nel copiare il corpo della macro nel punto in cui viene invocata. Questo significa che, se la macro è invocata più volte, il suo corpo verrà copiato e sostituito nel codice più volte.

La conseguenza è che la dimensione del codice finale sarà maggiore rispetto al caso di una funzione. Usando una funzione, invece, il codice della funzione risiede in un unico punto e viene chiamato ogni volta che la funzione viene invocata.

Ovviamente, il problema aumenta all'aumentare del numero di invocazioni di una macro. Ed esplode nel caso in cui usiamo macro innestate.

Ad esempio, possiamo voler trovare il massimo tra tre numeri. Possiamo, in tal caso, usare la stessa macro MAX due volte in maniera innestata:

int massimo = MAX(MAX(a, b), c);

In tal caso il codice diventa:

int massimo = (((a) > (b) ? (a) : (b)) > (c) ? ((a) > (b) ? (a) : (b)) : (c));

Questo problema non riguarda affatto la leggibilità del codice. Dal punto di vista dello sviluppatore, infatti, il codice è molto chiaro e immediato. Il problema è che il codice generato è molto più grande rispetto al caso di una funzione. L'eseguibile finale potrebbe essere più grande in termini di occupazione di memoria.

Vero è, anche, che oggigiorno la memoria di un elaboratore non è più una risorsa così limitata da sfavorire l'uso delle macro parametriche. Tuttavia, specialmente quando si sviluppano programmi per sistemi embedded come micro-controllori, la dimensione del codice è un fattore critico.

I tipi degli argomenti non sono controllati

Sebbene questo aspetto sia stato già trattato nei vantaggi delle macro parametriche, è bene sottolineare che il fatto che i tipi degli argomenti non siano controllati può portare a errori difficili da individuare.

Quando viene invocata una funzione, il compilatore controlla che gli argomenti passati corrispondano al tipo richiesto. Se una funzione richiede un int in ingresso, il compilatore controllerà che l'argomento passato sia effettivamente un int. Nel caso in cui non lo fosse, il compilatore prova a convertire l'argomento passato nel tipo richiesto. Ad esempio, se proviamo a passare un float al posto di un int, il compilatore effettuerà la conversione, ma segnalerà comunque un warning per avvisarci della perdita di precisione. Se la conversione non è possibile, il compilatore segnalerà un errore.

Tutto ciò non accade con i parametri di una macro. Non vengono controllati i tipi nè vengono effettuate conversioni.

Questo può portare a degli errori molto difficili da scovare.

Non esistono puntatori a macro

Vedremo nelle prossime lezioni che è possibile definire puntatori a funzioni in linguaggio C. Questo concetto è molto potente e flessibile.

Dal momento che una macro non è una funzione, ma rappresenta un semplice meccanismo di sostituzione testuale, non è possibile definire un puntatore a macro.

Pertanto, in queste situazioni, si possono usare solo esclusivamente funzioni.

La valutazione degli argomenti di una macro può avvenire più volte

Questo è, forse, l'aspetto più insidioso dell'utilizzo delle macro parametriche.

Per comprendere in cosa consiste questo problema, consideriamo un esempio. Supponiamo di voler implementare una funzione che calcoli il cubo di un numero:

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

Adesso, supponiamo che il codice che invoca la funzione cubo sia fatto in questo modo:

int x = 10;
int y = cubo(x++);

Quello che accade in questo esempio è riportato di seguito:

  1. La variabile x viene inizializzata a 10;
  2. Prima di invocare la funzione cubo, vengono valutati gli argomenti. In questo caso abbiamo un solo argomento x++. L'operatore ++ incrementa il valore di x di 1 e restituisce il valore precedente. Quindi, il valore passato alla funzione cubo è 10, ma x viene incrementato a 11;
  3. La funzione cubo calcola il cubo di 10 e restituisce 1000;
  4. Il valore restituito viene assegnato alla variabile y.

Dopo l'esecuzione di questo stralcio di codice, come è lecito aspettarsi, il valore di x sarà 11 e il valore di y sarà 1000.

Ma cosa accade se volessimo implementare la stessa funzione cubo come macro parametrica?

#define CUBO(x) ((x) * (x) * (x))

E se volessimo invocare la macro CUBO nello stesso modo?

int x = 10;
int y = CUBO(x++);

Il risultato sarà molto diverso in questo caso. Infatti, il codice generato dalla macro sarà:

int x = 10;
int y = ((x++) * (x++) * (x++));

In questo caso, la variabile x viene incrementata tre volte. Questo perché, come abbiamo detto, una macro è una sostituzione di testo. Ogni occorrenza di x nel corpo della macro viene sostituita con il valore effettivo di x al momento della sostituzione. Inoltre, la variabile y sarà uguale a 10 * 11 * 12 = 1320.

Il risultato x = 12 e y = 1320 è molto diverso da quello che ci aspettavamo!

Errori di questo tipo possono essere molto difficili da individuare e risolvere. Anche perché invocare una funzione e invocare una macro parametrica sono, dal punto di vista sintattico, due operazioni identiche.

Il problema è ancora più grave dal momento che tali errori si possono manifestare solo in determinate condizioni. Tornando all'esempio di sopra, l'errore non si sarebbe manifestato se non avessimo usato l'operatore ++ all'interno dell'invocazione della macro. Ad esempio, il codice seguente:

int x = 10;
int y = CUBO(x);
x++;

è del tutto identico a:

int x = 10;
int y = cubo(x++);
Nota

Macro parametriche ed effetti collaterali

Dal momento che una macro parametrica può valutare i propri argomenti più di una volta, si consiglia di evitare di usare espressioni complesse come argomenti di una macro, specialmente se tali espressioni modificano lo stato del programma.

Pattern ricorrenti

Concludiamo questa lezione con un'ultimo interessante utilizzo delle macro parametriche: la definizione di pattern ricorrenti.

Uno dei modi più convenienti per utilizzare le macro parametriche è quello di sostituire pezzi di codice ripetuti.

Chiariamo con un esempio. Supponiamo che nel nostro codice dobbiamo stampare più volte dei valori double con una certa precisione. Potremmo avere una situazione del genere:

double a = 10.123456789;
printf("%.7f\n", a);

/* ... */

double b = 20.123456789;
printf("%.7f\n", b);

/* ... */

double c = 30.123456789;
printf("%.7f\n", c);

In questo esempio, abbiamo più variabili double da stampare a schermo con la stessa precisione. Inoltre, la precisione 7 è ripetuta più volte. Nel caso in cui volessimo cambiare la precisione, dovremmo modificare tutti i punti in cui viene stampato il valore double.

Per evitare di ripetere lo stesso codice, possiamo definire una macro parametrica che accetti come parametro la variabile da stampare. Ad esempio:

#define STAMPA_DOUBLE(x) printf("%.7f\n", x)

In questo modo, possiamo stampare i valori double in questo modo:

double a = 10.123456789;
STAMPA_DOUBLE(a);

/* ... */

double b = 20.123456789;
STAMPA_DOUBLE(b);

/* ... */

double c = 30.123456789;
STAMPA_DOUBLE(c);

Se volessimo cambiare la precisione con cui vengono stampati i valori double, possiamo farlo modificando una sola volta la definizione della macro.

Il vantaggio di usare una macro in questo caso è evidente. Inoltre, il codice risulta più chiaro e leggibile. Per cui, quando si trova uno schema di codice ripetuto è possibile utilizzare una macro parametrica per evitare di ripetere lo stesso codice più volte.

In Sintesi

In questa lezione abbiamo visto come definire e utilizzare le macro parametriche in linguaggio C. Abbiamo visto che una macro parametrica è una macro che accetta dei parametri in ingresso e che può essere utilizzata in modo simile ad una funzione.

La sintassi per definire una macro parametrica è la seguente:

#define NOME_MACRO(parametro1, parametro2, ..., parametroN) corpo_macro

Ci siamo poi concentrati sui vantaggi e sugli svantaggi delle macro parametriche:

  • Vantaggi:

    • Si possono ottenere delle performance migliori rispetto alle funzioni;
    • Le macro sono generiche e possono essere utilizzate con qualsiasi tipo di dato;
    • Si possono definire macro parametriche senza parametri;
    • Si possono definire pattern ricorrenti.
  • Svantaggi:

    • Il codice generato è di dimensioni maggiori rispetto al caso di una funzione;
    • I tipi degli argomenti non sono controllati;
    • Non esistono puntatori a macro;
    • La valutazione degli argomenti di una macro può avvenire più volte.

Nella prossima lezione studieremo due operatori molto importanti in linguaggio C: l'operatore di concatenazione ## e l'operatore di conversione in stringa #. Questi due operatori possono essere utilizzati esclusivamente all'interno del corpo delle macro.