背景 std::format在傳參數量少於格式串所需參數數量時,會拋出異常。而在大部分的應用場景下,參數數量不一致提供編譯報錯更加合適,可以促進我們更早發現問題併進行改正。 最終效果 // 測試輸出介面。 template <typename... T> void Print(const std:: ...
背景
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;
}