使用constexpr进行基本编译时格式字符串检查



在我们的项目中,我们使用printf兼容函数将消息添加到外部日志文件中。例:我们可以写

__LOG_INFO( "number of files = %dn", number_of_files );
__LOG_INFO( "Just for informationn" );

__LOG_INFO的函数声明如下

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 ) );
}
...

我们现在想添加一些简单的编译时格式字符串检查使用c++ 11 constexpr功能,例如做一个非常简单的检查在格式字符串参数的数量,我们有这个函数

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

现在的问题是我们不能在__LOG_INFO函数本身中添加static_assert之类的东西,因为编译器抱怨fmt不是一个整型常数。现在我们有了这个丑陋的宏解决方案:

#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__); 
  }

所以不调用__LOG_INFO,必须调用LOG_INFO

除了使用上面的宏,还有更好的解决方案吗?

我正在编写一个编译时格式字符串库,在此期间我遇到了类似的问题。所以我将在这里分享我的发现。

主要问题是,constexpr函数在c++中被定义为可在编译时和运行时调用。下面的示例无效,因为F必须能够从运行时上下文中调用。

/* 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.

如果形参的类型是允许作为模板形参的类型,但解决方法很简单,我们只需通过模板形参传递它。但如果不是,您可以使用lambda将表达式的constexpr性包装在任意作用域的类中。

constexpr参数

/* 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);
}

呼叫站点是荒谬的。虽然我很讨厌宏,但我们可以用它来清理一下。

#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);
}

一般来说,我们可以使用这种技术,将constexpr表达式包装在static constexpr函数中,通过函数参数保持其constexpr性。但请注意,这可能会破坏编译时间,因为每次调用F都会导致不同的模板实例化,即使我们使用相同的字符串调用它两次。

些微好转

与其每次调用F都实例化一个不同的模板,不如对相同的格式字符串重用相同的实例化。

template <char... Cs>
class Format {
  private:
  static constexpr const char data_[] = {Cs..., ''};
  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);
}

好吧,让我们使用另一个宏使Format构造"更好"。

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

希望这对你有帮助!

最新更新