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');