背景

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

通过宏封装实现std::format编译期检查参数数量是否一致的更多相关文章

  1. static_assert enable_if 模板编译期检查

    conceptC++ http://www.generic-programming.org/faq/?category=conceptcxx Checking Concept Without Conc ...

  2. C++17尝鲜:编译期 if 语句

    Constexpr If(编译期 if 语句) 以 if constexpr 打头的 if 语句被称为 Constexpr If. Constexpr If 是C++17所引入的新的语法特性.它为C+ ...

  3. 数值类型中JDk的编译期检查和编译期优化

    byte b1 = 5;//编译期检查,判断是否在byte范围内 byte b2 = 5+4;//编译期优化,相当于b2=9 byte b3 = 127;//编译通过,在byte范围内 byte b4 ...

  4. C++ 编译期封装-Pimpl技术

    Pimpl技术——编译期封装 Pimpl 意思为“具体实现的指针”(Pointer to Implementation), 它通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏, 是隐藏实 ...

  5. c++ 编译期与运行期

    分享到 一键分享 QQ空间 新浪微博 百度云收藏 人人网 腾讯微博 百度相册 开心网 腾讯朋友 百度贴吧 豆瓣网 搜狐微博 百度新首页 QQ好友 和讯微博 更多... 百度分享 转自:http://h ...

  6. Java编译期注解处理器详细使用方法

    目录 Java编译期注解处理器 启用注解处理器 遍历语法树 语法树中的源节点 语法树节点的操作 给类增加注解 给类增加import语句 构建一个内部类 使用方法 chainDots方法 总结 Java ...

  7. c++ 编译期计算 (一)

    编译期就是编译器进行编译,产生.obj文件的所处的那一段时间(如果是广义的编译期,那么一般还包括了链接期,因为现在很多编译器都会自动调用链接器进行链接)执行期就是你执行某个已经链接好的程序的那段时间. ...

  8. 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态

    1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), class Widget { public: Widget(); virtual ~W ...

  9. Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)

    注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理.在编译期处理注解时,会处理到不再产生新的源文件为止,之后再对所有源文件进行编译. Java5中提供了apt工具来进行编译期的注解处 ...

随机推荐

  1. 菜鸟学git的基本命令及常见错误

    Git init //在当前项目工程下履行这个号令相当于把当前项目git化,变身!\ git config --global user.name "xxx" # 配置用户名 git ...

  2. cve_2019_0708_bluekeep漏洞

    一.环境说明 kali linux windows 7 sp1 二.cve_2019_0708_bluekeep漏洞利用 msf5 auxiliary(dos/windows/rdp/ms12_020 ...

  3. zabbix-5.0自动发现

    1. 安装zabbix5.0 rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-release-5.0-1.el7.no ...

  4. 搭建uipath

    我对windows也不太熟,也是第一次安装Uipath Orchestrator,希望有问题指出一起交流,可以留言,Uipath中文qq交流群:4656303241. 下载镜像 windows ser ...

  5. 面试突击62:group by 有哪些注意事项?

    注意:本文以下内容基于 MySQL 5.7 InnoDB 数据库引擎. 1.group by 后面不能加 where 在 MySQL 中,所有的 SQL 查询语法要遵循以下语法顺序: select f ...

  6. Visual Studio 生产环境配置方案:SlowCheetah

    原文 SlowCheetah 能满足你不同编译模式产生不同 app.config 配置的需求,已被微软纳入麾下,支持XML,JSON格式. 下面我们用XML的格式来试试如何使用SlowCheetah, ...

  7. ApiDay002_01 正则表达式

    正则表达式 用于检测.测试字符串规则的表达式. 经常用于检测字符串是否符合特定的规则,在网站上经常用于检测用户输入数据是否符合规范: 检测 用户名 是否为 8~10 数字 英文(大小写) 检测 电话号 ...

  8. day02-2

    JAVA入门 1.C&&C++ 1972年C诞生 贴近硬件,运行极快,效率极高 操作系统,编译器,数据库,网络系统等 指针和内存管理 1982年C++诞生 面向对象 兼容C 图形领域. ...

  9. NOI / 2.1基本算法之枚举 1749:数字方格

    描述: 如上图,有3个方格,每个方格里面都有一个整数a1,a2,a3.已知0 <= a1, a2, a3 <= n,而且a1 + a2是2的倍数,a2 + a3是3的倍数, a1 + a2 ...

  10. Centos7中配置NIS:用户账号管理

    NIS:网络信息服务 Linux系统中用户按地域分两类:本地用户,远程用户(NIS.LDAP.AD)模式:C/S模式 ypbind是定义NIS服务器的客户端进程,一旦确定了服务器位置,客户端绑定到了服 ...