通過宏封裝實現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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章