背景

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