Compile-time checking of printf arguments in C++ Format

As an author of C++ Format, a library that implements safe Python-like and printf-like formatting, every now and then I hear questions whether it supports compile-time checking of format strings and arguments. Until recently I didn’t know any way to do this, but then it occurred to me that it is possible to have some compile-time checking based on GCC’s format attribute described here. This approach is somewhat limited and not particularly elegant, but it can be useful in some cases such as logging.

If you are not familiar with the GCC’s format attribute, here’s an example demonstrating how it works:

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

int main() {
  format("%s", 42);
}
$ g++ test.cc
test.cc: In function ‘int main()’:
test.cc:4:18: warning: format ‘%s’ expects argument of type ‘char*’, but argument 2 has type ‘int’ [-Wformat=]
   format("%s", 42);
                  ^

This is a pretty nice but, of course, only works with literal format strings. Unfortunately the format attribute requires varargs and doesn’t support variadic function templates:

template <typename... Args>
void format(const char *format_str, const Args& ... args)
  __attribute__((format(printf, 1, 2)));

int main() {
  format("%s", 42);
}
$ g++ -std=c++11 test.cc
test.cc: In substitution of ‘template<class ... Args> void format(const char*, const Args& ...) [with Args = {int}]’:
test.cc:6:18:   required from here
test.cc:2:6: error: args to be formatted is not ‘...’
 void format(const char *format_str, const Args& ... args)
      ^

and for obvious safety reasons C++ Format avoids varargs.

The main (and ugly) part of the solution is to use a macro with a call to a dummy vararg function declared with the format attribute and a call to the actual formatting function fmt::printf:

#include "format.h"

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

#define FMT_PRINTF(...) \
  if (false) check_args(__VA_ARGS__); \
  fmt::printf(__VA_ARGS__);

int main() {
  try {
    FMT_PRINTF("%s", 42);
  } catch (const std::exception &e) {
    fmt::print("error: {}\n", e.what());
  }
}

The check_args function is never called, so it doesn’t introduce runtime overheads or safety issues. But it makes the compiler do its magic:

$ g++ -std=c++11 test.cc
test.cc: In function ‘int main()’:
test.cc:6:36: warning: format ‘%s’ expects argument of type ‘char*’, but argument 2 has type ‘int’ [-Wformat=]
   if (false) check_args(__VA_ARGS__); \
                                    ^
test.cc:11:5: note: in expansion of macro ‘FMT_PRINTF’
     FMT_PRINTF("%s", 42);
     ^

One of the limitations of this method is that it can’t be used with objects of non-trivially-copyable C++ types such as std::string passed as formatting arguments.

Of course, this only gives a compile-time diagnostic for literal format string, but C++ Format got you covered in all cases with runtime type checking which is relatively cheap. Also if you use Python-like format strings, you can often omit type specifier in the format string which makes the check unnecessary:

fmt::print("{}", 42); // nothing to check here, any argument is fine

The approach described in this post has been successfully applied in the logging system of TrinityCore, a well-known open-source MMO framework.

comments powered by Disqus