They see me printfin' they hatin'

Jul 8, 2014 • Guilherme Lampert


Gotta tell you folks, I’m a C++ coder that uses printf-like functions. Jokes apart, there are some serious holy wars involving the use of those format functions in C++, functions like std::printf(), std::sscanf() and similar C functions that take a variable amount of parameters.

Sure C++ has its << >> stream operators that can do the same job in a way nobody can easily read, but I just think that nothing beats the simplicity and clarity of a printf:

const char * name = "Lampert";
const int answer  = 42;

std::printf("Hello %s, the answer is: %i\n", name, answer);

I mean, there is no way you can’t read that, even if you come from a language that lets you concatenate strings with the + operator. Now compare that to an std::cout:

const char * name = "Lampert";
const int answer  = 42;

std::cout << "Hello " << name << ", the answer is: " << answer << "\n";

Actually, that example doesn’t look that bad in the std::cout version. Now how about printing a simple hexadecimal number? Things start to get worse for Mister cout:

std::printf("0x%04x\n", 0x424);

// Versus:

std::cout << "0x" << std::hex << std::setfill('0') << std::setw(4) << 0x424 << "\n";

The main problem with these variadic or “format” functions, however, is that they make it super easy for you to shoot yourself in the foot.

I’ll always remember a day when a friend asked me for help on how to parse a string read from a file. I suggested that he used std::sscanf(). He did, only he was new to these functions (coming from C# to C++) and tried to pass an std::string object to the function. The compiler accepted that happily, but of course, the state of the program was corrupted after the sscanf call returned. Variadic functions, being inherited from C, can only deal with native types. They don’t understand the concept of C++ objects and would try to treat them as raw bytes.

There are several replacements for the old C format functions these days, Boost provides a fantastic Formating Library. With C++11, we can now implement safer templated printf-like functions that accept any data type and are type-safe.

But way before Boost and C++11 there was a way to avoid errors like the one I’ve described above. GCC has the __attribute__((format)) extension. Clang on OSX also fully supports it.

So if you declare your format functions as:

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

And try do do something silly like passing a C++ string to it, the compiler will pull your years:

std::string name = "Lampert";
const int answer = 42;

my_printf("Hello %s, the answer is: %d\n", name, answer);

/*
$ clang++ test.cpp

test.cpp:12:45: error: cannot pass non-POD object of type 'std::string'
   (aka 'basic_string) to variadic function; expected type from
      format string was 'char *' [-Wnon-pod-varargs]
        my_printf("Hello %s, the answer is: %d\n", name, answer);
                         ~~                        ^~~~

test.cpp:12:45: note: did you mean to call the c_str() method?
        my_printf("Hello %s, the answer is: %d\n", name, answer);
                          ^
*/

Yey! The compiler is doing its job! You will be able to get some sleep tonight. This also produces a warning if you pass types that are incompatible with the format flag in the string, such as mismatching int (%d/%i) with float|double (%f/%lf).

const float f = 3.141592;
my_printf("f is %d", f);

/*
$ clang++ test.cpp

test.cpp:10:22: warning: format specifies type 'int'
   but the argument has type 'float' [-Wformat]
        my_printf("f is %d", f);
                        ~~   ^
                        %f
*/

__attribute__((format)) can be used with any type of function, including class methods. But there is one little detail when using it in a class. A class method has an implicit first parameter, the this pointer. So you need to increment the string-index and first-to-check of format() by one:

class DebugLog
{
public:
    void logComment(const char * format, ...) __attribute__((format(printf, 2, 3)));
};

In fact, this is such a basic feature that even Microsoft has finally added it to Visual Studio with the SA_FormatString annotation:

#include <CodeAnalysis\SourceAnnotations.h>

void my_printf([SA_FormatString(Style="printf")] const char * format, ...);

A slightly weird syntax, but does the job.

So if you love variadic function like I do, then do yourself a favor and use these source annotations/attributes and let the compiler do the validation for you (and don’t ignore the warnings when you get them, obviously!).