通过宏封装实现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; } 

发表评论

评论已关闭。

相关文章