背景
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
。
所用技术
-
静态断言:
static_assert
-
格式串参数数量获取:
GetFormatStringArgsNum
,该接口声明为constexpr
,从而获得编译期执行的能力。其实现大致为遍历字符串,检查其中{}
的数量。 -
传参数量的获取: 由于使用宏进行封装,最后其实就是需要获得
__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; }