• 通过宏封装实现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 
    #include 
    #include 
    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_constantsize_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;
    }
    
  • 相关阅读:
    Linux下swap(交换分区)的增删改
    python coding with ChatGPT 打卡第21天| 二叉树:最近公共祖先
    (三)行为模式:8、状态模式(State Pattern)(C++示例)
    Unity Audio
    基于Matlab求解高教社杯全国大学生数学建模竞赛(CUMCM2012A题)-葡萄酒的评价(源码+数据)
    刷题记录:牛客NC17193简单瞎搞题
    【概率论教程01】对贝叶斯定理的追忆
    请问swat输出的是有机氮、 有机磷、 硝态氮等,但目前只有总磷、总氮和氨氮的数据,该怎么率定呢?
    数据结构--顺序表
    C语言内存分区
  • 原文地址:https://www.cnblogs.com/hchlqlz-oj-mrj/p/16636252.html