METAC Metaprogrammazione in C

Metaprogrammazione in C

Il C è un linguaggio base, non prevede, a differenza di altri linguaggi come C++ o Rust, meccanismi avanzati per metaprogrammazione.

Ciònonostante è possibile anche nel nostro amato linguaggio C creare delle astrazioni in grado di ridurre le righe di codice che dobbiamo scrivere, grazie all’uso del preprocessore.

Il preprocessore C

Tutti quelli che leggeranno questo articolo conosceranno il preprocessore C: è quello strumento che elabora i nostri file sorgenti prima di darli in pasto al compilatore. Un tempo era proprio uno strumento separato, anche se oggigiorno è stato integrato all’interno dei compilatori. Nonostante questo è ancora possibile andare ad eseguirlo come programma a se stante, e ci potrà essere utile per capirne il funzionamento.

Ad esempio, prendiamo questo semplice file C:

#define NAME "ale"
#define f(x) "Ciao, " x

f(NAME)

Eseguengo il comando cpp ex1.c otteniamo il seguente output:


# 0 "ex1.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "ex1.c"



"Ciao, " "ale"

Come possiamo vedere, oltre ad alcune righe di diagnostica (che verranno poi interpretate dal compilatore per avere informazioni sul file sorgente originale), vediamo che il preprocessore ha interpretato le nostre define.

L’istruzione #define

In questo articolo ci concentreremo sulla creazione di macro, che si effettuano con l’istruzione #define. Questa istruzione prevede la definizione di due tipologie di macro:

Le macro testuali, che sono le più note, fanno sì che il preprocessore sostituisca al simbolo definito tutta la sequenza di token a destra, fino al successivo fineriga. Notare che ho parlato di token, e non caratteri, perché il preprocessore tokenizza l’input prima di parsarlo. Ad esempio se mettessimo una sequenza di più caratteri spazio questi verrebbero ridotti ad un singolo spazio, in quanto per il linguaggio C non fa differenza.

Le macro a funzione, invece, effettuano sempre una sostituzione, ma possono prendere dei parametri, che vengono sostituiti all’interno del testo.

Errori comuni con le macro (e come evitarli)

Valutazione doppia degli argomenti

L’errore più comune nello scrivere macro, specialmente a funzione, è quello della valutazione doppia degli argomenti. Supponiamo di avere una macro MIN definita come segue:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

Cosa accadrebbe chiamando la macro con: MIN(f(x), 42)? Il risultato della sostituzione effettuata dal preprocessore sarebbe f(x) < 42 ? f(x) : 42 ovvero nel caso in cui il risultato della funzione f sia inferiore a 42 questa viene chiamata 2 volte.

Nella migliore delle ipotesi f() non ha side effect per cui è solo un problema di performance, nella peggiore potremmo andare incontro a bug non facilmente identificabili.

Parentesi mancanti attorno alle espressioni

Questa immagino sia capitata ad un programmatore C almeno una volta. Supponiao di avere una define del tipo:

#define ITEM_SIZE 20 + 4

int arr[ITEM_SIZE * 10];

Riesci a vedere l’errore? Dopo l’espansione si avrà:

int arr[20 + 4 * 10];

che non è propio quel che il programmatore si aspettava!

Come regola generale, è bene sempre mettere le parentesi attorno all’espressione in una macro, ed attorno ad ogni argomento nelle macro a funzione, a meno che la macro non sia elementare, ovvero una define che ha a destra un solo token.

Macro a funzione non sanificate

Questa è un po’ più avanzata, supponiamo di voler scrivere una macro CHECK_ERROR definita come segue:

#define CHECK_ERROR(x) if ((x) < 0) perror("unexpected error")

All’apparenza sembra una macro ben fatta: gli argomenti sono valutati una sola volta, ci sono le parentesi tonde attorno agli argomenti, cosa potrebbe andare storto?

int fd = open("hello.txt", O_WRONLY);
if (fd != -1)
    CHECK_ERROR(write(fd, "hello", 5));
else
    perror("file not found!");

Vediao il risultato dell’espansione:

int fd = open("hello.txt", O_WRONLY);
if (fd != -1)
    if ((write(fd, "hello", 5)) < 0) perror("unexpected error");
else
    perror("file not found!");

Visto il bug? Proviamo ad identare meglio…

int fd = open("hello.txt", O_WRONLY);
if (fd != -1)
    if ((write(fd, "hello", 5)) < 0) 
        perror("unexpected error");
    else
        perror("file not found!");

Ops!

La tecnica do-while

Un modo per evitare l’errore appena visto è la oramai nota tecnica del do-while:

#define CHECK_ERROR(x) do { \
    if ((x) < 0)            \
        perror("error: ");  \
    } while (0) /* <-- notare il ; mancante! */

Uno si potrebbe chidere perché il do-while, e non semplicemente le parentesi graffe: il motivo è che il do-while richiede alla fine il ;. Omettendolo il compilatore genererebbe un errore di sintassi.

Questo fa sì che le macro fatte in questo modo si userrebbero allo stesso modo delle funzioni, consentendo agevolmente di trasformare una macro in una funzione o viceversa. Sarebbe invece un errore sintattico mettere il ; dopo una parentesi graffa chiusa in alcuni contesti, quali un ramo di un if!

Per questo primo articolo direi che è tutto: nel prossimo vedreo esempi ancora più sofisticati di programmazione di macro in C.