La compilazione condizionale permette a un solo file sorgente di diventare diversi programmi. Il preprocessore decide, prima che il compilatore parta, quali blocchi di testo sopravvivono: il codice dentro un ramo falso quindi non viene semplicemente saltato a runtime, viene eliminato. Non viene mai analizzato, quindi non contribuisce in nulla all'eseguibile e può perfino contenere costrutti che il compilatore attuale rifiuterebbe.
Questa è la differenza rispetto a un normale if. Entrambi scelgono tra percorsi; solo uno dei due sceglie prima che il programma esista.
#ifdef e #ifndef
La domanda più semplice è se una macro sia definita o no:
Cancella la riga #define DEBUG e ogni stampa di debug sparisce dalla build: non disattivata, sparita. Eseguilo una volta così com'è, poi togli quella riga ed eseguilo di nuovo.
#ifndef è la negazione: mantiene il blocco solo se la macro non è definita. L'uso più comune è l'include guard trattata nei file header, ed è anche il modo per fornire un valore predefinito che chi compila può sovrascrivere:
Ogni #ifdef e #ifndef va chiuso con #endif. Nei file lunghi, annotare la chiusura, #endif /* DEBUG */, fa risparmiare tempo vero in seguito.
#if, #elif, #else
#if riceve un'espressione intera costante e mantiene il blocco quando è diversa da zero. Questo rende possibili i confronti di versioni e livelli:
Cambia LOG_LEVEL in 0 o 3 e riesegui per vedere il programma stesso cambiare forma.
L'espressione viene valutata dal preprocessore, il che significa che può usare solo costanti intere, operatori aritmetici e di confronto e macro che si espandono in queste cose. Non vede sizeof, i valori di un enum, le variabili o qualsiasi cosa richieda il compilatore. Una macro non definita dentro un #if vale 0 invece di essere un errore, cosa comoda e a volte sorprendente:
#if FEATURE_X /* FEATURE_X non è mai definita da nessuna parte -> 0 -> blocco scartato */
defined(NAME) trasforma la domanda di #ifdef in qualcosa di utilizzabile dentro un #if, così puoi combinare i test:
#if defined(DEBUG) && !defined(NDEBUG)
/* build di debug, e le asserzioni sono attive */
#endif
#if defined(LINUX) || defined(BSD)
/* uno qualsiasi dei sistemi unix-like */
#endif
#if defined(X) e #ifdef X significano la stessa cosa; usa il primo quando ti servono &&, || o !.
Definire macro da riga di comando
La vera forza di questi interruttori è che il sorgente non deve cambiare affatto. gcc -D definisce una macro per tutta la compilazione:
gcc -DDEBUG program.c -o program # DEBUG definita come 1
gcc -DLOG_LEVEL=3 program.c -o program # valore specifico
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app
-DNAME da solo equivale a #define NAME 1. -U NAME ne annulla la definizione, cosa che conta quando un header definisce qualcosa che vuoi disattivato.
Quindi la disposizione abituale è: il sorgente contiene valori predefiniti protetti da #ifndef e il comando di build sceglie una configurazione.
/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 /* silenzioso di default */
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# build di sviluppo
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev
# build di release
gcc -DNDEBUG -O2 src/*.c -o app
NDEBUG è standardizzata: definirla disattiva ogni assert() del programma, perché <assert.h> è a sua volta scritto con la compilazione condizionale. È lo schema in miniatura: un header che si compila in cose diverse a seconda di cosa ha definito la build.
Interruttori per piattaforma
I compilatori predefiniscono macro che identificano il sistema di destinazione, così un solo file sorgente può chiamare l'API giusta su ciascuno:
#if defined(_WIN32)
#include <windows.h>
#define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#elif defined(__linux__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#else
#error "Unsupported platform"
#endif
Le più comuni: _WIN32 (definita sia su Windows a 32 bit sia a 64 bit), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. Anche l'identità del compilatore ha il suo insieme, __GNUC__, __clang__, _MSC_VER, e così l'architettura, __x86_64__, __aarch64__.
Una versione eseguibile che dice per cosa è stata compilata:
#error merita di essere conosciuta di per sé: interrompe la compilazione con il tuo messaggio. Chiudere una catena di piattaforme con #else / #error "Unsupported platform" trasforma una build sbagliata e silenziosa in un fallimento chiaro in cima al log di build.
Per elencare tutte le macro che il tuo compilatore predefinisce:
gcc -dM -E - < /dev/null
Quell'output è la risposta autorevole a "cosa posso verificare su questa macchina".
#if 0 per disattivare codice
Commentare un blocco con /* ... */ fallisce appena il blocco contiene un suo commento, perché i commenti del C non si annidano: il primo */ all'interno chiude il commento esterno e tutto quello che segue diventa codice vagante. #if 0 non ha questo problema:
#if 0
/* Tutta questa regione viene rimossa, commenti compresi. */
legacy_init();
int n = old_calculation(42); /* anche questo commento va bene */
report(n);
#endif
Cambialo in #if 1 per riportare il codice in vita. Gli editor continuano a evidenziarlo come C, e puoi annidare un #if 0 dentro un altro #if.
È uno strumento di debug, non un sistema di archiviazione. Il codice che sta in #if 0 non viene mai compilato, quindi marcisce in silenzio: quando qualcuno riattiverà l'interruttore non compilerà più. Usalo mentre isoli un problema, poi cancella il blocco e lascia che sia il controllo di versione a ricordarlo.
Dove la compilazione condizionale va storta
Tre modi di fallire spiegano la maggior parte dei problemi.
Il codice mai compilato non viene mai controllato. Un refuso dentro un ramo #ifdef inattivo resta invisibile finché qualcuno non compila quella configurazione, magari mesi dopo, magari in CI su una piattaforma che non hai. Se un progetto ha rami per piattaforma importanti, compilali tutti regolarmente.
Mescolare #ifdef con il flusso di controllo diventa illeggibile in fretta. Questo genere di cose è difficile da ragionare:
if (ready) {
#ifdef FAST_PATH
fast_send(buf);
} else {
#endif
slow_send(buf);
}
Graffe aperte in un ramo e chiuse in un altro sono lecite e orribili. Preferisci condizioni che racchiudono intere funzioni e scegli tra quelle:
#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif
Spesso un if a runtime è meglio. Se entrambi i rami compilerebbero ovunque, un normale if (debug_enabled) mantiene entrambi i percorsi controllati nei tipi, testabili e attivabili senza ricompilare. Riserva il preprocessore a ciò che solo lui può fare: codice che davvero non può compilare sull'altra piattaforma.
Per lo schema che combina tutto questo, un header di valori predefiniti, un -D per ogni build e rami per piattaforma racchiusi in include guard, vedi i file header; per #define in sé, le macro.
Domande frequenti
Cos'è la compilazione condizionale in C?
È l'uso delle direttive del preprocessore per mantenere o scartare blocchi di codice sorgente prima che il compilatore parta. Il codice dentro un #ifdef falso viene eliminato del tutto dal testo: non viene mai analizzato, mai compilato e non compare mai nell'eseguibile. È così che un solo file sorgente supporta più piattaforme o tipi di build.
Qual è la differenza tra #ifdef e #if?
#ifdef NAME chiede solo se la macro esiste, indipendentemente dal suo valore. #if expression valuta un'espressione intera costante, quindi può confrontare valori: #if VERSION >= 3. Usa #ifdef per i flag che sono semplicemente presenti o assenti, #if quando il valore conta. #if defined(NAME) unisce le due cose e si può combinare con && e ||.
Come si definisce una macro da riga di comando con gcc?
Usa -D: gcc -DDEBUG program.c definisce DEBUG come se il file iniziasse con #define DEBUG 1, e -DMAX=50 le assegna un valore specifico. È così che i sistemi di build attivano funzionalità senza modificare il sorgente, e -U NAME ne annulla la definizione.
Perché usare #if 0 invece di commentare il codice?
Perché i commenti /* ... */ non si annidano: il primo */ dentro la regione chiude il commento in anticipo e il resto diventa codice non valido. #if 0 ... #endif rimuove qualsiasi quantità di codice contenente qualsiasi numero di commenti, mantiene attiva l'evidenziazione della sintassi e si riattiva banalmente con #if 1.