Skip to main content
2 of 5
add %ll length specifier
Steve Lorimer
  • 337
  • 1
  • 4
  • 10

Compile-time printf-style format checking

Compile time checking of printf-like format strings

Inspired by this open ticket 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 on the previous iteration of this code here

Here is the below code running on ideone

#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');
Steve Lorimer
  • 337
  • 1
  • 4
  • 10