aerofly
aerofly

Reputation: 71

Basic compile time format string checking using constexpr

In our project we use a printf compatible function to add messages to an external log file. e.g. We can write

__LOG_INFO( "number of files = %d\n", number_of_files );
__LOG_INFO( "Just for information\n" );

The function declarations of __LOG_INFO look like this

template<int N>
inline void __LOG_INFO( const char (&fmt)[N] )
{
    call_printf( MODULE_NAME, fmt, debug_parameters() );
}

template<int N, typename T1>
static void __LOG_INFO( const char (&fmt)[N], const T1 &t1 )
{
    call_printf( MODULE_NAME, fmt, debug_parameters( t1 ) );
}

template<int N, typename T1, typename T2>
static void __LOG_INFO( const char (&fmt)[N], const T1 &t1, const T2 &t2 )
{
    call_printf( MODULE_NAME, fmt, debug_parameters( t1, t2 ) );
}

...

We now would like to add some simple compile time format string checking using the C++ 11 constexpr functionality, e.g. to do a very simple checking of the number of parameters in the format string we have this function

template<int N>
constexpr static int count_arguments( const char (&fmt)[N], int pos = 0, int num_arguments = 0 )
{
    return pos >= N-2 ? num_arguments :
                        fmt[pos] == '%' && fmt[pos+1] != '%' ? count_arguments( fmt, pos+1, num_arguments+1 ) :
                                                               count_arguments( fmt, pos+1, num_arguments );
}

The problem now is that we cannot add something like static_assert inside the __LOG_INFO functions themselves, since the compiler complains that fmt is not an integral constant. So right now we have this ugly macro solution:

#define COUNT_ARGS(...) COUNT_ARGS_(,##__VA_ARGS__,8,7,6,5,4,3,2,1,0)
#define COUNT_ARGS_(z,a,b,c,d,e,f,g,h,cnt,...) cnt

#define LOG_INFO(a, ...) \
  { \
      static_assert( count_arguments(a)==COUNT_ARGS(__VA_ARGS__), "wrong number of arguments in format string" ); \
      __LOG_INFO(a,##__VA_ARGS__); \
  }

So instead of calling __LOG_INFO, one has to call LOG_INFO.

Is there any better solution besides using those macros above?

Upvotes: 7

Views: 3046

Answers (1)

mpark
mpark

Reputation: 7904

I'm working on a compile-time format string library during which I ran into similar issues. So I'll share my findings here.

The main issue is that, constexpr functions are defined in C++ to be callable during compile-time as well as runtime. The following example is invalid, because F must be capable to being called from a runtime context.

/* Invalid constexpr function */
constexpr int F(int x) { static_assert(x == 42, ""); return x; }

/* Compile-time context */
static_assert(F(42) == 42, "");

/* Runtime context */
void G(int y) { F(y); }  // The way F is defined, this can't ever be valid.

If the type of the parameter is one that is allowed as the template paramter, but solution is simple, we just pass it through the template parameter. But if it's not, you can wrap theconstexpr-ness of the expression in a class in an arbitrary scope with a lambda.

constexpr parameters

/* Simply counts the number of xs, using C++14. */
static constexpr std::size_t count_xs(const char *fmt, std::size_t n) {
  std::size_t result = 0;
  for (std::size_t i = 0; i < n; ++i) {
    if (fmt[i] == 'x') {
      ++result;
    }  // if
  }  // for
  return result;
}

template <typename StrLiteral, typename... Args>
constexpr void F(StrLiteral fmt, Args &&... args) {
  static_assert(count_xs(fmt, fmt.size()) == sizeof...(Args), "");
}

int main() {
  F([]() {
      class StrLiteral {
        private:
        static constexpr decltype(auto) get() { return "x:x:x"; }
        public:
        static constexpr const char *data() { return get(); }
        static constexpr std::size_t size() { return sizeof(get()) - 1; }
        constexpr operator const char *() { return data(); }
      };
      return StrLiteral();
    }(), 0, 0, 0);
}

The call-site is ridiculous. As much as I hate macros, we can use it to clean up a little bit.

#define STR_LITERAL(str_literal) \
  []() { \
    class StrLiteral { \
      private: \
      static constexpr decltype(auto) get() { return str_literal; } \
      public: \
      static constexpr const char *data() { return get(); } \
      static constexpr std::size_t size() { return sizeof(get()) - 1; } \
      constexpr operator const char *() { return data(); } \
    }; \
    return StrLiteral(); \
  }()

int main() {
  F(STR_LITERAL("x:x:x"), 0, 0, 0);
}

In general, we can use this technique of wrapping a constexpr expression in a static constexpr function to preserve its constexpr-ness through the function parameter. But note that this may kill compile-time, since every call to F will cause a different template instantiation even if we call it twice with equivalent strings.

Slight Improvement

Rather than instantiating a different template for every call to F, we can make it so that for the same format strings it reuses the same instantiation.

template <char... Cs>
class Format {
  private:
  static constexpr const char data_[] = {Cs..., '\0'};
  public:
  static constexpr const char *data() { return data_; }
  static constexpr std::size_t size() { return sizeof(data_) - 1; }
  constexpr operator const char *() { return data(); }
};

template <char... Cs>
constexpr const char Format<Cs...>::data_[];

template <char... Cs, typename... Args>
constexpr void F(Format<Cs...> fmt, Args &&... args) {
  static_assert(count_xs(fmt, fmt.size()) == sizeof...(Args), "");
}

int main() {
  F(Format<'x', ':', 'x', ':', 'x'>(), 0, 0, 0);
}

Welp, let's use another macro to make Format construction "nicer".

template <typename StrLiteral, std::size_t... Is>
constexpr auto MakeFormat(StrLiteral str_literal,
                          std::index_sequence<Is...>) {
  return Format<str_literal[Is]...>();
}

#define FMT(fmt) \
   MakeFormat(STR_LITERAL(fmt), std::make_index_sequence<sizeof(fmt) - 1>())

int main() {
  F(FMT("x:x:x"), 0, 0, 0);
}

Hope this helps!

Upvotes: 8

Related Questions