C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingVariadic Functions (stdarg.h)
Advanced Topics

Variadic Functions

Master variadic functions with variable arguments. Learn va_list, va_start, va_arg, va_end, type-safe approaches, common patterns, and how printf/scanf work. Build flexible APIs accepting any number of arguments.

Understanding Variadic Functions

Variadic functions accept variable numbers of arguments. printf() and scanf() are famous examples. Use stdarg.h macros: va_list holds arguments, va_start initializes, va_arg retrieves next, va_end cleans up. No type safety - caller must know argument types.

C
#include <stdio.h>
#include <stdarg.h>

/* Basic variadic function */
void print_ints(int count, ...) {
    va_list args;          /* Argument list */
    va_start(args, count); /* Initialize with last named parameter */
    
    for (int i = 0; i < count; i++) {
        int value = va_arg(args, int);  /* Get next int */
        printf("%d ", value);
    }
    
    va_end(args);  /* Cleanup */
    printf("\n");
}

/* Usage */
void variadic_example(void) {
    print_ints(3, 10, 20, 30);        /* 10 20 30 */
    print_ints(5, 1, 2, 3, 4, 5);     /* 1 2 3 4 5 */
}

/* How printf works (simplified) */
int my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    int count = 0;
    for (const char *p = format; *p; p++) {
        if (*p == '%') {
            p++;  /* Skip % */
            switch (*p) {
                case 'd': {
                    int i = va_arg(args, int);
                    /* Print integer */
                    count += printf("%d", i);
                    break;
                }
                case 's': {
                    char *s = va_arg(args, char*);
                    /* Print string */
                    count += printf("%s", s);
                    break;
                }
                case 'f': {
                    double f = va_arg(args, double);
                    /* Print float */
                    count += printf("%f", f);
                    break;
                }
                /* More format specifiers... */
            }
        } else {
            putchar(*p);
            count++;
        }
    }
    
    va_end(args);
    return count;
}

/* Requirements for variadic functions */
/*
   1. At least one named parameter
   2. Named parameter must come before ...
   3. No type checking
   4. Caller must know argument count/types
   5. Default argument promotions apply:
      - char/short promoted to int
      - float promoted to double
*/

/* Syntax */
void func(int fixed, ...);  /* ... means variadic */

/* Invalid: No fixed parameter */
// void bad(...);  /* Error */

/* Invalid: ... must be last */
// void bad2(..., int x);  /* Error */

Stdarg Macros

stdarg.h provides va_list, va_start, va_arg, va_end, va_copy. Understanding these macros is essential for variadic functions. Order matters - call in sequence. Clean up with va_end prevents leaks.

C
#include <stdarg.h>

/* va_list - holds argument list */
va_list args;

/* va_start - initialize va_list */
va_start(args, last_named_param);

/* va_arg - get next argument */
int i = va_arg(args, int);
char *s = va_arg(args, char*);
double d = va_arg(args, double);

/* va_end - cleanup */
va_end(args);

/* va_copy - copy va_list (C99) */
va_list args2;
va_copy(args2, args);
/* Use args2 */
va_end(args2);

/* Complete example */
double sum_doubles(int count, ...) {
    va_list args;
    va_start(args, count);
    
    double total = 0.0;
    for (int i = 0; i < count; i++) {
        double value = va_arg(args, double);
        total += value;
    }
    
    va_end(args);
    return total;
}

/* Usage */
double s = sum_doubles(4, 1.5, 2.5, 3.5, 4.5);  /* 12.0 */

/* Multiple passes with va_copy */
void print_twice(const char *format, ...) {
    va_list args, args_copy;
    
    va_start(args, format);
    va_copy(args_copy, args);  /* Copy for second pass */
    
    /* First pass */
    vprintf(format, args);
    
    /* Second pass */
    vprintf(format, args_copy);
    
    va_end(args_copy);
    va_end(args);
}

/* Passing va_list to other functions */
void vlog_message(const char *format, va_list args) {
    printf("[LOG] ");
    vprintf(format, args);
    printf("\n");
}

void log_message(const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    vlog_message(format, args);  /* Forward va_list */
    
    va_end(args);
}

/* v-versions of standard functions */
/*
   vprintf(format, va_list)
   vsprintf(buffer, format, va_list)
   vsnprintf(buffer, size, format, va_list)
   vfprintf(file, format, va_list)
   vscanf(format, va_list)
   vsscanf(string, format, va_list)
*/

/* Safe wrapper example */
void error_log(const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    fprintf(stderr, "ERROR: ");
    vfprintf(stderr, format, args);
    fprintf(stderr, "\n");
    
    va_end(args);
}

/* Usage */
error_log("File not found: %s", filename);
error_log("Invalid value: %d (expected %d)", actual, expected);

/* Type promotion rules */
void promotion_example(void) {
    va_list args;
    /* char promoted to int */
    /* short promoted to int */
    /* float promoted to double */
    
    /* Always use promoted type */
    // char c = va_arg(args, char);    /* WRONG */
    int c = va_arg(args, int);         /* RIGHT */
    
    // float f = va_arg(args, float);  /* WRONG */
    double f = va_arg(args, double);   /* RIGHT */
}

Common Variadic Patterns

Several patterns communicate argument count/types: explicit count, sentinel value, format string, tagged arguments. Each has trade-offs. Choose based on use case. Format strings most flexible, sentinels simple, counts fastest.

C
/* Pattern 1: Explicit count */
int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    
    va_end(args);
    return total;
}

sum(3, 10, 20, 30);  /* Count first */

/* Pattern 2: Sentinel value */
int sum_sentinel(int first, ...) {
    va_list args;
    va_start(args, first);
    
    int total = first;
    int value;
    
    while ((value = va_arg(args, int)) != 0) {  /* 0 = end */
        total += value;
    }
    
    va_end(args);
    return total;
}

sum_sentinel(10, 20, 30, 0);  /* 0 terminates */

/* Pattern 3: Format string (like printf) */
void log_values(const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    for (const char *p = format; *p; p++) {
        switch (*p) {
            case 'i': {
                int i = va_arg(args, int);
                printf("%d ", i);
                break;
            }
            case 's': {
                char *s = va_arg(args, char*);
                printf("%s ", s);
                break;
            }
            case 'f': {
                double f = va_arg(args, double);
                printf("%f ", f);
                break;
            }
        }
    }
    
    va_end(args);
    printf("\n");
}

log_values("isi", 42, "hello", 30);  /* Format describes args */

/* Pattern 4: Tagged arguments */
typedef enum {
    ARG_END,
    ARG_INT,
    ARG_STRING,
    ARG_DOUBLE
} ArgType;

/* Standard C (before C23) requires at least one named parameter */
void print_args(ArgType first_type, ...) {
    va_list args;
    va_start(args, first_type);
    
    ArgType type = first_type;
    while (type != ARG_END) {
        switch (type) {
            case ARG_INT: {
                int i = va_arg(args, int);
                printf("%d ", i);
                break;
            }
            case ARG_STRING: {
                char *s = va_arg(args, char*);
                printf("%s ", s);
                break;
            }
            case ARG_DOUBLE: {
                double d = va_arg(args, double);
                printf("%f ", d);
                break;
            }
            case ARG_END:
                break;
        }
        type = va_arg(args, ArgType);
    }
    
    va_end(args);
    printf("\n");
}

print_args(ARG_INT, 42, ARG_STRING, "test", ARG_END);

/* Min/Max functions */
int min(int count, ...) {
    va_list args;
    va_start(args, count);
    
    int minimum = va_arg(args, int);
    
    for (int i = 1; i < count; i++) {
        int value = va_arg(args, int);
        if (value < minimum) {
            minimum = value;
        }
    }
    
    va_end(args);
    return minimum;
}

int m = min(5, 30, 10, 50, 20, 40);  /* 10 */

/* String concatenation */
char* concat(int count, ...) {
    va_list args;
    va_start(args, count);
    
    /* Calculate total length */
    size_t total_len = 0;
    va_list args_copy;
    va_copy(args_copy, args);
    
    for (int i = 0; i < count; i++) {
        const char *s = va_arg(args_copy, const char*);
        total_len += strlen(s);
    }
    va_end(args_copy);
    
    /* Allocate */
    char *result = malloc(total_len + 1);
    if (result == NULL) {
        va_end(args);
        return NULL;
    }
    
    /* Concatenate */
    result[0] = '\0';
    for (int i = 0; i < count; i++) {
        const char *s = va_arg(args, const char*);
        strcat(result, s);
    }
    
    va_end(args);
    return result;
}

char *str = concat(3, "Hello", " ", "World");  /* "Hello World" */
free(str);

/* Building command line */
char** build_args(int count, ...) {
    va_list args;
    va_start(args, count);
    
    char **argv = malloc((count + 1) * sizeof(char*));
    if (argv == NULL) {
        va_end(args);
        return NULL;
    }
    
    for (int i = 0; i < count; i++) {
        const char *arg = va_arg(args, const char*);
        argv[i] = strdup(arg);
    }
    argv[count] = NULL;  /* Null-terminate */
    
    va_end(args);
    return argv;
}

Type Safety and Error Handling

Variadic functions lack type safety - wrong types cause undefined behavior. Use format strings for type information. Validate arguments when possible. Consider alternatives: arrays, structures, or function pointers for type-safe variable arguments.

C
/* Problem: No type safety */
void unsafe(int count, ...) {
    va_list args;
    va_start(args, count);
    
    for (int i = 0; i < count; i++) {
        int x = va_arg(args, int);  /* Expects int */
        printf("%d ", x);
    }
    
    va_end(args);
}

unsafe(2, 10, 20);           /* OK */
unsafe(2, "hello", "world"); /* UNDEFINED BEHAVIOR! */

/* Solution 1: Format string */
void type_safe(const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    for (const char *p = format; *p; p++) {
        if (*p == 'd') {
            int i = va_arg(args, int);
            printf("%d ", i);
        } else if (*p == 's') {
            char *s = va_arg(args, char*);
            printf("%s ", s);
        }
    }
    
    va_end(args);
}

type_safe("dd", 10, 20);      /* OK */
type_safe("ss", "hi", "bye"); /* OK */

/* Solution 2: Tagged union */
typedef enum { TYPE_INT, TYPE_STRING, TYPE_DOUBLE } ValueType;

typedef struct {
    ValueType type;
    union {
        int i;
        char *s;
        double d;
    } data;
} Value;

void print_values_safe(int count, ...) {
    va_list args;
    va_start(args, count);
    
    for (int i = 0; i < count; i++) {
        Value v = va_arg(args, Value);
        switch (v.type) {
            case TYPE_INT:
                printf("%d ", v.data.i);
                break;
            case TYPE_STRING:
                printf("%s ", v.data.s);
                break;
            case TYPE_DOUBLE:
                printf("%f ", v.data.d);
                break;
        }
    }
    
    va_end(args);
}

Value v1 = {TYPE_INT, {.i = 42}};
Value v2 = {TYPE_STRING, {.s = "hello"}};
print_values_safe(2, v1, v2);  /* Type-safe */

/* Solution 3: Array + count */
void print_ints_array(const int *values, int count) {
    for (int i = 0; i < count; i++) {
        printf("%d ", values[i]);
    }
}

int arr[] = {10, 20, 30};
print_ints_array(arr, 3);  /* Type-safe */

/* Solution 4: Null-terminated array */
void print_strings(const char **strings) {
    while (*strings != NULL) {
        printf("%s ", *strings);
        strings++;
    }
}

const char *strs[] = {"hello", "world", NULL};
print_strings(strs);  /* Type-safe */

/* GCC format checking */
__attribute__((format(printf, 1, 2)))
void my_printf(const char *format, ...);

/* Wrong usage caught by compiler */
// my_printf("%d", "string");  /* Warning! */

/* Custom format checking */
#define CHECK_FORMAT __attribute__((format(printf, 1, 2)))

CHECK_FORMAT
void log_msg(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
}

/* Error handling */
int safe_sum(int count, ...) {
    if (count < 0) {
        return -1;  /* Invalid count */
    }
    
    va_list args;
    va_start(args, count);
    
    int total = 0;
    for (int i = 0; i < count; i++) {
        int value = va_arg(args, int);
        
        /* Check overflow */
        if (total &gt; INT_MAX - value) {
            va_end(args);
            return -1;  /* Overflow */
        }
        
        total += value;
    }
    
    va_end(args);
    return total;
}

/* When NOT to use variadic functions */
/*
   Avoid when:
   - Type safety critical
   - Performance critical
   - Simple API possible
   - Fixed arguments work
   
   Use when:
   - Printf-like formatting needed
   - Truly variable arguments
   - Convenience outweighs safety
   - Backward compatibility required
*/

Advanced Techniques

Advanced variadic patterns include wrappers around printf/scanf, building DSLs, dynamic function calls. Understanding these techniques enables powerful metaprogramming and flexible APIs.

C
/* Printf wrapper with timestamp */
void tprintf(const char *format, ...) {
    time_t now = time(NULL);
    printf("[%s] ", ctime(&now));
    
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
}

/* Logging levels */
typedef enum { LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG } LogLevel;

void log_msg(LogLevel level, const char *format, ...) {
    const char *level_str[] = {"ERROR", "WARN", "INFO", "DEBUG"};
    printf("[%s] ", level_str[level]);
    
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);
    
    printf("\n");
}

log_msg(LOG_ERROR, "Failed to open %s", filename);
log_msg(LOG_INFO, "Connected to %s:%d", host, port);

/* SQL-like query builder */
void execute_query(const char *sql, ...) {
    va_list args;
    va_start(args, sql);
    
    /* Build query with parameters */
    char query[1024];
    vsnprintf(query, sizeof(query), sql, args);
    
    va_end(args);
    
    /* Execute query */
    printf("Executing: %s\n", query);
}

execute_query("SELECT * FROM users WHERE id = %d", 42);
execute_query("INSERT INTO logs VALUES ('%s', %d)", "event", time(NULL));

/* Function call dispatcher */
typedef void (*Handler)(va_list args);

void dispatch(Handler handler, ...) {
    va_list args;
    va_start(args, handler);
    handler(args);  /* Forward to handler */
    va_end(args);
}

void int_handler(va_list args) {
    int a = va_arg(args, int);
    int b = va_arg(args, int);
    printf("Sum: %d\n", a + b);
}

dispatch(int_handler, 10, 20);

/* Building JSON (simplified) */
void json_object(int count, ...) {
    printf("{");
    
    va_list args;
    va_start(args, count);
    
    for (int i = 0; i < count; i++) {
        const char *key = va_arg(args, const char*);
        const char *value = va_arg(args, const char*);
        
        printf("\"%s\": \"%s\"", key, value);
        if (i < count - 1) {
            printf(", ");
        }
    }
    
    va_end(args);
    printf("}\n");
}

json_object(2, "name", "John", "age", "30");
/* {"name": "John", "age": "30"} */

/* Configuration builder */
typedef struct Config Config;

Config* config_new(const char *key, ...) {
    Config *cfg = malloc(sizeof(Config));
    if (cfg == NULL) {
        return NULL;
    }
    
    va_list args;
    va_start(args, key);
    
    const char *k = key;
    while (k != NULL) {
        const char *value = va_arg(args, const char*);
        /* Set config[k] = value */
        k = va_arg(args, const char*);
    }
    
    va_end(args);
    return cfg;
}

Config *cfg = config_new("host", "localhost",
                        "port", "8080",
                        NULL);

/* Test framework */
void run_tests(int count, ...) {
    va_list args;
    va_start(args, count);
    
    int passed = 0;
    for (int i = 0; i < count; i++) {
        void (*test)(void) = va_arg(args, void (*)(void));
        
        printf("Running test %d... ", i + 1);
        test();
        printf("PASSED\n");
        passed++;
    }
    
    va_end(args);
    printf("%d/%d tests passed\n", passed, count);
}

void test1(void) { /* test */ }
void test2(void) { /* test */ }

run_tests(2, test1, test2);

Summary & What's Next

Key Takeaways:

  • ✅ Variadic functions accept variable arguments
  • ✅ Use va_list, va_start, va_arg, va_end
  • ✅ Must have at least one named parameter
  • ✅ No type checking - caller responsibility
  • ✅ Use format strings to communicate types
  • ✅ va_copy for multiple passes (C99)
  • ✅ Prefer type-safe alternatives when possible
  • ✅ Use v-versions to forward va_list

What's Next?

Let's learn about function pointers and callbacks for flexible programming!