Skip to main content

Preprocessor Mastery

The C preprocessor is a powerful code transformation tool. Learn to use it effectively without creating maintenance nightmares.

Preprocessor Basics

How It Works

// The preprocessor runs BEFORE compilation
// It performs text substitution

// Preprocessing: gcc -E file.c -o file.i

// Stages:
// 1. Trigraph replacement (obsolete)
// 2. Line splicing (backslash-newline)
// 3. Tokenization
// 4. Macro expansion
// 5. #include processing
// 6. Conditional compilation

Include Guards

// header.h - Traditional guard
#ifndef HEADER_H
#define HEADER_H

// Header contents...

#endif // HEADER_H

// Modern alternative (non-standard but widely supported)
#pragma once

// Contents...

Include Paths

#include <stdio.h>    // Search system paths first
#include "myheader.h" // Search current directory first

// Compiler flags for include paths:
// gcc -I/path/to/headers
// gcc -isystem /path/to/system/headers (suppress warnings)

Macros

Object-like Macros

// Simple constants
#define MAX_SIZE 1024
#define PI 3.14159265358979

// Multi-line (use sparingly)
#define LONG_STRING "This is a very long string that " \
                    "spans multiple lines"

// Empty macro (for conditional compilation markers)
#define DEPRECATED

// Undef to remove
#undef MAX_SIZE

Function-like Macros

#include <stdio.h>

// Basic macro function
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x)    ((x) < 0 ? -(x) : (x))

// CRITICAL: Always parenthesize arguments AND the whole expression
#define BAD_SQUARE(x)  x * x           // BAD: BAD_SQUARE(1+2) = 1+2*1+2 = 5
#define GOOD_SQUARE(x) ((x) * (x))     // GOOD: GOOD_SQUARE(1+2) = ((1+2)*(1+2)) = 9

// DANGER: Side effects evaluated multiple times!
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5, y = 10;
int z = MAX(x++, y++);  // UNDEFINED! x++ and y++ may be evaluated twice

// Statement macro with do-while(0)
#define SWAP(a, b) do { \
    typeof(a) _tmp = (a); \
    (a) = (b); \
    (b) = _tmp; \
} while(0)

// Why do-while(0)?
if (condition)
    SWAP(x, y);  // Works correctly with any if/else
else
    other();

Variadic Macros

#include <stdio.h>

// C99 variadic macros
#define DEBUG_LOG(fmt, ...) \
    fprintf(stderr, "[DEBUG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// ##__VA_ARGS__ handles the case when no extra args
DEBUG_LOG("Starting");           // No extra args
DEBUG_LOG("Value: %d", 42);      // With args
DEBUG_LOG("x=%d, y=%d", x, y);   // Multiple args

// Generic print macro
#define PRINT(fmt, ...) printf(fmt "\n", ##__VA_ARGS__)

// Counting arguments (advanced)
#define COUNT_ARGS(...) COUNT_ARGS_(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define COUNT_ARGS_(_1, _2, _3, _4, _5, N, ...) N

// Usage: COUNT_ARGS(a, b, c) = 3

String Operations

Stringification

#include <stdio.h>

// # converts argument to string literal
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)  // Two-level for macro expansion

#define VERSION 2

int main(void) {
    printf("%s\n", STRINGIFY(VERSION));   // Prints: VERSION
    printf("%s\n", TOSTRING(VERSION));    // Prints: 2
    
    printf("%s\n", STRINGIFY(hello));     // Prints: hello
    printf("%s\n", STRINGIFY(1 + 2));     // Prints: 1 + 2
    
    return 0;
}

// Useful for assertion messages
#define ASSERT(expr) \
    do { \
        if (!(expr)) { \
            fprintf(stderr, "Assertion failed: %s\n" \
                           "  at %s:%d\n", \
                    #expr, __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)

Token Pasting

#include <stdio.h>

// ## concatenates tokens
#define CONCAT(a, b) a##b
#define MAKE_VAR(n) var_##n

int main(void) {
    int MAKE_VAR(1) = 10;  // Creates: int var_1 = 10;
    int MAKE_VAR(2) = 20;  // Creates: int var_2 = 20;
    
    printf("%d %d\n", var_1, var_2);
    
    return 0;
}

// Generating function names
#define DEFINE_GETTER(type, name) \
    type get_##name(void) { return name; }

static int value = 42;
DEFINE_GETTER(int, value)  // Creates: int get_value(void) { return value; }

// Generic min/max for different types
#define DEFINE_MINMAX(type) \
    static inline type type##_min(type a, type b) { return a < b ? a : b; } \
    static inline type type##_max(type a, type b) { return a > b ? a : b; }

DEFINE_MINMAX(int)
DEFINE_MINMAX(double)
// Creates: int_min, int_max, double_min, double_max

X-Macros

A powerful technique for maintaining parallel data structures.
#include <stdio.h>

// Define data once, use multiple ways
#define ERROR_CODES \
    X(OK,              0, "Success") \
    X(ERR_NOMEM,       1, "Out of memory") \
    X(ERR_INVALID,     2, "Invalid argument") \
    X(ERR_IO,          3, "I/O error") \
    X(ERR_TIMEOUT,     4, "Operation timed out")

// Generate enum
typedef enum {
    #define X(name, code, desc) name = code,
    ERROR_CODES
    #undef X
} ErrorCode;

// Generate string table
const char* error_strings[] = {
    #define X(name, code, desc) [code] = desc,
    ERROR_CODES
    #undef X
};

// Generate name table
const char* error_names[] = {
    #define X(name, code, desc) [code] = #name,
    ERROR_CODES
    #undef X
};

const char* error_to_string(ErrorCode e) {
    if (e >= 0 && e < sizeof(error_strings)/sizeof(error_strings[0])) {
        return error_strings[e];
    }
    return "Unknown error";
}

// States example
#define STATES \
    X(IDLE) \
    X(RUNNING) \
    X(PAUSED) \
    X(STOPPED)

typedef enum {
    #define X(s) STATE_##s,
    STATES
    #undef X
    STATE_COUNT
} State;

const char* state_names[] = {
    #define X(s) [STATE_##s] = #s,
    STATES
    #undef X
};

X-Macro for Struct Serialization

#define PERSON_FIELDS \
    X(char*, name,   "%s") \
    X(int,   age,    "%d") \
    X(float, height, "%.2f")

typedef struct {
    #define X(type, name, fmt) type name;
    PERSON_FIELDS
    #undef X
} Person;

void print_person(const Person* p) {
    #define X(type, name, fmt) printf(#name ": " fmt "\n", p->name);
    PERSON_FIELDS
    #undef X
}

// Generates:
// printf("name: %s\n", p->name);
// printf("age: %d\n", p->age);
// printf("height: %.2f\n", p->height);

Conditional Compilation

// Check if macro is defined
#ifdef DEBUG
    // Debug code
#endif

#ifndef NDEBUG
    // Code when NOT in release mode
#endif

// Check macro value
#if DEBUG_LEVEL >= 2
    // Verbose debugging
#elif DEBUG_LEVEL == 1
    // Basic debugging
#else
    // No debugging
#endif

// Logical operators
#if defined(LINUX) || defined(__linux__)
    // Linux-specific code
#elif defined(_WIN32)
    // Windows-specific code
#elif defined(__APPLE__)
    // macOS-specific code
#else
    #error "Unsupported platform"
#endif

// Check compiler
#if defined(__GNUC__)
    #define LIKELY(x)   __builtin_expect(!!(x), 1)
    #define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
    #define LIKELY(x)   (x)
    #define UNLIKELY(x) (x)
#endif

// Feature test macros (for POSIX features)
#define _GNU_SOURCE      // Enable GNU extensions
#define _POSIX_C_SOURCE 200809L

Platform Abstraction

// platform.h

#if defined(_WIN32)
    #define PLATFORM_WINDOWS 1
    #define PATH_SEPARATOR '\\'
    #define EXPORT __declspec(dllexport)
    #define IMPORT __declspec(dllimport)
    #include <windows.h>
    typedef HANDLE thread_t;
#elif defined(__linux__) || defined(__APPLE__)
    #define PLATFORM_POSIX 1
    #define PATH_SEPARATOR '/'
    #define EXPORT __attribute__((visibility("default")))
    #define IMPORT
    #include <pthread.h>
    typedef pthread_t thread_t;
#else
    #error "Unsupported platform"
#endif

// Compiler attributes
#if defined(__GNUC__) || defined(__clang__)
    #define UNUSED       __attribute__((unused))
    #define PACKED       __attribute__((packed))
    #define ALIGNED(n)   __attribute__((aligned(n)))
    #define NORETURN     __attribute__((noreturn))
    #define PRINTF_FMT(a, b) __attribute__((format(printf, a, b)))
#elif defined(_MSC_VER)
    #define UNUSED
    #define PACKED
    #define ALIGNED(n)   __declspec(align(n))
    #define NORETURN     __declspec(noreturn)
    #define PRINTF_FMT(a, b)
#endif

Predefined Macros

#include <stdio.h>

int main(void) {
    // Standard predefined macros
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Function: %s\n", __func__);  // C99, not preprocessor
    printf("Date: %s\n", __DATE__);
    printf("Time: %s\n", __TIME__);
    
    // Standard version
    #if __STDC_VERSION__ >= 201112L
        printf("C11 or later\n");
    #elif __STDC_VERSION__ >= 199901L
        printf("C99\n");
    #else
        printf("C89/C90\n");
    #endif
    
    // Compiler-specific
    #ifdef __GNUC__
        printf("GCC version: %d.%d.%d\n",
               __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
    #endif
    
    #ifdef __clang__
        printf("Clang version: %d.%d.%d\n",
               __clang_major__, __clang_minor__, __clang_patchlevel__);
    #endif
    
    // Counter (GCC/Clang extension)
    #ifdef __COUNTER__
        int unique1 = __COUNTER__;  // 0
        int unique2 = __COUNTER__;  // 1
        int unique3 = __COUNTER__;  // 2
    #endif
    
    return 0;
}

Advanced Techniques

Compile-Time Assertions

// C11 way
#include <assert.h>
static_assert(sizeof(int) == 4, "int must be 4 bytes");
static_assert(sizeof(void*) == 8, "Expected 64-bit pointers");

// Pre-C11 workaround
#define STATIC_ASSERT(cond, msg) \
    typedef char static_assertion_##msg[(cond) ? 1 : -1]

STATIC_ASSERT(sizeof(int) >= 4, int_too_small);
// If condition is false, creates array of size -1 → compilation error

Generic Selection (C11)

#include <stdio.h>
#include <math.h>

// Type-generic macro
#define ABS(x) _Generic((x), \
    int:    abs(x), \
    long:   labs(x), \
    float:  fabsf(x), \
    double: fabs(x), \
    default: (x) < 0 ? -(x) : (x))

#define PRINT_VALUE(x) _Generic((x), \
    int:    printf("%d\n", x), \
    float:  printf("%f\n", x), \
    double: printf("%lf\n", x), \
    char*:  printf("%s\n", x), \
    default: printf("Unknown type\n"))

int main(void) {
    PRINT_VALUE(42);        // Uses int branch
    PRINT_VALUE(3.14);      // Uses double branch
    PRINT_VALUE("hello");   // Uses char* branch
    
    return 0;
}

Debug Macros

#include <stdio.h>

#ifdef DEBUG
    #define DEBUG_PRINT(fmt, ...) \
        fprintf(stderr, "[DEBUG] %s:%d:%s(): " fmt "\n", \
                __FILE__, __LINE__, __func__, ##__VA_ARGS__)
    
    #define DEBUG_EXEC(code) code
    
    #define TRACE_ENTER() \
        fprintf(stderr, "[TRACE] Entering %s\n", __func__)
    
    #define TRACE_EXIT() \
        fprintf(stderr, "[TRACE] Exiting %s\n", __func__)
#else
    #define DEBUG_PRINT(fmt, ...) ((void)0)
    #define DEBUG_EXEC(code) ((void)0)
    #define TRACE_ENTER() ((void)0)
    #define TRACE_EXIT() ((void)0)
#endif

// Usage
void process(int x) {
    TRACE_ENTER();
    DEBUG_PRINT("x = %d", x);
    
    DEBUG_EXEC({
        // Only compiled in debug mode
        printf("Debug mode active\n");
    });
    
    TRACE_EXIT();
}

Container_of Macro (Linux Kernel Style)

#include <stddef.h>

// Get pointer to containing structure from member pointer
#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

typedef struct {
    int data;
} ListNode;

typedef struct {
    int id;
    char name[32];
    ListNode node;  // Embedded list node
} Entry;

void process_node(ListNode *n) {
    // Get the Entry containing this node
    Entry *e = container_of(n, Entry, node);
    printf("Entry id: %d, name: %s\n", e->id, e->name);
}

Best Practices

DO

  • Use ALL_CAPS for macro names
  • Parenthesize all arguments
  • Use do-while(0) for statement macros
  • Prefer inline functions when possible
  • Document complex macros

DON'T

  • Use macros for constants (use const/enum)
  • Create macros with side effects
  • Make macros that don’t look like expressions
  • Overuse macros when functions work
  • Forget to undef temporary macros
// BAD: Unclear, side effects
#define INC(x) x++
#define CHECK(x) if(x) fail()

// GOOD: Clear, safe
static inline void inc(int *x) { (*x)++; }
#define CHECK(x) do { if(x) fail(); } while(0)

Exercises

1

Safe Max Macro

Create a MAX macro that evaluates each argument only once (hint: use GCC’s statement expressions or make it type-specific).
2

X-Macro Commands

Create an X-macro for a command-line tool’s commands, generating the enum, help strings, and dispatch table.
3

Compile-Time Hash

Implement a compile-time string hash using recursive macros (advanced).
4

Cross-Platform Header

Create a platform.h that abstracts file operations (open/read/write/close) for Windows and POSIX.

Next Up

Data Structures in C

Implement fundamental data structures from scratch