通过宏封装实现std::format编译期检查参数数量是否一致

背景

std::format在传参数量少于格式串所需参数数量时,会抛出异常。而在大部分的应用场景下,参数数量不一致提供编译报错更加合适,可以促进我们更早发现问题并进行改正。

最终效果

// 测试输出接口。 template <typename... T> void Print(const std::string& _Fmt, const T&... _Args) {     cout << std::vformat(_Fmt, std::make_format_args(_Args...)) << endl; }  // 封装宏,实现参数数量一致的检查 #define PRINT(fmt, ...) /     do { static_assert(GetFormatStringArgsNum(fmt) == decltype(VariableArgsNumHelper(__VA_ARGS__))::value, "Invalid format string or mismatched number of arguments"); Print(fmt, __VA_ARGS__); } while(0)  int main() {     PRINT("{}", "hello");     PRINT("{} {}", "hello");      return 0; } 

上例代码中,使用PRINT宏封装了Print函数,后续使用PRINT进行控制台输出,如果出现参数数量不一致,将产生编译报错:Invalid format string or mismatched number of arguments

所用技术

  1. 静态断言: static_assert

  2. 格式串参数数量获取: GetFormatStringArgsNum,该接口声明为constexpr,从而获得编译期执行的能力。其实现大致为遍历字符串,检查其中{}的数量。

  3. 传参数量的获取: 由于使用宏进行封装,最后其实就是需要获得__VA_ARGS__中附带了几个参数,网上可以搜到各种解决方案,这里采用的是声明一个模板函数,模板函数返回integral_constant结构体,其对不同的参数数量,自动生成不同的结构体类型,之后使用decltype(VariableArgsNumHelper(__VA_ARGS__))获得返回值类型,并从返回值类型中获得代表参数数量的常量值,由于运行期用不到该函数,因此只提供声明,不提供实现。

整体代码

#include <iostream> #include <string> #include <format> using namespace std;  constexpr int GetFormatStringArgsNum(const std::string& fmt) {  enum STATE  {   NORMAL,   // 正在解析普通串   REPLACEMENT, // 正在解析大括号中的内容  };   // 按标准规定,格式串中要么都指定参数编号,要么都不指定  // 原文:  // The arg-ids in a format string must all be present or all be omitted.   // Mixing manual and automatic indexing is an error.  enum RULE  {   UNKNOWN,  // 格式串规则   SPECIFIEDID, // 指定编号,如{0}   UNSPECIFIEDID, // 不指定编号,如{}  };   // 指定参数编号的最大值  const int MAX_ARGS_NUM = 10000;  // 初始状态  STATE state = NORMAL;  // 初始规则  RULE rule = UNKNOWN;  // 当前参数编号  int nIndex = -1;  // 参数数量  int nArgsNum = 0;  for (int i = 0; i < fmt.size(); ++i)  {   switch (state)   {   case NORMAL:   {    // 普通串解析时,遇到左大括号或右大括号,才有可能改变状态    if (fmt[i] == '{')    {     if (i + 1 < fmt.size() && fmt[i + 1] == '{')     {      // 遇到 {{,则将他们视为普通字符      ++i;     }     else     {      // 进入替换串状态      state = REPLACEMENT;     }    }    else if (fmt[i] == '}')    {     ++i;     if (i >= fmt.size() || fmt[i] != '}')     {      // 普通串解析状态,遇上右大括号时,只有当接下来也是右大括号时,才属于合法串      return -1;     }    }   }   break;   case REPLACEMENT:   {    // 替换串状态下,正常只会遇到右大括号、数字、冒号,其他符号均为错误    if (fmt[i] == '}')    {     // 遇到右大括号,则进入普通串解析状态,这里不考虑}},正常{} 中间不应该出现}     state = NORMAL;      // 如果之前某个{} 已经指定参数编号,则所有参数都应该指定编号     if (rule == SPECIFIEDID)     {      // 如果这个{} 不指定编号,则视为非法格式串      if (nIndex == -1)      {       return -1;      }      // 在指定编号的情况下,可变参数的数量至少要比编号大1      nArgsNum = std::max(nArgsNum, nIndex + 1);      // 重置当前编号      nIndex = -1;     }     else     {      // 如果当前规则未明或者当前规则为不指定编号,则参数数量进行自增。      state = NORMAL;      rule = UNSPECIFIEDID;      ++nArgsNum;     }    }    else if (fmt[i] >= '0' && fmt[i] <= '9')    {     // 遇到数字,说明指定了参数编号     if (rule == UNSPECIFIEDID)     {      // 如果当前规则已明确为不指定编号,则视为非法格式串      return -1;     }     else     {      // 否则,将当前规则改为指定编号,并维护当前编号      rule = SPECIFIEDID;      if (nIndex == -1)      {       nIndex = 0;      }       nIndex = nIndex * 10 + (fmt[i] - '0');      if (nIndex >= MAX_ARGS_NUM)      {       // 当前编号大于最大上限,则直接视为非法格式串       return -1;      }     }    }    else if (fmt[i] == ':')    {     // 遇到冒号,说明接下来是格式串规则,直接跳过     for (; i + 1 < fmt.size() && fmt[i + 1] != '}'; ++i)     {      ;     }    }    else    {     // 解析替换串时,遇上其他字符,均将格式串视为非法。     return -1;    }   }   break;   }  }   // 最终状态必须为普通串解析状态。  return state == NORMAL ? nArgsNum : -1; }  // 可变参数数量辅助器 template <typename ... Args> std::integral_constant<std::size_t, sizeof...(Args)> VariableArgsNumHelper(const Args  & ...);  // 测试输出接口。 template <typename... T> void Print(const std::string& _Fmt, const T&... _Args) {  cout << std::vformat(_Fmt, std::make_format_args(_Args...)) << endl; }  // 封装宏,实现参数数量一致的检查 #define PRINT(fmt, ...) /     do { static_assert(GetFormatStringArgsNum(fmt) == decltype(VariableArgsNumHelper(__VA_ARGS__))::value, "Invalid format string or mismatched number of arguments"); Print(fmt, __VA_ARGS__); } while(0)   int main() {  PRINT("{} {}", "hello");   return 0; } 

商匡云商
Logo
注册新帐户
对比商品
  • 合计 (0)
对比
0
购物车