Operatori per le Macro in Linguaggio C

Le macro del linguaggio C, siano esse semplici o parametriche, funzionano utilizzando il meccanismo della sostituzione testuale o espansione. Il corpo della macro viene semplicemente sostituito al posto della chiamata della macro. Non vengono eseguite operazioni.

Tuttavia, il linguaggio C mette a disposizione due operatori specifici per le macro che hanno significato esclusivamente nel corpo delle macro stesse. In altre parole sono operatori riconosciuti dal precompilatore ma non dal compilatore.

Questi operatori sono:

  • L'operatore di conversione in stringa letterale: #
  • L'operatore di concatenazione: ##

In questa lezione vedremo come funzionano e come possono essere utilizzati per creare macro più complesse.

Operatore per la conversione in stringa letterale: #

Il primo operatore specifico per le macro che il linguaggio C mette a disposizione è l'operatore di conversione in stringa letterale. In inglese, questo operatore viene chiamato con il nome di stringizing operator, un nome abbastanza stravagante che potrebbe essere tradotto con operatore di stringhizzazione.

Questo operatore è composto da un singolo # (cancelletto) e viene utilizzato per convertire un token in una stringa letterale.

Per comprendere come funziona vediamo un'applicazione concreta. Supponiamo di voler introdurre nel nostro programma delle stampe di diagnostica o debug, il cui scopo è quello di stampare il nome e il contenuto di alcune variabili, supponiamo, di tipo int. Il risultato potrebbe essere uno stralcio di codice del genere:

int a, b, c;

a = 10;
b = 20;
c = 30;

/* ... altro codice ... */

/* Vogliamo verificare in questo punto quanto vale a */
printf("Il valore di a è: %d\n", a);

/* Vogliamo verificare in questo punto quanto vale b */
printf("Il valore di b è: %d\n", b);

/* ... altro codice ... */

/* Vogliamo verificare in questo punto quanto vale c */
printf("Il valore di c è: %d\n", c);

/* Vogliamo verificare in questo punto quanto vale a nuovamente */
printf("Il valore di a è: %d\n", a);

/* ... e così via ... */

Come si può osservare dal codice di sopra, è presente uno schema abbastanza ripetitivo. Inoltre, se dovessimo cambiare il nome di una variabile, dovremmo modificare manualmente tutte le stampe di diagnostica. Questo lavoro può essere automatizzato con l'uso delle macro. In particolare, possiamo usare l'operatore di conversione stringa a nostro vantaggio in questo modo:

#define STAMPA_INTERO(a) printf("Il valore di " #a " è: %d\n", a)

Avendo definito una macro in questo modo, possiamo modificare il codice di sopra così:

int a, b, c;

a = 10;
b = 20;
c = 30;

/* ... altro codice ... */

STAMPA_INTERO(a);
STAMPA_INTERO(b);

/* ... altro codice ... */

STAMPA_INTERO(c);
STAMPA_INTERO(a);

/* ... e così via ... */

Prendiamo l'istruzione:

STAMPA_INTERO(a)

Quando il preprocessore trova questa istruzione, la sostituisce con:

printf("Il valore di " "a" " è: %d\n", a)

Questo, perché, #a viene sostituito con "a", che è una stringa letterale. In questo modo, il nome della variabile a viene stampato insieme al suo valore.

L'istruzione di sopra è valida in quanto, come abbiamo visto nel capitolo dedicato alle stringhe, ogniqualvolta il compilatore incontra stringhe letterali adiacenti, provvede automaticamente ad unirle. Quindi:

printf("Il valore di " "a" " è: %d\n", a)

è equivalente a:

printf("Il valore di a è: %d\n", a)

Ricapitolando:

Definizione

Operatore di conversione in stringa letterale per le macro: #

L'operatore #, chiamato operatore di conversione in stringa letterale o stringizing operator, viene utilizzato per convertire un token (o parametro della macro) in una stringa letterale.

Questo operatore è valido soltanto nel corpo di una macro.

La sintassi è:

#define MACRO(parametro) corpo macro #parametro corpo macro

Operatore di concatenazione: ##

Il secondo operatore specifico per le macro è l'operatore di concatenazione. Come suggerisce il nome, questo operatore prende in ingresso due token e li incolla fino a formare un singolo token.

Se uno dei due token è uno dei parametri della macro, la concatenazione avviene dopo la sostituzione.

Prendiamo un esempio. Supponiamo di voler definire una macro che ci consenta di creare delle variabili con uno stesso nome, ma con un suffisso numerico. Ad esempio, vogliamo creare le variabili identificativo_numerico_1, identificativo_numerico_2, identificativo_numerico_3, ecc. Se volessimo procedere in maniera manuale dovremmo scrivere:

int identificativo_numerico_1;
int identificativo_numerico_2;
int identificativo_numerico_3;
/* ... e così via ... */

Possiamo creare una macro che ci semplifica la scrittura di questo codice in questo modo:

#define CREA_ID(numero) int identificativo_numerico_##numero

Possiamo, poi, usarla in questo modo:

CREA_ID(1);
CREA_ID(2);
CREA_ID(3);
/* ... e così via ... */

Dopo la sostituzione effettuata dal preprocessore, il codice risultante sarà:

int identificativo_numerico_1;
int identificativo_numerico_2;
int identificativo_numerico_3;
/* ... e così via ... */

Proprio come il codice che avremmo scritto manualmente.

Questo perché, se prendiamo come esempio la riga CREA_ID(1), il preprocessore prima sostituisce numero con 1, e poi unisce identificativo_numerico_ con 1, ottenendo identificativo_numerico_1.

Ricapitolando:

Definizione

Operatore di concatenazione per le macro: ##

L'operatore ##, chiamato operatore di concatenazione o token-pasting operator, viene utilizzato per concatenare due token.

Questo operatore è valido soltanto nel corpo di una macro.

La sintassi è:

#define MACRO(parametro) corpo_macro##parametro

La concatenazione avviene soltanto dopo la sostituzione dei parametri.

Questo operatore, in realtà, non è molto utilizzato nella pratica, anche perché i campi di applicazione non sono molti.

Un possibile utilizzo è quello di creare delle macro che generano codice in modo automatico. Supponiamo di voler realizzare delle versioni di una stessa funzione per tipi differenti. Potremmo scrivere una macro che, dato un tipo, genera il codice della funzione per quel tipo. Ad esempio, supponiamo di voler creare delle funzioni che sommano due variabili tra di loro. Queste funzioni sono sostanzialmente identiche a meno del tipo in quanto avrebbero una struttura del genere:

tipo somma(tipo a, tipo b) {
    return a + b;
}

Volendo lavorare senza macro, dovremmo scrivere una funzione per ogni tipo:

int somma(int a, int b) {
    return a + b;
}

/* CODICE NON VALIDO: nome della funzione ripetuto */
float somma(float a, float b) {
    return a + b;
}

/* CODICE NON VALIDO: nome della funzione ripetuto */
double somma(double a, double b) {
    return a + b;
}

/* ... e così via ... */

Nel codice di sopra c'è un problema: il linguaggio C non consente di definire più funzioni con lo stesso nome, anche se i tipi dei parametri sono diversi. Questo significa che il codice di sopra non compila.

Per tal motivo dovremmo riscrivere il codice in questo modo:

int somma_int(int a, int b) {
    return a + b;
}

float somma_float(float a, float b) {
    return a + b;
}

double somma_double(double a, double b) {
    return a + b;
}

/* ... e così via ... */

Questo è un approccio abbastanza ripetitivo e soggetto ad errori. Possiamo, però, sfruttare le macro parametriche con l'operatore di concatenazione per automatizzare la creazione di queste funzioni:

#define CREA_FUNZIONE_SOMMA(tipo) \
    tipo somma_##tipo(tipo a, tipo b) { \
        return a + b; \
    }

Grazie a questa macro, possiamo creare le funzioni per i tipi int, float e double in questo modo:

CREA_FUNZIONE_SOMMA(int);
CREA_FUNZIONE_SOMMA(float);
CREA_FUNZIONE_SOMMA(double);

Dopo la sostituzione effettuata dal preprocessore, la riga CREA_FUNZIONE_SOMMA(int); diventa:

int somma_int(int a, int b) {
    return a + b;
}

Questo perché a tipo viene sostituito int e a somma_##tipo viene sostituito somma_int.

In Sintesi

Le macro mettono a disposizione due operatori riconosciuti soltanto dal precompilatore ma non sono validi per il compilatore. Questi operatori sono:

  • L'operatore # permette di convertire un token in una stringa letterale.
  • L'operatore ## permette di concatenare due token.

Questi operatori sono utili per creare macro più complesse e per automatizzare la generazione di codice ripetitivo.

Nella prossima lezione studieremo quali sono le proprietà generali delle macro, siano esse semplici o parametriche.