Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 588
aerofly Avatar asked Aug 09 '14 11:08

aerofly


People also ask

Does constexpr compile time?

constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time.

Is constexpr always evaluated at compile time?

A constexpr function that is eligible to be evaluated at compile-time will only be evaluated at compile-time if the return value is used where a constant expression is required. Otherwise, compile-time evaluation is not guaranteed.

Can String_view be constexpr?

string_view has a constexpr constructor and a trivial destructor; keep this in mind when using in static and global variables (see the style guide) or constants (see Tip #140).

What is constexpr static?

static defines the object's lifetime during execution; constexpr specifies that the object should be available during compilation. Compilation and execution are disjoint and discontiguous, both in time and space. So once the program is compiled, constexpr is no longer relevant.


1 Answers

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!

like image 132
mpark Avatar answered Sep 23 '22 11:09

mpark