C Variadic Macros

Creating flexible macros with variable arguments

🔧 What are Variadic Macros?

Variadic macros accept a variable number of arguments using ellipsis (...). They're perfect for creating flexible debugging tools, logging systems, and wrapper functions that can handle different parameter counts efficiently.


#include <stdio.h>

// Simple variadic macro for debugging
#define DEBUG_PRINT(format, ...) \
    printf("[DEBUG] " format "\n", ##__VA_ARGS__)

int main() {
    int x = 42;
    char* name = "Alice";
    
    DEBUG_PRINT("Starting program");
    DEBUG_PRINT("Value: %d", x);
    DEBUG_PRINT("User: %s, Age: %d", name, 25);
    
    return 0;
}
                                    

Output:

[DEBUG] Starting program
[DEBUG] Value: 42
[DEBUG] User: Alice, Age: 25

Key Variadic Macro Concepts

📝

Ellipsis (...)

Represents variable arguments

#define MACRO(fmt, ...) \
    printf(fmt, __VA_ARGS__)
🔍

__VA_ARGS__

Expands to the variable arguments

#define LOG(msg, ...) \
    fprintf(stderr, msg, __VA_ARGS__)
🛡️

## Operator

Handles empty argument lists

#define SAFE_PRINT(fmt, ...) \
    printf(fmt, ##__VA_ARGS__)
🎯

Flexibility

One macro, multiple argument counts

#define TRACE(...) \
    printf(__VA_ARGS__)

🔹 Basic Variadic Macro Syntax

Variadic macros allow preprocessor macros to accept a variable number of arguments, providing flexibility similar to variadic functions. The syntax uses ... to represent variable arguments and __VA_ARGS__ to expand them. For example, #define LOG(format, ...) printf(format, __VA_ARGS__) creates a logging macro that accepts any number of arguments after the format string. This feature enables creating flexible debugging macros, type-safe printf wrappers, and assertion systems. The __VA_OPT__ extension (C23) helps handle cases where no variadic arguments are provided. Variadic macros are particularly useful for building diagnostic systems, creating domain-specific languages, and implementing compile-time code generation without runtime overhead.

#include <stdio.h>

// Basic variadic macro structure
#define PRINT_VALUES(format, ...) \
    printf("Values: " format "\n", __VA_ARGS__)

// Macro with safety for empty arguments
#define SAFE_LOG(level, format, ...) \
    printf("[%s] " format "\n", level, ##__VA_ARGS__)

int main() {
    // Using with multiple arguments
    PRINT_VALUES("x=%d, y=%d", 10, 20);
    
    // Using with single argument
    PRINT_VALUES("name=%s", "John");
    
    // Safe macro with no extra arguments
    SAFE_LOG("INFO", "Application started");
    SAFE_LOG("ERROR", "Failed with code %d", 404);
    
    return 0;
}

Output:

Values: x=10, y=20
Values: name=John
[INFO] Application started
[ERROR] Failed with code 404

🔹 Practical Examples

Real-world applications of variadic macros include debugging systems, logging frameworks, test harnesses, and compile-time code generation utilities. Common uses include creating assert macros with automatic file/line information, building flexible logging systems with severity levels and formatting, implementing printf-like functions with type safety checks, and generating repetitive code patterns. For example, #define ASSERT(condition, ...) if(!(condition)) report_error(__FILE__, __LINE__, __VA_ARGS__) creates rich assertion messages. Variadic macros excel in scenarios requiring compile-time text manipulation or when wrapping existing variadic functions. They're widely used in testing frameworks like Unity, logging libraries, and serialization code generators where compile-time flexibility reduces runtime overhead and improves developer productivity.

🔸 Debug Logging System

#include <stdio.h>
#include <time.h>

// Advanced logging macro with timestamp
#define LOG_WITH_TIME(level, format, ...) do { \
    time_t now = time(NULL); \
    char* timestr = ctime(&now); \
    timestr[strlen(timestr)-1] = '\0'; /* Remove newline */ \
    printf("[%s] %s: " format "\n", timestr, level, ##__VA_ARGS__); \
} while(0)

// Conditional debug macro
#ifdef DEBUG
    #define DBG(format, ...) \
        printf("[DEBUG %s:%d] " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
    #define DBG(format, ...) /* Nothing */
#endif

int main() {
    LOG_WITH_TIME("INFO", "Program started");
    LOG_WITH_TIME("WARNING", "Low memory: %d%% used", 85);
    
    int user_id = 123;
    DBG("Processing user %d", user_id);
    
    return 0;
}

🔸 Assert Macro with Custom Messages

#include <stdio.h>
#include <stdlib.h>

// Custom assert macro with formatted message
#define ASSERT_MSG(condition, format, ...) do { \
    if (!(condition)) { \
        printf("Assertion failed: %s\n", #condition); \
        printf("Message: " format "\n", ##__VA_ARGS__); \
        printf("File: %s, Line: %d\n", __FILE__, __LINE__); \
        exit(1); \
    } \
} while(0)

int divide(int a, int b) {
    ASSERT_MSG(b != 0, "Division by zero: %d / %d", a, b);
    return a / b;
}

int main() {
    int result = divide(10, 2);
    printf("Result: %d\n", result);
    
    // This would trigger the assertion
    // divide(10, 0);
    
    return 0;
}

🔹 Best Practices

Following variadic macro best practices ensures maintainable, safe, and efficient preprocessor code that avoids common pitfalls and debugging challenges. Always use parentheses around macro arguments to prevent operator precedence issues. Document the expected argument types since macros lack type checking. Consider using inline functions instead of macros when type safety is critical. Use do { ... } while(0) wrapping for multi-statement macros to ensure they work correctly in all contexts. Avoid side effects in macro arguments as they may be evaluated multiple times. Prefix macro names with project-specific identifiers to prevent naming conflicts. Use __VA_OPT__ (C23) for handling optional trailing commas. Test macros thoroughly with various argument combinations and edge cases.

✅ Do:

  • Use ##__VA_ARGS__: Handles empty argument lists safely
  • Wrap in do-while(0): For multi-statement macros
  • Add parentheses: Around macro parameters
  • Use for logging: Perfect for debug and error messages

❌ Don't:

  • Forget type safety: Macros don't check types
  • Make them too complex: Keep logic simple
  • Use for performance-critical code: Consider inline functions
// Good: Safe variadic macro
#define GOOD_MACRO(fmt, ...) do { \
    printf("[LOG] " fmt "\n", ##__VA_ARGS__); \
} while(0)

// Problematic: No safety for empty args
#define BAD_MACRO(fmt, ...) \
    printf(fmt, __VA_ARGS__)  // Fails if no args after fmt

🧠 Test Your Knowledge

What does __VA_ARGS__ represent in a variadic macro?