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
Copy
// 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
Copy
// 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
Copy
#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
Copy
// 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
Copy
#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
Copy
#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
Copy
#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
Copy
#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.Copy
#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
Copy
#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
Copy
// 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
Copy
// 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
Copy
#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
Copy
// 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)
Copy
#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
Copy
#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)
Copy
#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
Copy
// 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