Macro Programming - STB1019/SkullOfSummer GitHub Wiki

Introduzione

In questa sezione si andranno ad imparare i concetti relativi alla macro programming, una sezione non banale del linguaggio C. La macro programming sfrutta il comando del preprocessore #define per poter programmare in maniera ancora più flessibile rispetto alla normalità. Con la macro programming è possibile evitare di copia-incollare blocchi di codice simili ma non identici, blocchi di codice che normalmente non sarebbe possibile comprimere in una funzione. Alcuni scenari in cui la macro programming potrebbe esservi utile:

  • Avete pezzi di codice simili tra di loro, ma cambiano alcune righe del codice stesso (per esempio c'è una chiamata di una funzione f nella prima e g nella seconda;
  • Una funzione lunga da scrivere viene richiamata spesso. Con la macro programming potreste compattarla scrivendo così meno codice;
  • Volete rendere più leggibile il codice, nascondendone le parti tecniche per far risaltare la logica dello stesso;

Un avviso comunque: la macro programming spesso, se usata male o sovrasfruttata, rende il codice non mantenibile! Quindi, da grandi poteri derivano grandi responsabilità!

Come funziona

La base della macro programming è il #define. Tramite esso è possibile, normalmente, definire costanti usate in tutto il codice. Per esempio:

#define BUFFER_SIZE 1000
char buffer[BUFFER_SIZE];

Una macro è una stringa che, in fase di preprocessing, viene sostituita in maniera cieca con il corrispettivo valore. Nell'esempio di prima il preprocessore ogni volta che vede nel codice sorgente la stringa "BUFFER_SIZE" la sostituirà con la stringa "1000". Non esiste il concetto di tipo, né di funzione, né di altro.

Le macro vengono sostituite durante la fase di preprocessing. Quindi, quando viene evocato il compilatore vero e proprio, le macro sono già state sostituite con il corrispettivo valore. Per esempio il compilatore vedrà solo:

char buffer[1000];

Siccome la macro programming è eseguita dal preprocessore, è importante capire che in essa non c'è alcun concetto di variabili, costanti, tipi o quant'altro; la macro programming è solo la copia bruta di pezzi di stringhe in altre stringhe. Non importa se esse siano o meno sintatticamente corrette con il C: a quello ci pensa il compilatore, non il preprocessore.

Per esempio:

#define BUFFER_SIZE amicoBello!?
char buffer[BUFFER_SIZE];

sorpassa con successo la fase di preprocessing, ma la fase di compilazione sarà piena di errori.

Macro funzioni

La base della macro programming sono le function-like macros. Esse sono macro parametrizzate. Per esempio:

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

Supponiamo che nel codice sorgente questa macro sia stata usata con diversi parametri. La tabella mostra cosa il preprocessore sostituisce:

macro nel codice sorgente valore di a valore di b stringa che il preprocessore sostituisce nel codice
MAX(4,5) 4 5 4 > 5 ? 4 : 5
MAX(10.5, 6) 10.5 6 10.5 > 6 ? 10.5 : 6
MAX(int, 6) int 6 int > 6 ? int : 6
MAX(6, cosa) 6 cosa 6 > cosa ? 6 : cosa
MAX(6, &grafo->node[5] 6 &grafo->node[5] 6 > &grafo->node[5] ? 6 : &grafo->node[5]
MAX(6, getIncrementalID(7) 6 getIncrementalID(7) 6 > getIncrementalID(7) ? 6 : getIncrementalID(7)

Puoi ben capire che il primo ed il secondo esempio sono corretti; il terzo esempio darà un evidente errore in fase di compilazione. Il quarto esempio funzionerà solo se lo sviluppatore ha definito una variabile chiamata "cosa". Il quinto esempio funzionerà solo se esisterà una variabile "grafo" su cui è possibile applicare le operazioni (&, ->, node, [5]). L'ultimo esempio è corretto, ma solo se getIncrementalID è idempotente (non ha effetti collaterali) (vedi più avanti).

In generale la macro programming è tutta questa: essa consiste nel costruire funzioni macro che possano essere usate per semplificare il codice sorgente.

Giusto per semplicità, volendo è possibile estendere il #define di una funzione macro su più righe, facendo in modo che l'ultimo carattere della linea sia \, ad esempio:

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

L'ultima riga non deve avere \.

Pitfalls

Una cosa apparentemente semplice come la macro programming nasconde in realtà incredibili insidie. Vediamo le più importanti.

Duplication of side effects

Consideriamo la nostra macro:

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

ed applichiamo al codice sorgente:

MAX(1, getIncrementalID())

dove getIncrementalID è:

int getIncrementalID() { static int id = 0; id++; return id; }

Questa è una funzione con effetti collaterali, nel senso che la sua esecuzione con gli stessi parametri di ingressi da effetti collaterali (in questo caso la variabile statica id aumenta sempre):

getIncrementalID(); //da 1
getIncrementalID(); //da 2
getIncrementalID(); //da 3

In questo caso eseguire la macro darà il risultato 2! Perché? Perché la macro viene espansa in:

1 > getIncrementalID() ? 1 :  getIncrementalID()

La prima volta getIncrementalID() darà 1, 1 > 1 sarà falso; ciò implica che verrà scelto come valore di ritorno getIncrementalID(), che però ora darà 2. Bisogna quindi stare attenti al fatto che l'input di una macro potrebbe essere una funzione non idempotente: in questo caso una soluzione è assegnare un valore temporaneo alle variabili:

#define MAX(a,b) \
    int _a = a;  \
    int _b = b;  \
    return _a > _b ? _a : _b

precedenze errate

Consideriamo sempre la nostra funzione macro MAX:

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

Ora, consideriamo questo pezzo di codice:

printf("%d\n", MAX(5,4));
printf("%d\n", MAX(5,4)*2);

Se si prova ad eseguirlo, il programma darà il seguente output:

5
5

Il primo risultato va bene, mentre il secondo no: noi vogliamo stampare il doppio del massimo tra 4 e 5, ed invece otteniamo il 5. Questa pitfall è causata dal fatto che, nell'espandere la macro, la seconda chiamata diventa:

printf("%d\n", 5 > 4 ? 5 : 4*2);

Dove il *2 non è più applicato al massimo, ma solo al 4! Una soluzione a questo è l'aggiunta di parentesi all'output della funzione MAX:

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

Anzi, spesso per evitare lo stesso problema negli input, si mettono parentesi anche intorno agli input stessi:

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

Operatori nelle macro

All'interno delle macro, è possibile utilizzare 2 operatori:

  1. # x: l'operatore #x ti permette di ottenere la stringa di x (double-quotes incluse!) dove x è un parametro di ingresso della funzione macro. Per esempio:

     #define STR(x) #x
    
     int main() {
         printf(STR(ciao));
         printf("\n");
     }
    

diventerà:

    int main() {
        printf("ciao"); //notare che l'operator # aggiunge le " automaticamente!
        printf("\n");
    }
  1. x ## y: l'operatore ti permette di contatenare 2 sequenze di caratteri. I 2 operatori non devono essere per forza 2 parametri di ingressi di una macro. Per esempio:

     #define CMD(x) command_ ## x
    

applicato a:

   #define CMD(quit)

diventerà:

   command_quit

Variadic macro functions

In C sono definite variadic functions quelle funzioni che accettano un numero indefinito di argomenti. Un esempio di variadic function potrebbe essere la funzione printf:

printf("hello world!"); //qui printf ha 1 argomento
printf("Hello %s!", "Gigi"); //qui printf ha 2 argomenti
printf("Hello % %s!", "Gigi", "Rossi"); //qui printf ha 3 argomenti

Come esistono le variadic functions, esistono anche le variadic macro functions. Facciamo il classico esempio:

#define eprintf(template, ...) fprintf (stderr, template, __VA_ARGS__)

Essa può essere usata come:

eprintf("Attention! Error number #%d", 5);

che viene espansa in:

fprintf(stderr, "Attention! Error number #%d", 5);

Il variadic in realtà è finito qui. L'unica cosa importante da ricordare è che VA_ARGS può anche essere vuoto. In questo caso la precedente macro creerebbe un qualcosa di non compilabile:

eprintf("Unknown scenario") --> fprintf(stderr, "Unknown scenario", ); //virgola di troppo

Per questo il GCC (però è un qualcosa del GCC, non dello standard C99!) prevede un comportamento particolare dell'operatore ##: quando il primo termine di ## è una virgola, essa viene automaticamente eliminata. Concretamente, se scriviamo la macro come:

#define eprintf(template, ...) fprintf(stderr, template, ## __VA_ARGS__)

Scrivere poi:

eprintf("Unknown scenario");

Verrà espanso come:

fprintf(stderr, "Unknown scenario");

Un esempio completo (e complesso!)

la macro MAX non è così utile: una banale funzione può benissimo sostituire la macro. Consideriamo un esempio più sensato.

Supponiamo di avere un buffer di 1000 elementi contenente interi. Nel nostro programma supponiamo di dover scansionare questo buffer tantissime volte. Scrivere ogni volta un ciclo che lo scansioni è lungo. Quindi vogliamo scrivere una macro che lo "nasconda". Lo possiamo fare per esempio:

#define ITERATE_BUFFER() \
    for(int i=0; i<1000; i++)

In questo modo possiamo scansionare il buffer in questo modo:

ITERATE_BUFFER() {
    printf("buffer value is %d = %d\n", i , buffer[i]);
}

Ok, carino. Però vorremmo avere anche automaticamente il valore del buffer nella posizione i senza dover scrivere buffer[i]. Possiamo farlo in questo modo:

#define ITERATE_BUFFER() \
for ( \
	int i=0, v=buffer[0] \
	; \
	i<BUFFER_SIZE \
	; \
	i++, v=(i < BUFFER_SIZE ? buffer[i] : v) \
)

Ed usarlo in questo modo:

ITERATE_BUFFER() {
    printf("buffer value is %d = %d\n", i , v);
}

Ok, così abbiamo risparmiato altri tasti. Supponiamo però di voler scrivere:

ITERATE_BUFFER() {
    ITERATE_BUFFER() {
        //Compara i 2 valori
    }
}

Qua abbiamo 2 problemi:

  1. entrambi gli ITERATE_BUFFER definiscono 2 variabili intere i e v, quindi ci sarà una dichiarazione duplicata che non ci permetterà di compilare;
  2. i e v si riferiscono a quale ciclo? il primo od il secondo?

Possiamo risolvere il problema aggiungendo il nome della variabile di ciclo come un parametro di ingresso della macro:

#define ITERATE_BUFFER(index, value) \
for ( \
	int index=0, value=buffer[0] \
	; \
	index<BUFFER_SIZE \
	; \
	index++, value=(index < BUFFER_SIZE ? buffer[index] : value) \
)

E usare la macro in questo modo:

ITERATE_BUFFER(i,v) {
    ITERATE_BUFFER(j, w) {
        if (i < j) {
            printf("sum is %d\n", v + w);
        }
    }
}

Esercizi

  1. Scrivi la macro MIN(a,b) che calcola il minimo tra 2 numeri. Alcuni esempi:
    • MIN(4,5) = 4
    • MIN(-1, -5) = -5
  2. Scrivere la macro A_FROM_1(array, index) che ti permette di usare un array i cui indici iniziano da 0 come se fosse un array i cui indici iniziino da 1. Alcuni esempi:
    • A_FROM_1(buffer, 1) = buffer[0]
    • A_FROM_1(buffer, 2) = buffer[1]
    • A_FROM_1(buffer, 3) = buffer[2]
  3. Scrivere la funzione SWAP(x, y) che ti permette di swappare il contenuto della variabile x con quella della variabile y. La macro deve funzionare con qualsiasi tipo di variabile. (Esercizio truffaldino! Per risolvere l'esercizio ti serve inserire un parametro aggiuntivo nella macro. Perché? Ragiona sul fatto che per fare lo swapping ti serve una variabile temporanea di un certo tipo...). Per esempio:
    • int x=5; int y=6; SWAP(x, y); //ora x=6 e y=5
    • double* x=&c; double* y=&d; SWAP(x, y); //ora x=&d e y=&c
  4. Scrivere una macro che effettua la somma di tutti i numeri tra parentesi. Si supponga di avere una funzione vsum che, dati n numeri, computi la somma. Per esempio:
    • SUM(5,4,2,5,7) = 23
  5. (solo per temerari). Come l'esercizio 4, ma ora assumi di non avere la funzione vsum (Suggerimento: usa un array costante).

Riferimenti

  1. manuale delle macro gcc. Qui c'è tutto quello che vi serve per capire l'argomento: https://gcc.gnu.org/onlinedocs/cpp/Macros.html;
  2. https://gcc.gnu.org/onlinedocs/cpp/Operator-Precedence-Problems.html#Operator-Precedence-Problems;
  3. pitfall delle macro al MIT: http://web.mit.edu/rhel-doc/3/rhel-cpp-en-3/macro-pitfalls.html;
  4. Uso delle parentesi nelle macro: https://stackoverflow.com/questions/10820340/the-need-for-parentheses-in-macros-in-c;
  5. variadic macro functions: https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html#Variadic-Macros