Dettagli su Macro e Sintassi in Linguaggio C

In questa lezione affronteremo alcuni dettagli sulla sintassi e sul funzionamento delle macro che abbiamo volutamente tralasciato nelle lezioni precedenti.

In particolare, ci concentreremo su come evitare errori di valutazione e di precedenza, e su come realizzare macro che combinano più espressioni o più istruzioni.

Macro e Parentesi

Nelle lezioni precedenti abbiamo definito macro parametriche e non utilizzando, all'interno del loro corpo, una quantità di parentesi a prima vista spropositato.

Ad esempio, quando abbiamo definito la macro MAX, nella lezione sulle macro parametriche, abbiamo scritto:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

In questo caso, abbiamo sia racchiuso i parametri tra parentesi ogniqualvolta li abbiamo utilizzati, sia racchiuso l'intera espressione tra parentesi.

Sorge spontanea, a questo punto, la domanda: sono tutte queste parentesi necessarie? La risposta è positiva come vedremo adesso.

In generale, quando si scrive la definizione di una macro, vanno seguite due regole.

La prima regola è: se il corpo di una macro contiene uno dei parametri, conviene sempre racchiuderlo tra parentesi. Questo perché, se non si fa, si rischia di ottenere risultati inaspettati.

Prendiamo un esempio. Supponiamo di voler implementare una macro, SQUARE, che calcoli il quadrato di un numero. La definizione potrebbe essere la seguente:

#define SQUARE(x) x * x

Tuttavia, una definizione così scritta, nasconde un'insidia. Infatti, se utilizziamo la macro SQUARE in questo modo:

int valore = 5;

/* Calcoliamo il quadrato di valore + 1 */
int risultato = SQUARE(valore + 1);

ci aspetteremmo che, utilizzando la macro SQUARE, il valore di risultato sia 36. Ossia, che la macro calcoli il quadrato di 6. In realtà non è così.

Il valore di risultato sarà, invece, 11! Questo perché la macro SQUARE è stata espansa in:

int risultato = valore + 1 * valore + 1;

Quindi l'espressione sarà valutata come 5 + 1 * 5 + 1, che è uguale a 11, in quanto l'operatore di moltiplicazione * ha una precedenza maggiore rispetto all'operatore di somma +.

Per risolvere questo problema, dobbiamo racchiudere i parametri tra parentesi. La definizione corretta della macro SQUARE è:

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

Così facendo, in fase di espansione della macro, il codice diventerà:

int risultato = (valore + 1) * (valore + 1);

e il valore di risultato sarà 36, come ci aspettavamo.

La seconda regola da rispettare, per evitare brutte sorprese, è: se il corpo di una macro contiene uno o più operatori, conviene sempre racchiudere tutto il corpo tra parentesi.

Ritorniamo all'esempio di prima della macro SQUARE. Supponiamo volere calcolare, sfruttando SQUARE, il reciproco del quadrato di un numero:

r = \frac{1}{x^2}

Potremmo scrivere il codice in questo modo:

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

double x;

printf("Inserisci il valore di x:\n");
scanf("%lf", &x);

double r = 1 / SQUARE(x);

Il problema è che, con questo codice, non otterremo mai ciò che ci aspettiamo. Infatti, la macro SQUARE verrà espansa in:

double r = 1 / x * x;

Tuttavia, poiché l'operatore di divisione / ha la stessa precedenza dell'operatore di moltiplicazione *, l'espressione verrà valutata come:

r = \frac{1}{x} \cdot x

Quindi il valore di r sarà sempre 1!!!

La versione corretta del codice di sopra si ottiene racchiudendo tutto il corpo della macro tra parentesi:

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

Facendo così, l'espressione verrà valutata come:

double r = 1 / ((x) * (x));

e il valore di r sarà il reciproco del quadrato di x, come ci aspettavamo.

In generale, quindi, è sempre meglio essere prudenti e racchiudere tra parentesi sia i parametri, sia l'intero corpo della macro. Ricapitolando:

Consiglio

Regole per la definizione di una macro

Quando si scrive una macro, parametrica o meno, in linguaggio C, conviene sempre seguire le seguenti regole per evitare errori nascosti ed effetti indesiderati:

  1. Racchiudere i parametri tra parentesi: se il corpo di una macro contiene uno dei parametri, è sempre meglio racchiuderlo tra parentesi per evitare errori di valutazione.
  2. Racchiudere tutto il corpo tra parentesi: se il corpo di una macro contiene uno o più operatori, è sempre meglio racchiudere tutto il corpo tra parentesi per evitare errori di precedenza.

Macro e Istruzioni multiple

Un utilizzo molto interessante delle macro è la combinazione di istruzioni multiple. Questo significa che, all'interno del corpo di una macro, possiamo scrivere più di un'istruzione, separate dall'operatore virgola.

Ad esempio, supponiamo di voler realizzare una macro, chiamata PROMPT_DOUBLE, che mostra all'utente un messaggio e legge un valore double da tastiera. La definizione potrebbe essere la seguente:

#define PROMPT_DOUBLE(variabile) \
    (printf("Inserisci " #variabile ": "), scanf("%lf", &variabile))

In questa macro, sia la chiamata a printf che la chiamata a scanf sono due espressioni, quindi è possibile combinarle con l'operatore virgola.

Possiamo invocare la macro PROMPT_DOUBLE in questo modo:

double x;
PROMPT_DOUBLE(x);

Da notare che abbiamo racchiuso il corpo della macro tra parentesi tonde. Avremmo potuto utilizzare le parentesi graffe, ma in questo caso avremmo dei problemi.

Infatti, se scrivessimo la macro in questo modo:

#define PROMPT_DOUBLE(variabile) { \
    printf("Inserisci " #variabile ": "); \
    scanf("%lf", &variabile); \
}

E poi la utilizzassimo in questo modo:

double x;

if (condizione)
    PROMPT_DOUBLE(x);
else
    x = 0;

Otterremmo un errore di compilazione, in quanto la macro PROMPT_DOUBLE verrebbe espansa in:

1
2
3
4
5
6
7
8
9
double x;

if (condizione)
{
    printf("Inserisci " "x" ": ");
    scanf("%lf", &x);
};
else
    x = 0;

In particolare, la riga 7 provocherebbe un errore. Infatti, il compilatore tratterebbe il punto e virgola singolo come un'istruzione nulla, concludendo l'istruzione if. La clausola else non apparterrebbe a nessun if, generando un errore.

Avremmo potuto risolvere il problema semplicemente omettendo il punto e virgola dopo la macro in questo modo:

double x;

if (condizione)
    PROMPT_DOUBLE(x)
else
    x = 0;

Ma, facendo così, il nostro programma risulterebbe strano e non manterremmo la coerenza tra le istruzioni.

Ricapitolando:

Consiglio

Macro come combinazione di espressioni multiple

Quando si scrive una macro in linguaggio C, è possibile combinare più espressioni all'interno del corpo della macro. Per farlo, è sufficiente separare le espressioni con l'operatore virgola.

In tal caso, conviene sempre racchiudere il corpo della macro tra parentesi tonde, per evitare errori di compilazione.

La sintassi generale di una macro con più espressioni è:

#define NOME_MACRO(parametri) (espressione1, espressione2, ..., espressioneN)

Quanto detto finora vale se vogliamo combinare espressioni multiple. Non vale, invece, se vogliamo combinare istruzioni multiple. In tal caso l'operatore virgola servirebbe a ben poco, in quanto non è in grado di combinare istruzioni.

Per risolvere il problema, in questo caso, possiamo sfruttare un trucco. Possiamo, ovvero, combinare le istruzioni all'interno di un ciclo do-while che viene eseguito una sola volta. Vediamo come fare con un esempio.

Vogliamo scrivere una versione modificata della macro PROMPT_DOUBLE che, oltre a chiedere all'utente di inserire un valore double, controlli che il valore inserito sia maggiore di zero. Se non lo è, del valore viene salvato il valore assoluto.

Possiamo scrivere la macro in questo modo:

#define PROMPT_POSITIVE_DOUBLE(variabile) \
    do { \
        printf("Inserisci " #variabile ": "); \
        scanf("%lf", &variabile); \
        if (variabile <= 0) \
            variabile = -variabile; \
    } while (0)

Facciamo qualche osservazione. Per prima cosa, abbiamo racchiuso l'insieme di istruzioni nel corpo del ciclo do-while. Questo ci permette di scrivere più di un'istruzione all'interno della macro.

Come condizione del ciclo, abbiamo scelto 0. Questo perché il ciclo do-while viene eseguito almeno una volta, indipendentemente dalla condizione. In questo caso, la condizione è sempre falsa, quindi il ciclo viene eseguito una sola volta. Quindi, le nostre istruzioni verranno eseguite una sola volta.

Infine, notiamo che abbiamo omesso il punto e virgola alla fine del ciclo. In questo modo, quando andiamo ad utilizzare la macro, possiamo inserire il punto e virgola come se fosse un'unica istruzione.

Possiamo utilizzare la macro PROMPT_POSITIVE_DOUBLE in questo modo:

double x;

PROMPT_POSITIVE_DOUBLE(x);

Dopo l'espansione della macro, il codice diventerà:

double x;

do {
    printf("Inserisci " "x" ": ");
    scanf("%lf", &x);
    if (x <= 0)
        x = -x;
} while (0);

In questo modo, il nostro codice funzionerà correttamente e manterrà la coerenza tra le istruzioni.

Ricapitolando:

Consiglio

Macro come combinazione di istruzioni multiple

Quando si vuole realizzare una macro come combinazione di più istruzioni in linguaggio C, non si può adoperare l'operatore virgola.

Si può, tuttavia, sfruttare un ciclo do-while che viene eseguito una sola volta, ossia con la condizione sempre falsa.

Per cui, la sintassi generale di una macro con più istruzioni è:

#define NOME_MACRO(parametri) \
    do { \
        istruzione1; \
        istruzione2; \
        ... \
        istruzioneN; \
    } while (0)

Quando la si vuole utilizzare nel codice, si può utilizzare la sintassi seguente:

NOME_MACRO(parametri);

In Sintesi

Questa lezione ci è stata utile per comprendere come scrivere macro evitando errori di valutazione e di precedenza.

In particolare, abbiamo trovato due regole empiriche:

  1. Racchiudere i parametri tra parentesi: se il corpo di una macro contiene uno dei parametri, è sempre meglio racchiuderlo tra parentesi per evitare errori di valutazione.
  2. Racchiudere tutto il corpo tra parentesi: se il corpo di una macro contiene uno o più operatori, è sempre meglio racchiudere tutto il corpo tra parentesi per evitare errori di precedenza.

Abbiamo anche visto come realizzare macro che combinano più espressioni o più istruzioni, sfruttando l'operatore virgola o un ciclo do-while che viene eseguito una sola volta.

Nella prossima lezione vedremo le macro predefinite, ossia macro che il compilatore definisce a priori e che possiamo utilizzare per ottenere informazioni sul codice sorgente.