How to tell your compiler that you have implemented printf-like functions

Table of contents

Functions such as printf
have been a hallmark of C. These variadic functions accept a variable number of arguments as input and convert them into a character sequence according to a format string. For example:
int temperature = 20;
// ... possibly recalculate the variable temperature.
printf("Room temperature at %u\n", temperature);
An experienced programmer will recognize the subtle bug in the above example. The program intends to print a line of the form Room temperature at 20
or whatever the value of the variable temperature
is. That seems to work well when the argument is a positive number: the format string Room temperature at %u
should indicate that printf should fetch an integer from the stack (or register) and print its value.
But here is the bug: the format specifier %u
tells printf to fetch an unsigned integer even though the argument provided is a signed integer. That will not cause difficulties if the signed integer is positive, at least on mainstream CPU targets: the positive integers within the range of signed int
have the same bit representations when represented in unsigned int
, and the printed characters will be the same.
However, if the variable temperature
contains a negative value, then the interpretation as unsigned int
will be a large positive integer. And that is exactly what printf will display.
The code is still legal C. Hence, such bugs generally cannot be detected at compile-time. However, there are ways and means to mitigate the situation: we can enable a wide range of compiler flags when using GCC or Clang, which will trigger warnings in many situations where format strings are misused by the programmer.
For example, the above lines of code will trigger a warning if we enable the flag -Wformat-signedness
. In other words, GCC and Clang offer static code analysis functionality to specifically analyze the usage of the functions such as printf
, snprintf
, or vprintf
.
A previous article has discussed these options in greater detail.
Writing your own printf-style functions
Variadic functions are part of the C language, and we can easily write such functions ourselves. There are circumstances where we would like, more specifically, a variadic function that mimics the behavior of printf
. For example, the following function will behave like printf but prepend the prefix [LOG]
before each output:
void printf_log(const char *fmt, ...)
{
va_list params;
va_start(params, fmt);
fputs("[LOG] ", stdout);
vprintf(fmt, params);
va_end(params);
}
Let us break down the code. The function contains one named parameter: the format string fmt
of type const char*
. The ellipsis ... marks the function as variadic, so it can take any number of further optional arguments.
How do we access these additional parameters? In our case, we want to forward these to some printf-like function. First, we declare a variable params
whose type is va_list
: this type can hold information about variadic arguments and is implementation-defined. We do not need to know its further details.
The call va_start(params,fmt)
invokes a macro: it indicates that the variadic parameters start after the parameter fmt
and stores that information in params
.
The call va_end(params)
is complementary to the invocation of va_start
: it is a macro that indicates that we are done with processing the variadic argument list.
The action takes place in the middle: after the output of our logging prefix, we forward the variadic argument list to vprintf
. The behavior of this function is identical to that of printf except that it directly receives the va_list
variable instead of the separate arguments.
Clearly, such functions are useful in practice. However, unlike the C standard library functions such as printf
, these user-declared functions do not enjoy the extensive warning options facilitated by GCC or Clang. That is a major drawback because we could accidentally write, say,
printf_log("The value of pi is ca. %d", 3.14159);
but receive no warning about such misuse. Here, the function will print gibberish because it interprets the bits of the double value 3.14159
as an int
, as indicated by %d
in the format string.
Fortunately, there is a remedy for that situation.
The format attribute
GCC and Clang offer the format attribute to indicate that a variadic function should behave like one of the functions in the standard library and, most crucially, trigger the same warning behavior whenever the format string does not match the provided arguments.
We explain the attribute with our example:
void printf_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)))
{
va_list params;
va_start(params, fmt);
fputs("[LOG] ", stdout);
vprintf(fmt, params);
va_end(params);
}
The attribute receives three arguments. The first argument tells the compiler that printf_log
receives arguments in the same relation with each other as the ones given to printf
. The second argument then specifies which of the parameters of printf_log
receive the format string. In our case, that is the first (1
) parameter. Lastly, we provide the argument with the position of the first parameter to be processed as the parameter list.
With that declaration, the compiler will know how to interpret the format string. It will issue the respective warnings whenever it detects some mismatch.
The attribute must be visible at the call site to enable the warnings, so we typically include this attribute at the declaration. The header declaration of our example will normally look like this:
void printf_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
With that, we are ready to go!
Subscribe to my newsletter
Read articles from Martin Licht directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
