## Compile time checking of printf-like format strings Inspired by [this open ticket](https://svn.boost.org/trac/boost/ticket/6815) on boost, this seeks to complete the work there Given a printf-style format string and associated arguments, a `static_assert` is performed on whether the format string and arguments are valid. I'm particularly interested in: - Have I covered all possible format strings? - Am I doing this in the most efficient way? ### Code: This includes changes based on the comments from [@Loki Astari](http://codereview.stackexchange.com/users/507/loki-astari) on the previous iteration of this code [here](http://codereview.stackexchange.com/a/84795/47528) [Here is the below code running on ideone](https://ideone.com/EzstNy) #include <cstddef> #include <cstdio> #include <stdexcept> #include <boost/utility/string_ref.hpp> #include <boost/format.hpp> #ifndef BOOST_PP_VARIADICS # define BOOST_PP_VARIADICS #endif #include <boost/preprocessor.hpp> template<typename... Ts> struct Format { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n); }; ////////////////////// template<std::size_t N> constexpr bool checkValidFormats(const char (&fmt)[N], size_t n, char c) { return n >= N ? throw std::logic_error("invalid format for type") : fmt[n] == c ? true : checkValidFormats(fmt, n + 1, c); } template<class> struct Type; #define SUPPORTED_TYPE(T, Fmts) \ template<> \ struct Type<T> \ { \ template<std::size_t N> \ constexpr static bool check(const char (&fmt)[N], std::size_t n) \ { \ return n >= N ? \ throw std::logic_error("invalid format for type") \ : checkValidFormats(Fmts, 0, fmt[n]); \ } \ } SUPPORTED_TYPE(char, "c"); SUPPORTED_TYPE(int8_t, "cd"); SUPPORTED_TYPE(uint8_t, "cu"); SUPPORTED_TYPE(int16_t, "d"); SUPPORTED_TYPE(uint16_t, "u"); SUPPORTED_TYPE(int32_t, "d"); SUPPORTED_TYPE(uint32_t, "u"); SUPPORTED_TYPE(char*, "s"); SUPPORTED_TYPE(unsigned char*, "s"); SUPPORTED_TYPE(const char*, "s"); SUPPORTED_TYPE(std::string, "s"); SUPPORTED_TYPE(boost::string_ref, "s"); SUPPORTED_TYPE(double, "f"); SUPPORTED_TYPE(float, "f"); #define SUPPORTED_LL_TYPE(T, C) \ template<> \ struct Type<T> \ { \ template<std::size_t N> \ static constexpr bool check(const char (&fmt)[N], std::size_t n) \ { \ return n < N && \ n - 2 >= 0 && \ fmt[n] == C && \ fmt[n - 1] == 'l' && \ fmt[n - 2] == 'l' ? \ true \ : throw std::logic_error("invalid format for type"); \ } \ } SUPPORTED_LL_TYPE(int64_t, 'd'); SUPPORTED_LL_TYPE(uint64_t, 'u'); template<typename... Ts> struct Argument { template<std::size_t N> static constexpr bool check(const char (&)[N], std::size_t) { return false; } }; template<typename T, typename... Ts> struct Argument<T, Ts...> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { // %[<flags>][<width>][.<precision>][<length>]<specifier> // specifier := d|i|u|o|x|X|f|F|e|E|g|G|a|A|c|s|p|n return Type< typename std::decay<T>::type>::check(fmt, n) && Format<Ts...>::check(fmt, n + 1); } }; /////////////////////////// template<size_t N> constexpr bool isDoubleLengthSpecifier(const char (&fmt)[N], std::size_t n) { // hh | ll return n + 2 < N && ((fmt[n] == 'h' && fmt[n + 1] == 'h') || (fmt[n] == 'l' && fmt[n + 1] == 'l')); } template<size_t N> constexpr bool isSingleLengthSpecifier(const char (&fmt)[N], std::size_t n) { // h | l | j | z | t | L return n + 1 < N && (fmt[n] == 'h' || fmt[n] == 'l' || fmt[n] == 'j' || fmt[n] == 'z' || fmt[n] == 't' || fmt[n] == 'L'); } template<size_t N> constexpr size_t nextNonLengthSpecifier(const char (&fmt)[N], std::size_t n) { return isDoubleLengthSpecifier(fmt, n) ? n + 2 : isSingleLengthSpecifier(fmt, n) ? n + 1 : n; } template<typename... Ts> struct Length { template<std::size_t N> static constexpr bool check(const char (&)[N], std::size_t) { return false; } }; template<typename T, typename... Ts> struct Length<T, Ts...> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { // %[<flags>][<width>][.<precision>][<length>]<specifier> // length := hh|h|l|ll|j|z|t|L return Argument<T, Ts...>::check(fmt, nextNonLengthSpecifier(fmt, n)); } }; /////////////////////////// template<std::size_t N> constexpr size_t nextNonLiteralPrecision(const char (&fmt)[N], std::size_t n) { return n >= N ? throw std::logic_error("invalid format string - parsing precision") : fmt[n] >= '0' && fmt[n] <= '9' ? nextNonLiteralPrecision(fmt, n + 1) : n; } template<typename... Ts> struct Precision { template<std::size_t N> static constexpr bool check(const char (&)[N], std::size_t) { return false; } }; template<typename T, typename... Ts> struct Precision<T, Ts...> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { // %[<flags>][<width>][.<precision>][<length>]<specifier> // precision := <number>|'*' // A number or a '*' // if precision is a provided argument, validate it is integral return n + 1 < N && fmt[n] == '.' && fmt[n + 1] == '*' ? std::is_integral<T>::value && Length<Ts...>::check(fmt, n + 2) // otherwise skip over any literal precision : n + 1 < N && fmt[n] == '.' ? Length<T, Ts...>::check(fmt, nextNonLiteralPrecision(fmt, n + 1)) : Length<T, Ts...>::check(fmt, n); } }; /////////////////////////// template<std::size_t N> constexpr size_t nextNonLiteralWidth(const char (&fmt)[N], std::size_t n) { return n >= N ? throw std::logic_error("invalid format string - parsing width") : fmt[n] >= '0' && fmt[n] <= '9' ? nextNonLiteralWidth(fmt, n + 1) : n; } template<typename... Ts> struct Width { template<std::size_t N> static constexpr bool check(const char (&)[N], std::size_t) { return false; } }; template<typename T, typename... Ts> struct Width<T, Ts...> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { // %[<flags>][<width>][.<precision>][<length>]<specifier> // width := <number>|'*' // A number or a '*' // if width is a provided argument, validate it is integral return fmt[n] == '*' ? std::is_integral<T>::value && Precision<Ts...>::check(fmt, n + 1) // otherwise skip over any literal width : Precision<T, Ts...>::check(fmt, nextNonLiteralWidth(fmt, n)); } }; /////////////////////////// template<size_t N> constexpr bool isFlag(const char (&fmt)[N], std::size_t n) { return n + 1 < N && (fmt[n] == '-' || fmt[n] == '+' || fmt[n] == ' ' || fmt[n] == '#' || fmt[n] == '0'); } template<std::size_t N> constexpr size_t nextNonFlag(const char (&fmt)[N], std::size_t n) { return n >= N ? throw std::logic_error("invalid format string") : isFlag(fmt, n) ? nextNonFlag(fmt, n + 1) : n; } template<typename T, typename... Ts> struct Flags { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { // %[<flags>][<width>][.<precision>][<length>]<specifier> // flags := [-+ #0]* // Zero or more return Width<T, Ts...>::check(fmt, nextNonFlag(fmt, n)); } }; /////////////////////////// template<size_t N> constexpr bool isLiteralPercent(const char (&fmt)[N], std::size_t n) { return n + 1 <= N && fmt[n] == '%' && fmt[n + 1] == '%'; } template<typename T, typename... Ts> struct Format<T, Ts...> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { return n >= N ? throw std::logic_error("too many arguments for provided format string") // skip non-format specifiers (ie: not a % character) : fmt[n] != '%' ? Format<T, Ts...>::check(fmt, n + 1) // %% : isLiteralPercent(fmt, n) ? Format<T, Ts...>::check(fmt, n + 2) // we've found a format specifier : Flags<T, Ts...>::check(fmt, n + 1); } }; template<> struct Format<> { template<std::size_t N> static constexpr bool check(const char (&fmt)[N], std::size_t n) { return n>= N ? true : fmt[n] != '%' ? check(fmt, n + 1) : fmt[n + 1] == '%' ? check(fmt, n + 2) : throw std::logic_error("too few arguments for provided format string"); } }; //////////////// // printing... void add(boost::format&) { } template<typename T, typename... Ts> void add(boost::format& f, const T& arg, const Ts&... ts) { f % arg; add(f, ts...); } //////////////// #define PP_PARENTHESISE_WITH_TOKEN(r, token, i, e) \ BOOST_PP_COMMA_IF(i) token(e) #define PP_CSV_SEQ_PARENTHESISE_WITH_TOKEN(...) \ BOOST_PP_SEQ_FOR_EACH_I(PP_PARENTHESISE_WITH_TOKEN, decltype, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) #define PP_PERFORM_LOG_FORMAT_CHECK(fmt, ...) \ Format<BOOST_PP_IF(BOOST_PP_EQUAL(BOOST_PP_TUPLE_SIZE((,##__VA_ARGS__)), 1), \ BOOST_PP_EXPAND, PP_CSV_SEQ_PARENTHESISE_WITH_TOKEN)(__VA_ARGS__)>::check(fmt, 0) #define LOG(fmt, ...) \ { \ static_assert(PP_PERFORM_LOG_FORMAT_CHECK(fmt, ##__VA_ARGS__), ""); \ boost::format f(fmt); \ add(f, ##__VA_ARGS__); \ std::cout << f.str() << std::endl; \ } int main() { // nothing LOG("hello world"); // char LOG("%c", 'x'); // integral LOG("%d", -123); LOG("%ld", -123); LOG("%u", 123u); LOG("%lu", 123u); // strings LOG("%s", "hello world"); LOG("%-s", "hello world"); LOG("%s", std::string("hello world")); { const char* s = "hello world"; LOG("%s", s); } { std::string s = "hello world"; LOG("%s", s); } { std::string s = "hello world"; boost::string_ref r(s); LOG("%s", r); } // floating point LOG("%f", 1.23); LOG("%f", 1.23f); // width & precision LOG("%02d", 1); LOG("% 3s", "hello"); LOG("% 3s", "yo"); LOG("%.2d", 123); LOG("%.2f", 1.2345); LOG("%2f", 1.23456789); LOG("%02f", 0.1); LOG("%02.2f", 0.1); // width & precision as arguments // not supported by boost::format // LOG("%*d", 3, 12); // LOG("%.*s", 3, "hello"); // LOG("%.*d", 3, 12345); // LOG("%*.*s", 3, 3, "hello"); // LOG("%*.*d", 3, 3, 12345); // mix of multiple different arguments LOG("%s", "hi"); LOG("%s %d", "hi", 1); LOG("%s %d %u %lf %f %c", "hi", -1, 12u, 1.23, 1.33, 'c'); // too few arguments // LOG("%s %d %u %lf %f %c", "hi", -1, 12u, 1.23, 1.33); // too many arguments // LOG("%s %d %u %lf %f %c", "hi", -1, 12u, 1.23, 1.33, 'c', 1); // incorrect argument for format string // LOG("%s %d %u %lf %f %c", "hi", -1, 12u, 1, 1.33, 'c'); } I can probably improve the `%ll` length specifier check as this is currently implemented as a backwards search into the format string for 64-bit integral types. I also haven't done checking for `%hh` length specifier. Any suggestions on this would be greatly appreciated. ##Error cases: too few arguments: main.cpp:285:84: error: expression ‘<throw-expression>’ is not a constant-expression : throw std::logic_error("too few arguments for provided format string"); too many arguments: main.cpp:257:87: error: expression ‘<throw-expression>’ is not a constant-expression throw std::logic_error("too many arguments for provided format string") mismatch between argument type and format: main.cpp:29:68: error: expression ‘<throw-expression>’ is not a constant-expression { return C != c ? throw std::logic_error("invalid fmt for type") : true; } \ ^ main.cpp:33:1: note: in expansion of macro ‘SUPPORTED_TYPE’ SUPPORTED_TYPE(int, 'd');