13
\$\begingroup\$

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?

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.

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');
\$\endgroup\$
  • 5
    \$\begingroup\$ Before any votes to close. The error messages are the expected output given the input. We are looking to generate compile time errors deliberately. \$\endgroup\$ – Martin York Mar 26 '15 at 5:10
5
\$\begingroup\$

Unfortunately I don't have time to do a full review, I can only offer some nitpicks that are a bit too big to fit in a comment...

Inconsistent use of size_t and std::size_t

For example here:

template<size_t N>
constexpr bool isLiteralPercent(const char (&fmt)[N], std::size_t n)

There are other places too.

Prefer the _t helpers for type_traits type templates in stl.

In struct Argument<T, Ts...>, this:

return Type< typename std::decay<T>::type>::check(fmt, n) &&
            Format<Ts...>::check(fmt, n + 1);

could be:

return Type<std::decay_t<T>>::check(fmt, n) &&
            Format<Ts...>::check(fmt, n + 1);

to get rid of the typename nonsense. :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.