Docs

Conditional Compilation

Conditional Compilation in C

Table of Contents

  1. •Introduction
  2. •Basic Conditional Directives
  3. •#ifdef and #ifndef
  4. •#if, #elif, #else, #endif
  5. •defined Operator
  6. •Conditional Compilation Use Cases
  7. •Platform-Specific Code
  8. •Debug and Release Builds
  9. •Feature Toggles
  10. •Version-Specific Code
  11. •Best Practices
  12. •Common Patterns

Introduction

Conditional compilation is a powerful preprocessor feature that allows you to include or exclude portions of code from compilation based on certain conditions. This is evaluated at compile-time, not runtime, meaning excluded code is never compiled and doesn't exist in the final binary.

Why Use Conditional Compilation?

  1. •Platform Portability: Write code that compiles on different operating systems
  2. •Debug vs Release: Include debugging code only in development builds
  3. •Feature Flags: Enable/disable features without changing code
  4. •Hardware Adaptation: Optimize for different hardware architectures
  5. •API Versions: Support multiple versions of libraries
  6. •Code Organization: Manage large codebases with optional components

Preprocessor vs Runtime Conditionals

AspectPreprocessorRuntime
EvaluationCompile-timeRuntime
Binary SizeExcluded code absentAll code present
PerformanceZero overheadCondition check overhead
FlexibilityFixed at compileCan change during execution
DebuggingCan't debug excluded codeAll code debuggable

Basic Conditional Directives

The C preprocessor provides several directives for conditional compilation:

Complete List of Directives

DirectivePurpose
#ifIf expression is true (non-zero)
#ifdefIf macro is defined
#ifndefIf macro is not defined
#elifElse if (another condition)
#elseElse (no condition)
#endifEnd of conditional block
#defineDefine a macro
#undefUndefine a macro

Basic Structure

#if condition
    // Code compiled if condition is true
#elif another_condition
    // Code compiled if another_condition is true
#else
    // Code compiled if all conditions are false
#endif

Simple Example

#define DEBUG 1

#if DEBUG
    printf("Debug mode is enabled\n");
#else
    printf("Release mode\n");
#endif

#ifdef and #ifndef

These directives check whether a macro is defined, regardless of its value.

#ifdef Syntax

#ifdef MACRO_NAME
    // Code compiled if MACRO_NAME is defined
#endif

#ifndef Syntax

#ifndef MACRO_NAME
    // Code compiled if MACRO_NAME is NOT defined
#endif

Examples

// Debug printing - only if DEBUG is defined
#ifdef DEBUG
    #define DEBUG_PRINT(msg) printf("[DEBUG] %s\n", msg)
#else
    #define DEBUG_PRINT(msg) ((void)0)
#endif

// Default configuration if not provided
#ifndef MAX_BUFFER_SIZE
    #define MAX_BUFFER_SIZE 1024
#endif

// Multiple conditions with #ifdef
#ifdef FEATURE_A
    void feature_a_function(void) {
        // Implementation
    }
#endif

#ifdef FEATURE_B
    void feature_b_function(void) {
        // Implementation
    }
#endif

Checking if Defined with Value

// These are different!
#define ENABLED 1
#define DISABLED 0
#define FEATURE    // Defined but no value (empty)

#ifdef ENABLED     // True - ENABLED is defined
#ifdef DISABLED    // True - DISABLED is defined
#ifdef FEATURE     // True - FEATURE is defined
#ifdef UNDEFINED   // False - not defined

#if ENABLED        // True - value is 1 (non-zero)
#if DISABLED       // False - value is 0
#if FEATURE        // Error or 0 - no value

#if, #elif, #else, #endif

The #if directive evaluates constant expressions at compile time.

Expression Operators

The following operators can be used in #if expressions:

| Operator | Description | | ----------------------- | ------------------------- | ----------------- | ----------------- | | ==, != | Equality comparison | | <, >, <=, >= | Relational comparison | | &&, | |, ! | Logical operators | | +, -, *, /, % | Arithmetic operators | | &, |, ^, ~ | Bitwise operators | | <<, >> | Shift operators | | defined() | Check if macro is defined |

Basic #if Examples

#define VERSION 2

#if VERSION == 1
    printf("Version 1\n");
#elif VERSION == 2
    printf("Version 2\n");
#elif VERSION >= 3
    printf("Version 3 or later\n");
#else
    printf("Unknown version\n");
#endif

Compound Conditions

#define MAJOR_VERSION 2
#define MINOR_VERSION 5
#define PLATFORM_LINUX 1

#if MAJOR_VERSION > 2 || (MAJOR_VERSION == 2 && MINOR_VERSION >= 5)
    #define HAS_NEW_FEATURES 1
#endif

#if defined(PLATFORM_LINUX) && MAJOR_VERSION >= 2
    #define LINUX_OPTIMIZED 1
#endif

Nested Conditionals

#define OS_WINDOWS 0
#define OS_LINUX 1
#define OS_MACOS 0
#define ARCH_X86 0
#define ARCH_X64 1

#if OS_LINUX
    #if ARCH_X64
        #define PLATFORM_STRING "Linux x64"
    #elif ARCH_X86
        #define PLATFORM_STRING "Linux x86"
    #else
        #define PLATFORM_STRING "Linux (unknown arch)"
    #endif
#elif OS_WINDOWS
    #if ARCH_X64
        #define PLATFORM_STRING "Windows x64"
    #else
        #define PLATFORM_STRING "Windows x86"
    #endif
#elif OS_MACOS
    #define PLATFORM_STRING "macOS"
#else
    #define PLATFORM_STRING "Unknown platform"
#endif

defined Operator

The defined operator checks if a macro is defined and can be used in #if expressions.

Syntax

#if defined(MACRO_NAME)
// or
#if defined MACRO_NAME

Equivalence

// These are equivalent:
#ifdef DEBUG
#if defined(DEBUG)
#if defined DEBUG

// These are equivalent:
#ifndef DEBUG
#if !defined(DEBUG)
#if !defined DEBUG

Advantage of defined()

The defined operator can be combined with other conditions:

// This requires defined() - can't do with #ifdef alone
#if defined(DEBUG) && defined(VERBOSE)
    printf("Verbose debugging enabled\n");
#endif

// Complex condition
#if defined(WINDOWS) || defined(WIN32) || defined(_WIN32)
    #define IS_WINDOWS 1
#endif

// Negation with other conditions
#if !defined(NDEBUG) && LEVEL > 2
    #define ENABLE_EXTRA_CHECKS 1
#endif

Multiple Macro Checks

// Check for any of several macros
#if defined(__GNUC__) || defined(__clang__)
    #define COMPILER_GCC_COMPATIBLE 1
#endif

// Check that all required macros are defined
#if defined(CONFIG_A) && defined(CONFIG_B) && defined(CONFIG_C)
    #define FULL_CONFIG 1
#endif

// Feature detection pattern
#if defined(HAS_FEATURE_X) && HAS_FEATURE_X > 0
    // Use feature X
#endif

Conditional Compilation Use Cases

Use Case 1: Including/Excluding Code Sections

// Expensive assertions only in debug mode
#ifdef DEBUG
    #define ASSERT(cond) do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n", #cond); \
            fprintf(stderr, "File: %s, Line: %d\n", __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)
#else
    #define ASSERT(cond) ((void)0)
#endif

Use Case 2: Selecting Implementations

// Choose sorting algorithm at compile time
#if defined(USE_QUICKSORT)
    #include "quicksort.h"
    #define SORT(arr, n) quicksort(arr, n)
#elif defined(USE_MERGESORT)
    #include "mergesort.h"
    #define SORT(arr, n) mergesort(arr, n)
#else
    #include "bubblesort.h"
    #define SORT(arr, n) bubblesort(arr, n)
#endif

Use Case 3: Optional Dependencies

// Use OpenSSL if available, otherwise use built-in
#if defined(HAVE_OPENSSL)
    #include <openssl/sha.h>
    #define compute_hash(data, len) SHA256(data, len, NULL)
#else
    #include "simple_hash.h"
    #define compute_hash(data, len) simple_sha256(data, len)
#endif

Use Case 4: Compile-time Configuration

// Configurable buffer sizes
#ifndef INPUT_BUFFER_SIZE
    #define INPUT_BUFFER_SIZE 4096
#endif

#ifndef OUTPUT_BUFFER_SIZE
    #define OUTPUT_BUFFER_SIZE 8192
#endif

#ifndef MAX_CONNECTIONS
    #define MAX_CONNECTIONS 100
#endif

// Array sizing at compile time
char input_buffer[INPUT_BUFFER_SIZE];
char output_buffer[OUTPUT_BUFFER_SIZE];

Platform-Specific Code

Operating System Detection

// Detect operating system
#if defined(_WIN32) || defined(_WIN64)
    #define OS_WINDOWS 1
    #define OS_NAME "Windows"
#elif defined(__linux__)
    #define OS_LINUX 1
    #define OS_NAME "Linux"
#elif defined(__APPLE__) && defined(__MACH__)
    #define OS_MACOS 1
    #define OS_NAME "macOS"
#elif defined(__FreeBSD__)
    #define OS_FREEBSD 1
    #define OS_NAME "FreeBSD"
#elif defined(__unix__)
    #define OS_UNIX 1
    #define OS_NAME "Unix"
#else
    #define OS_UNKNOWN 1
    #define OS_NAME "Unknown"
#endif

Architecture Detection

// Detect CPU architecture
#if defined(__x86_64__) || defined(_M_X64)
    #define ARCH_X64 1
    #define ARCH_NAME "x86_64"
#elif defined(__i386__) || defined(_M_IX86)
    #define ARCH_X86 1
    #define ARCH_NAME "x86"
#elif defined(__aarch64__) || defined(_M_ARM64)
    #define ARCH_ARM64 1
    #define ARCH_NAME "ARM64"
#elif defined(__arm__) || defined(_M_ARM)
    #define ARCH_ARM 1
    #define ARCH_NAME "ARM"
#else
    #define ARCH_UNKNOWN 1
    #define ARCH_NAME "Unknown"
#endif

Platform-Specific Includes

#ifdef OS_WINDOWS
    #include <windows.h>
    #define SLEEP_MS(ms) Sleep(ms)
    #define PATH_SEPARATOR "\\"
#elif defined(OS_LINUX) || defined(OS_MACOS)
    #include <unistd.h>
    #define SLEEP_MS(ms) usleep((ms) * 1000)
    #define PATH_SEPARATOR "/"
#endif

Platform-Specific Functions

// Cross-platform file operations
#ifdef OS_WINDOWS
    int file_exists(const char *path) {
        DWORD attr = GetFileAttributesA(path);
        return (attr != INVALID_FILE_ATTRIBUTES);
    }
#else
    #include <sys/stat.h>
    int file_exists(const char *path) {
        struct stat st;
        return (stat(path, &st) == 0);
    }
#endif

// Cross-platform dynamic library loading
#ifdef OS_WINDOWS
    #include <windows.h>
    typedef HMODULE lib_handle_t;
    #define LIB_OPEN(name) LoadLibraryA(name)
    #define LIB_CLOSE(h) FreeLibrary(h)
    #define LIB_SYM(h, name) GetProcAddress(h, name)
#else
    #include <dlfcn.h>
    typedef void* lib_handle_t;
    #define LIB_OPEN(name) dlopen(name, RTLD_LAZY)
    #define LIB_CLOSE(h) dlclose(h)
    #define LIB_SYM(h, name) dlsym(h, name)
#endif

Debug and Release Builds

Debug Configuration

// Common debug setup
#ifdef DEBUG
    // Enable all assertions
    #undef NDEBUG

    // Enable verbose logging
    #define LOG_LEVEL 4  // Debug level

    // Enable memory tracking
    #define TRACK_ALLOCATIONS 1

    // Disable optimizations markers
    #define NO_INLINE __attribute__((noinline))
#else
    // Disable assertions
    #define NDEBUG 1

    // Minimal logging
    #define LOG_LEVEL 1  // Errors only

    // Disable tracking
    #define TRACK_ALLOCATIONS 0

    // Allow inlining
    #define NO_INLINE
#endif

#include <assert.h>

Debug Macros

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

    #define DEBUG_TRACE() \
        fprintf(stderr, "[TRACE] %s() in %s:%d\n", \
                __func__, __FILE__, __LINE__)

    #define DEBUG_ASSERT(cond, msg) do { \
        if (!(cond)) { \
            fprintf(stderr, "Assert failed: %s\n", msg); \
            fprintf(stderr, "Condition: %s\n", #cond); \
            fprintf(stderr, "Location: %s:%d\n", __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)

    #define DEBUG_DUMP_HEX(data, len) dump_hex(data, len)
#else
    #define DEBUG_LOG(fmt, ...) ((void)0)
    #define DEBUG_TRACE() ((void)0)
    #define DEBUG_ASSERT(cond, msg) ((void)0)
    #define DEBUG_DUMP_HEX(data, len) ((void)0)
#endif

Memory Debugging

#ifdef DEBUG
    // Track all allocations
    typedef struct {
        void *ptr;
        size_t size;
        const char *file;
        int line;
    } AllocationInfo;

    static AllocationInfo allocations[10000];
    static int allocation_count = 0;

    #define MALLOC(size) debug_malloc(size, __FILE__, __LINE__)
    #define FREE(ptr) debug_free(ptr, __FILE__, __LINE__)

    void *debug_malloc(size_t size, const char *file, int line);
    void debug_free(void *ptr, const char *file, int line);
    void dump_allocations(void);
#else
    #define MALLOC(size) malloc(size)
    #define FREE(ptr) free(ptr)
#endif

Feature Toggles

Feature Configuration Header

// features.h - Central feature configuration

// Core features
#define FEATURE_LOGGING 1
#define FEATURE_NETWORKING 1
#define FEATURE_DATABASE 0
#define FEATURE_GUI 0

// Optional enhancements
#define FEATURE_COMPRESSION 1
#define FEATURE_ENCRYPTION 0
#define FEATURE_CACHING 1

// Experimental features
#define FEATURE_EXPERIMENTAL_API 0
#define FEATURE_BETA_FEATURES 0

// Build variant
#define BUILD_LITE 0
#define BUILD_STANDARD 1
#define BUILD_PROFESSIONAL 0

// Derived configurations
#if BUILD_LITE
    #undef FEATURE_DATABASE
    #undef FEATURE_ENCRYPTION
    #undef FEATURE_CACHING
#elif BUILD_PROFESSIONAL
    #undef FEATURE_COMPRESSION
    #define FEATURE_COMPRESSION 1
    #undef FEATURE_ENCRYPTION
    #define FEATURE_ENCRYPTION 1
#endif

Using Feature Toggles

#include "features.h"

// Conditional includes
#if FEATURE_LOGGING
    #include "logging.h"
#endif

#if FEATURE_NETWORKING
    #include "network.h"
#endif

#if FEATURE_DATABASE
    #include "database.h"
#endif

// Conditional function definitions
void process_data(Data *data) {
    #if FEATURE_LOGGING
        log_info("Processing data...");
    #endif

    // Core processing
    transform(data);

    #if FEATURE_COMPRESSION
        compress(data);
    #endif

    #if FEATURE_ENCRYPTION
        encrypt(data);
    #endif

    #if FEATURE_CACHING
        cache_store(data);
    #endif

    #if FEATURE_LOGGING
        log_info("Processing complete");
    #endif
}

Feature Toggle Macros

// Helper macros for feature checking
#define IF_FEATURE(feature, code) \
    do { if (feature) { code } } while(0)

#define WHEN_ENABLED(feature) if (feature)

// Compile-time feature selection
#if FEATURE_ADVANCED_MATH
    #define SIN(x) fast_sin(x)
    #define COS(x) fast_cos(x)
#else
    #include <math.h>
    #define SIN(x) sin(x)
    #define COS(x) cos(x)
#endif

Version-Specific Code

Compiler Version Checks

// GCC version check
#ifdef __GNUC__
    #define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)

    #if GCC_VERSION >= 40800
        #define HAS_THREAD_LOCAL 1
    #endif

    #if GCC_VERSION >= 70000
        #define HAS_FALLTHROUGH_ATTR 1
    #endif
#endif

// Clang version check
#ifdef __clang__
    #define CLANG_VERSION (__clang_major__ * 10000 + __clang_minor__ * 100)

    #if CLANG_VERSION >= 30400
        #define HAS_NULLABILITY 1
    #endif
#endif

// MSVC version check
#ifdef _MSC_VER
    #if _MSC_VER >= 1900  // VS 2015
        #define HAS_CONSTEXPR 1
    #endif

    #if _MSC_VER >= 1910  // VS 2017
        #define HAS_STRUCTURED_BINDINGS 1
    #endif
#endif

C Standard Version Checks

// Check C standard version
#if defined(__STDC_VERSION__)
    #if __STDC_VERSION__ >= 201112L
        #define C11_OR_LATER 1
    #endif

    #if __STDC_VERSION__ >= 199901L
        #define C99_OR_LATER 1
    #endif
#endif

// Use C11 features if available
#ifdef C11_OR_LATER
    #define STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
    #define NORETURN _Noreturn
    #define ALIGNAS(n) _Alignas(n)
#else
    #define STATIC_ASSERT(cond, msg) \
        typedef char static_assertion_##__LINE__[(cond) ? 1 : -1]
    #define NORETURN
    #define ALIGNAS(n)
#endif

// C99 flexible array members
#ifdef C99_OR_LATER
    #define FLEX_ARRAY
#else
    #define FLEX_ARRAY 1
#endif

typedef struct {
    int count;
    int data[FLEX_ARRAY];  // Flexible array member
} FlexibleArray;

Library Version Checks

// OpenSSL version check
#ifdef OPENSSL_VERSION_NUMBER
    #if OPENSSL_VERSION_NUMBER >= 0x10100000L
        #define OPENSSL_1_1 1
    #else
        #define OPENSSL_1_0 1
    #endif
#endif

// API differences based on version
#ifdef OPENSSL_1_1
    #define SSL_CTX_CREATE() SSL_CTX_new(TLS_method())
#else
    #define SSL_CTX_CREATE() SSL_CTX_new(SSLv23_method())
#endif

Best Practices

1. Use Descriptive Macro Names

// Bad
#define F1 1
#define X 100

// Good
#define FEATURE_AUTHENTICATION 1
#define MAX_LOGIN_ATTEMPTS 100

2. Provide Defaults

// Always provide sensible defaults
#ifndef CONFIG_BUFFER_SIZE
    #define CONFIG_BUFFER_SIZE 1024
#endif

#ifndef CONFIG_TIMEOUT_MS
    #define CONFIG_TIMEOUT_MS 5000
#endif

#ifndef CONFIG_LOG_LEVEL
    #define CONFIG_LOG_LEVEL LOG_LEVEL_INFO
#endif

3. Document Conditional Sections

/*
 * Platform-specific network initialization
 * Windows uses Winsock, Unix uses BSD sockets
 */
#ifdef _WIN32
    /* Windows: Initialize Winsock */
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);
#else
    /* Unix: No initialization needed */
#endif

4. Keep Conditionals Simple

// Bad - complex inline condition
#if defined(DEBUG) && !defined(NDEBUG) && (LOG_LEVEL >= 3 || VERBOSE)
    // ...
#endif

// Good - break into named macros
#if defined(DEBUG) && !defined(NDEBUG)
    #define IS_DEBUG_BUILD 1
#else
    #define IS_DEBUG_BUILD 0
#endif

#if LOG_LEVEL >= 3 || defined(VERBOSE)
    #define IS_VERBOSE 1
#else
    #define IS_VERBOSE 0
#endif

#if IS_DEBUG_BUILD && IS_VERBOSE
    // Much clearer!
#endif

5. Use Include Guards

// header.h
#ifndef HEADER_H
#define HEADER_H

// Header content...

#endif /* HEADER_H */

6. Avoid Deep Nesting

// Bad - deeply nested
#ifdef A
    #ifdef B
        #ifdef C
            #ifdef D
                // Code
            #endif
        #endif
    #endif
#endif

// Good - combine conditions
#if defined(A) && defined(B) && defined(C) && defined(D)
    // Code
#endif

7. Use #error for Invalid Configurations

#if !defined(PLATFORM_WINDOWS) && !defined(PLATFORM_LINUX) && !defined(PLATFORM_MACOS)
    #error "No platform defined! Define PLATFORM_WINDOWS, PLATFORM_LINUX, or PLATFORM_MACOS"
#endif

#if defined(FEATURE_A) && defined(FEATURE_B)
    #error "FEATURE_A and FEATURE_B are mutually exclusive"
#endif

#if MAX_BUFFER < 256
    #error "MAX_BUFFER must be at least 256"
#endif

Common Patterns

Pattern 1: Include Guard

#ifndef MY_HEADER_H
#define MY_HEADER_H

// Header content

#endif /* MY_HEADER_H */

Pattern 2: C++ Compatibility

#ifdef __cplusplus
extern "C" {
#endif

// C declarations

#ifdef __cplusplus
}
#endif

Pattern 3: Optional Feature

#ifdef HAVE_FEATURE_X
    #define USE_FEATURE_X 1
    #include "feature_x.h"
#else
    #define USE_FEATURE_X 0
    // Provide stub or alternative
    static inline void feature_x_init(void) {}
    static inline void feature_x_cleanup(void) {}
#endif

Pattern 4: Debug/Release Toggle

#ifdef NDEBUG
    #define RELEASE_BUILD 1
    #define DEBUG_BUILD 0
#else
    #define RELEASE_BUILD 0
    #define DEBUG_BUILD 1
#endif

Pattern 5: Platform Abstraction

// platform.h
#ifdef _WIN32
    #include "platform_windows.h"
#elif defined(__linux__)
    #include "platform_linux.h"
#elif defined(__APPLE__)
    #include "platform_macos.h"
#else
    #error "Unsupported platform"
#endif

Pattern 6: Compiler-Specific Attributes

#ifdef __GNUC__
    #define UNUSED __attribute__((unused))
    #define PACKED __attribute__((packed))
    #define LIKELY(x) __builtin_expect(!!(x), 1)
    #define UNLIKELY(x) __builtin_expect(!!(x), 0)
#elif defined(_MSC_VER)
    #define UNUSED
    #define PACKED
    #define LIKELY(x) (x)
    #define UNLIKELY(x) (x)
#else
    #define UNUSED
    #define PACKED
    #define LIKELY(x) (x)
    #define UNLIKELY(x) (x)
#endif

Summary

Key Concepts

  1. •Conditional compilation selects code at compile-time
  2. •#ifdef/#ifndef check macro existence
  3. •#if/#elif/#else evaluate expressions
  4. •defined() operator enables complex conditions
  5. •Platform detection enables portable code
  6. •Feature toggles control optional functionality

Quick Reference

DirectiveUse Case
#ifdef XIf X is defined (any value)
#ifndef XIf X is not defined
#if XIf X evaluates to non-zero
#if defined(X)Same as #ifdef, but combinable
#elifElse-if (another condition)
#elseDefault case
#endifClose conditional block
#errorCompilation error message

Best Practices Summary

  1. •Use descriptive macro names
  2. •Always provide default values
  3. •Document conditional sections
  4. •Keep conditions simple and readable
  5. •Use #error for invalid configurations
  6. •Avoid deeply nested conditionals
  7. •Test all configuration combinations

This document is part of the C Programming Language course. For practical exercises, see the accompanying exercises.c file.

Conditional Compilation - C Programming Tutorial | DeepML