A compilação condicional permite que um único arquivo-fonte vire vários programas diferentes. O pré-processador decide, antes de o compilador rodar, quais blocos de texto sobrevivem - então o código dentro de um ramo que falha não é apenas pulado em tempo de execução, ele é apagado. Ele nunca é analisado, então não contribui em nada para o executável e pode até conter construções que o compilador atual rejeitaria.
Essa é a diferença em relação a um if comum. Os dois escolhem entre caminhos; só um deles escolhe antes de o programa existir.
#ifdef e #ifndef
A pergunta mais simples é se uma macro está definida:
Apague a linha #define DEBUG e todos os prints de depuração somem do build - não desativados, sumidos. Execute uma vez como está, depois remova essa linha e execute de novo.
#ifndef é a negação: mantenha o bloco só se a macro não estiver definida. Seu uso mais comum é a guarda de inclusão, coberta em arquivos de cabeçalho, e é também como você fornece um padrão que quem chama pode sobrescrever:
Todo #ifdef e #ifndef precisa ser fechado por um #endif. Anotar o fechamento em arquivos longos - #endif /* DEBUG */ - economiza tempo de verdade depois.
#if, #elif, #else
#if recebe uma expressão inteira constante e mantém o bloco quando ela é diferente de zero. Isso torna possíveis comparações de versão e de nível:
Mude o LOG_LEVEL para 0 ou 3 e execute de novo para ver o próprio programa mudar de forma.
A expressão é avaliada pelo pré-processador, o que significa que ela só pode usar constantes inteiras, operadores aritméticos e de comparação, e macros que expandem para isso. Ela não enxerga sizeof, valores de enum, variáveis ou qualquer coisa que precise do compilador. Uma macro não definida dentro de um #if avalia como 0 em vez de ser um erro, o que é conveniente e às vezes surpreendente:
#if FEATURE_X /* FEATURE_X nunca é definida em lugar nenhum -> 0 -> bloco descartado */
defined(NOME) transforma a pergunta do #ifdef em algo utilizável dentro de um #if, de modo que você pode combinar testes:
#if defined(DEBUG) && !defined(NDEBUG)
/* build de depuração, e as asserções estão ligadas */
#endif
#if defined(LINUX) || defined(BSD)
/* qualquer sistema do tipo unix */
#endif
#if defined(X) e #ifdef X significam a mesma coisa; recorra ao primeiro quando precisar de &&, || ou !.
Definir macros pela linha de comando
O verdadeiro poder dessas chaves é que o código-fonte não precisa mudar em nada. gcc -D define uma macro para toda a compilação:
gcc -DDEBUG program.c -o program # DEBUG definida como 1
gcc -DLOG_LEVEL=3 program.c -o program # valor específico
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app
-DNOME sozinho equivale a #define NOME 1. -U NOME remove a definição de uma, o que importa quando um cabeçalho define algo que você quer desligado.
Então o arranjo usual é: o código-fonte contém padrões protegidos por #ifndef, e o comando de build seleciona uma configuração.
/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 /* quieto por padrão */
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# build de desenvolvimento
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev
# build de produção
gcc -DNDEBUG -O2 src/*.c -o app
NDEBUG é padronizada: defini-la desativa todo assert() do programa, porque o próprio <assert.h> é escrito com compilação condicional. Esse é o padrão em miniatura - um cabeçalho que compila para coisas diferentes dependendo do que o build definiu.
Chaves de plataforma
Os compiladores predefinem macros que identificam o sistema alvo, de modo que um único arquivo-fonte pode chamar a API certa em cada um:
#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 "Plataforma não suportada"
#endif
As mais comuns: _WIN32 (definida tanto no Windows de 32 quanto no de 64 bits), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. A identidade do compilador tem seu próprio conjunto - __GNUC__, __clang__, _MSC_VER - e a arquitetura também - __x86_64__, __aarch64__.
Uma versão executável que informa em que ambiente foi compilada:
O #error vale conhecer por si só: ele interrompe a compilação com a sua mensagem. Terminar uma cadeia de plataformas com #else / #error "Plataforma não suportada" transforma uma compilação errada e silenciosa numa falha clara no topo do log de build.
Para listar todas as macros que seu compilador predefine:
gcc -dM -E - < /dev/null
Essa saída é a resposta definitiva para "o que eu posso testar nesta máquina".
#if 0 para desativar código
Comentar um bloco com /* ... */ falha no momento em que o bloco contém um comentário próprio, porque os comentários em C não aninham - o primeiro */ interno encerra o comentário externo e tudo depois dele vira código solto. #if 0 não tem esse problema:
#if 0
/* Toda esta região é removida, comentários e tudo. */
legacy_init();
int n = old_calculation(42); /* até este comentário está tranquilo */
report(n);
#endif
Troque para #if 1 para trazer o código de volta. Os editores continuam realçando como C, e você pode aninhar um #if 0 dentro de outro #if.
É uma ferramenta de depuração, não um sistema de armazenamento. Código parado num #if 0 nunca é compilado, então ele apodrece em silêncio - não vai compilar quando alguém finalmente virar a chave. Use enquanto estiver bissecionando um problema, depois apague o bloco e deixe o controle de versão se lembrar dele.
Onde a compilação condicional dá errado
Três modos de falha respondem pela maior parte da dor.
Código que nunca é compilado nunca é verificado. Um erro de digitação dentro de um ramo #ifdef inativo fica invisível até alguém compilar aquela configuração - possivelmente meses depois, possivelmente na CI, numa plataforma que você não tem. Se um projeto tem ramos de plataforma interessantes, compile todos eles regularmente.
Intercalar #ifdef com o fluxo de controle fica ilegível rapidinho. Uma coisa desse tipo é difícil de raciocinar:
if (ready) {
#ifdef FAST_PATH
fast_send(buf);
} else {
#endif
slow_send(buf);
}
Chaves abertas em um ramo e fechadas em outro são legais e horríveis. Prefira condicionais que envolvam funções inteiras e selecione entre elas:
#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
Um if em tempo de execução costuma ser melhor. Se os dois ramos compilariam em qualquer lugar, um if (debug_enabled) comum mantém os dois caminhos com os tipos verificados, testáveis e alternáveis sem recompilar. Reserve o pré-processador para o que só ele pode fazer: código que genuinamente não consegue compilar na outra plataforma.
Para o padrão que combina tudo isso - um cabeçalho de padrões, um -D por build e ramos de plataforma envolvidos em guardas de inclusão - veja arquivos de cabeçalho; para o #define em si, macros.
Perguntas frequentes
O que é compilação condicional em C?
É usar diretivas de pré-processamento para manter ou descartar blocos de código-fonte antes de o compilador rodar. Código dentro de um #ifdef que falha é apagado do texto por completo - nunca é analisado, nunca é compilado e nunca aparece no executável. É assim que um único arquivo-fonte suporta várias plataformas ou tipos de build.
Qual é a diferença entre #ifdef e #if?
#ifdef NOME pergunta apenas se a macro existe, seja qual for o valor dela. #if expressão avalia uma expressão inteira constante, então consegue comparar valores: #if VERSION >= 3. Use #ifdef para sinalizadores que só estão presentes ou ausentes, e #if quando o valor importa. #if defined(NOME) combina os dois e pode ser unido com && e ||.
Como defino uma macro pela linha de comando com o gcc?
Use -D: gcc -DDEBUG program.c define DEBUG como se o arquivo começasse com #define DEBUG 1, e -DMAX=50 dá a ela um valor específico. É assim que os sistemas de build ligam funcionalidades sem editar o código-fonte, e -U NOME remove a definição de uma.
Por que usar #if 0 em vez de comentar o código?
Porque os comentários /* ... */ não aninham - o primeiro */ dentro da região encerra o comentário cedo demais e o resto vira código quebrado. #if 0 ... #endif remove qualquer quantidade de código contendo qualquer número de comentários, mantém o realce de sintaxe funcionando e é trivialmente revertido para #if 1.