I recently implemented a logging library in C and was reminded of how much simpler variadic functions are in C++.
Consider a common variadic use case: format strings with functions like printf
or an equivalent log
interface:
void log(const char *fmt, ...) __printf_check(1,2);
Inside the function implementation, you’ll see the C variadic constructs:
// Declare a va_list which holds info needed by variadic funtions
va_list argp;
// Access to the variadic arguments for our format string
va_start(argp, fmt);
// Do some work…
{ … }
// End use of variadic arguments
va_end(argp)
At this stage, I have no specific complaints about C variadic functions. However, my frustration arises when I need to pass the variadic arguments to another function. Your function counts explode: you need to provide a variadic function and a secondary implementation that accepts a va_list
as input.
You might think this is a strange use case: why exactly am I passing variadic arguments around?
Here are some specific reasons for my logging library to pass around variadic arguments:
- I wanted an option to echo
log
strings to the console usingprintf
- I wanted an upper-level
log
API that takes in a log level (for verbosity filtering) and internally adds decorations such as a timestamp. - I needed a base-level
log
function that writes bytes to the log buffer without adding decoration.- This is used to provide specialized functions such as
log_data
, which can nicely format the contents of a block of memory
- This is used to provide specialized functions such as
By providing multiple entry points into the log buffer, my total required function count quickly exploded.
Let’s start with my pretty_log implementation:
void pretty_log(int level, const char *fmt, ...)
{
if(level > log_level_)
{
return;
}
TickType_t t = xTaskGetTickCount();
// Add the timestamp to the log buffer
// This needs a variadic log function
log("[%lu] ", t);
// Parse the variadic arguments from the pretty_log input
va_list argp;
va_start(argp, fmt);
// Pass our variadic arguments to the va_list log function
logv(fmt, argp);
if(console_output_en_)
{
// We'll print timestamp to console in a variadic function
consolePrintf("[%lu] ", t);
//But we have a va_list for our pretty_log string
//so we need a console print that takes a va_list
console_vprint(fmt, argp);
}
va_end(argp);
}
This one function alone implies the need for two versions of our internal log
function. Let’s see what those look like:
// logv function, which takes in a format string and va_list
void logv(const char *fmt, va_list argp)
{
// Log the requested string using a va_list version of eprintf
evprintf(putc_logbuf_, fmt, argp);
}
// Variadic log function
void log(const char *fmt, ...)
{
va_list argp;
va_start(argp, fmt);
// Keep implementation simple - forward to logv
logv(fmt, argp);
va_end(argp);
}
We need to treat our consolePrint
function similarly and provide two versions.
void consolePrint(const char * fmt, ...);
void console_vprint(const char * fmt, va_list argp);
But it didn’t stop with console printing! Check the logv
function above: we also need a va_list
version of eprintf
since we are forwarding on our variadic arguments even further. The madness never ends!
C++ has similar variadic function support, but also provides variadic templates. A variadic template is simply a template that takes an arbitrary number of types. Let’s take a quick look at how they work. I’ve lifted a very helpful example from this StackOverflow article:
template<typename ...T>
void f(T ... args)
{
g( args... ); //pattern = args
h( x(args)... ); //pattern = x(args)
m( y(args...) ); //pattern = args (as argument to y())
n( z<T>(args)... ); //pattern = z<T>(args)
}
You’ll notice that the template specifier includes an ellipsis (…
), and so does the function invocation. These indicate a variadic template.
When inside our variadic template function, the ellipsis is used to unpack our parameters. The rule is that the pattern to the left of the …
is repeated, and the unpacked parameters are separated by commas. The pattern =
indicators above show you which form of the pattern will be repeated. Here’s a demonstration of the functions using three arguments as input:
g( arg0, arg1, arg2 );
h( x(arg0), x(arg1), x(arg2) );
m( y(arg0, arg1, arg2) );
n( z<int>(arg0), z<char>(arg1), z<short>(arg2) );
By using C++ variadic templates, we can simply our implementation to require one log
function, one eprintf
function, and one consolePrint
function. Let’s look at pretty_log
and log
using variadic templates:
template <typename ... Args>
void pretty_log(int level, char const * const format, Args const & ... args) noexcept
{
if(level > log_level_)
{
return;
}
TickType_t t = xTaskGetTickCount();
// Add the timestamp to the log buffer
log("[%lu] ", t);
//We'll just forward the args
log(fmt, args…);
if(console_output_en_)
{
consolePrintf("[%lu] ", t);
consolePrintf(fmt, args…);
}
}
template <typename ... Args>
void log(char const * const format, Args const & ... args) noexcept
{
eprintf(putc_logbuf_, args…);
}
Thanks to variadic templates, we can create a more straightforward implementation. I don’t know about you, but I certainly love the convenience and look forward to never writing another chain of paired variadic functions!
Further Reading
- C
- C Variadic Functions
- comp.lang.c FAQ List: Question 15.12 – regarding passing variadic arguments around
- StackOverflow: Forward an invocation of a variadic function in C
- C++
Using C++ Without the Heap
Want to use C++, but worried about how much it relies on dynamic memory allocations? Our course provides a hands-on approach for learning a diverse set of patterns, tools, and techniques for writing C++ code that never uses the heap.
Learn More on the Course Page
Great article, Phil! I was playing around with this recently and was struck by the similarities to the command-line arguments that are given to a “main” function when it’s invoked: int argc and char * argv[]. I could be wrong, but it seems that one solution to this might be to use that same style of variable arguments instead of the variadic arguments. I guess you’d have to add the overhead of needing to pack variables into an array of strings (and then unpack them when you want to use them), but it seems to me that you’d at least be able to have one function with one signature again. The C++ template solution is much cleaner, but maybe this will be useful for those of us that are sometimes using strictly C!