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_liststores the current position in the argument sequence.va_startinitializes that sequence.va_argreads a value of a specified type.va_endcleans up after the sequence.va_copymakes 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 function | va_list version |
|---|---|
printf | vprintf |
fprintf | vfprintf |
sprintf | vsprintf |
snprintf | vsnprintf |
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.
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);
}
dbg_lvlreceives1.fmtpoints to"User %s has %d messages\n".- The remaining values,
"Mina"and3, are available through the unnamed argument sequence. va_start(args, fmt)positionsargsat the first unnamed value.- The first
fprintfprints the fixed prefix:[debug 1]. vfprintfreads%sfromfmt, obtains"Mina"fromargs, then reads%dand obtains3.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
vsnprintfto 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.
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_messageis easy for callers to use.vlog_messageis 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
| Situation | Use | Why |
|---|---|---|
| A normal caller is providing values | fprintf(stream, fmt, ...) | Convenient call syntax with explicit arguments. |
A variadic wrapper already has a va_list | vfprintf(stream, fmt, args) | Correctly forwards collected arguments. |
| Formatting into a fixed-size buffer | snprintf(buffer, size, fmt, ...) | Limits output length. |
| A wrapper formats into a fixed-size buffer | vsnprintf(buffer, size, fmt, args) | va_list counterpart of snprintf. |
| You need to consume arguments once | One va_list | No copy is necessary. |
| You need to consume arguments multiple times | va_copy | Each 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.
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_listwithva_start. - Pass that list to a matching
v...function:vprintf,vfprintf,vsnprintf, and so on. - Call
va_endexactly once for eachva_startand eachva_copy. - A function that consumes
va_listmay 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 *fmtwhen 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.
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_listcorrectly.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.