Question

Forward Variable Arguments in C with va_list and v Functions

cvariadic-functions

Question

Given a C function that accepts a variable number of arguments using ..., how can it call another function that needs those same arguments?

For example, how should debug_print forward the arguments associated with fmt to a formatting function?

void format_string(char *fmt, ...);

void debug_print(int dbg_lvl, char *fmt, ...) {
    format_string(fmt, /* how can the arguments from `...` be forwarded? */);
    fprintf(stdout, fmt);
}

The goal is to pass all arguments received by debug_print to another formatting or printing function correctly.

Short Answer

A C variadic function cannot pass its ... directly to another variadic function. Instead, it must read the arguments into a va_list and call a companion function that accepts that va_list, such as vfprintf, vsnprintf, or a custom vformat_string function.

Concept

C supports functions with a variable number of arguments through an ellipsis (...). Familiar examples include printf and fprintf.

However, ... is not a normal variable that can be forwarded. Inside the function body, those arguments are accessible only through the <stdarg.h> tools:

  • va_list stores the current position in the argument sequence.
  • va_start initializes that sequence.
  • va_arg reads a value of a specified type.
  • va_end cleans up after the sequence.
  • va_copy makes an independent copy when the same arguments must be used more than once.

Because direct forwarding is impossible, C library APIs commonly come in pairs:

Variadic functionva_list version
printfvprintf
fprintfvfprintf
sprintfvsprintf
snprintfvsnprintf

For your own API, use the same design: provide a format_string(const char *fmt, ...) wrapper and a vformat_string(const char *fmt, va_list args) implementation.

This matters for logging, error reporting, string formatting, and wrapper functions. It lets one function add behavior—such as a log level or timestamp—while preserving the formatting arguments supplied by its caller.

Mental Model

Think of ... as an unopened envelope containing an unknown number of typed values. You cannot hand the ... notation itself to another function; it is only part of a function declaration.

va_list is the handle used to inspect the envelope's contents. A function such as vfprintf is specifically designed to accept that handle and consume the values according to the format string.

If two operations both need to inspect the same envelope, make a duplicate handle with va_copy before either operation consumes it.

Take Quiz

Syntax and Examples

Include <stdarg.h> to work with variable arguments.

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

void debug_print(int dbg_lvl, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);

    fprintf(stdout, "[debug %d] ", dbg_lvl);
    vfprintf(stdout, fmt, args);

    va_end(args);
}

int main(void) {
    debug_print(2, "Connected to %s on port %d\n", "server.example", 443);
    return 0;
}

va_start(args, fmt) initializes args. The second argument must be the last named parameter before ..., which is fmt here.

vfprintf(stdout, fmt, args) uses fmt to determine the types and number of values to read from args. It produces:

[debug 2] Connected to server.example on port 443

Notice that fprintf(stdout, fmt) is not equivalent. It supplies only the format string and no formatting values, so a format such as %s or %d causes undefined behavior.

Step by Step Execution

Consider this call:

debug_print(1, "User %s has %d messages\n", "Mina", 3);

The function executes as follows:

void debug_print(int dbg_lvl, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);

    fprintf(stdout, "[debug %d] ", dbg_lvl);
    vfprintf(stdout, fmt, args);

    va_end(args);
}
  1. dbg_lvl receives 1.
  2. fmt points to "User %s has %d messages\n".
  3. The remaining values, "Mina" and 3, are available through the unnamed argument sequence.
  4. va_start(args, fmt) positions args at the first unnamed value.
  5. The first fprintf prints the fixed prefix: [debug 1] .
  6. vfprintf reads %s from fmt, obtains "Mina" from args, then reads %d and obtains 3.
  7. va_end(args) finishes use of the argument list.

The resulting output is:

[debug 1] User Mina has 3 messages

Real World Use Cases

  • Logging wrappers: Add severity levels, file names, timestamps, or request IDs around messages formatted like printf.
  • Error-reporting functions: Implement helpers such as report_error("Cannot open %s: %s", path, reason).
  • String-building APIs: Use vsnprintf to format a message into a caller-provided buffer or dynamically allocated string.
  • Console and file output: Forward a shared formatted message to stdout, a log file, or a platform-specific output destination.
  • Test diagnostics: Create assertion helpers that print detailed failure messages with formatting arguments.
Take Quiz

Real Codebase Usage

In real C code, the reusable function usually accepts va_list, while the public convenience function accepts ....

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

void vlog_message(FILE *stream, const char *fmt, va_list args) {
    vfprintf(stream, fmt, args);
}

void log_message(FILE *stream, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vlog_message(stream, fmt, args);
    va_end(args);
}

This pattern separates responsibilities:

  • log_message is easy for callers to use.
  • vlog_message is easy for other variadic wrappers to call.

A logging function often has an early-return guard to avoid formatting work when logging is disabled:

void debug_print(int dbg_lvl, const char *fmt, ...) {
    if (dbg_lvl > current_debug_level) {
        return;
    }

    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

If you need to write the same formatted arguments twice, copy the list first:

va_list args;
va_list copy;
va_start(args, fmt);
va_copy(copy, args);

vfprintf(stdout, fmt, args);
vfprintf(stderr, fmt, copy);

va_end(copy);
va_end(args);

Do not assume a va_list can safely be reused after a function has consumed it.

Common Mistakes

Trying to forward ... directly

This is not valid C:

void debug_print(const char *fmt, ...) {
    format_string(fmt, ...); /* Invalid */
}

The ellipsis can appear only in a function declaration or definition. Use va_list and a v... function instead.

Calling fprintf with only a format string

fprintf(stdout, fmt); /* Broken when fmt contains conversions such as %d */

If fmt contains conversion specifiers, fprintf expects matching additional arguments. Use:

vfprintf(stdout, fmt, args);

Omitting va_end

va_list args;
va_start(args, fmt);
vfprintf(stdout, fmt, args);
/* va_end(args) is missing */

Always call va_end for every successful va_start or va_copy.

Reusing a consumed va_list

vfprintf(stdout, fmt, args);
vfprintf(stderr, fmt, args); /* Not portable or reliable */

The first call may advance args. Use va_copy before the first consuming call.

Using the wrong last named parameter

void print_level(int level, const char *fmt, ...) {
    va_list args;
    va_start(args, level); /* Wrong */
}

The second argument to va_start must be the last parameter before ...:

va_start(args, fmt);

Mismatching the format string and values

debug_print(1, "%d\n", "hello"); /* Undefined behavior */

%d requires an int, not a string. The format string is the contract that tells vfprintf how to interpret each argument.

Comparisons

SituationUseWhy
A normal caller is providing valuesfprintf(stream, fmt, ...)Convenient call syntax with explicit arguments.
A variadic wrapper already has a va_listvfprintf(stream, fmt, args)Correctly forwards collected arguments.
Formatting into a fixed-size buffersnprintf(buffer, size, fmt, ...)Limits output length.
A wrapper formats into a fixed-size buffervsnprintf(buffer, size, fmt, args)va_list counterpart of snprintf.
You need to consume arguments onceOne va_listNo copy is necessary.
You need to consume arguments multiple timesva_copyEach consumer needs an independent traversal state.

printf-style variadic functions are appropriate when a format string describes the argument types. If the number and types of values are known at compile time, a regular function with explicit parameters is usually clearer and safer.

Take Quiz

Cheat Sheet

#include <stdarg.h>

void wrapper(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);      /* fmt is the last named parameter */

    vfprintf(stdout, fmt, args);

    va_end(args);
}
  • ... cannot be forwarded directly.
  • Convert unnamed arguments into a va_list with va_start.
  • Pass that list to a matching v... function: vprintf, vfprintf, vsnprintf, and so on.
  • Call va_end exactly once for each va_start and each va_copy.
  • A function that consumes va_list may advance it.
  • Use va_copy(copy, args) when two operations need the same values.
  • The format string and supplied argument types must match exactly.
  • Prefer const char *fmt when the function does not modify the format string.

FAQ

Can I pass ... from one C function to another directly?

No. The ... notation is not a value. Initialize a va_list, then call a function that accepts va_list.

Why are functions named vprintf and vfprintf?

The v indicates that the function accepts a va_list instead of a new ... sequence.

Can I call vfprintf twice with the same va_list?

Not reliably. The first call can consume or advance it. Make a copy with va_copy before the first call if both calls need the arguments.

Do I always need va_end?

Yes. Pair every va_start and va_copy with va_end, including on relevant error paths.

Can I write my own variadic forwarding API?

Yes. Provide both a variadic wrapper, such as log_message(fmt, ...), and a va_list version, such as vlog_message(fmt, va_list args).

Is fprintf(stdout, fmt) safe inside a variadic wrapper?

Only if fmt contains no conversion specifiers. Otherwise it is undefined behavior because the expected values are missing.

Should a format parameter be char * or const char *?

Use const char * unless the function intentionally modifies the format string. Formatting functions do not modify it.

Take Quiz

Mini Project

Description

Build a small debug logger that accepts printf-style messages, checks whether a message is enabled for the current debug level, and forwards the variable arguments safely to standard error.

Goal

Create a debug_print function that prints only enabled messages with a level prefix.

Requirements

  • Define a global or file-local current debug level.
  • Accept a debug level, a format string, and variable arguments.
  • Return early when the message level is disabled.
  • Print a prefix containing the message level.
  • Forward the formatting arguments with vfprintf.
  • Clean up the va_list correctly.
Take Quiz

Keep learning

Related questions

View more