通过宏封装实现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。
所用技术
静态断言:
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;
}
通过宏封装实现std::format编译期检查参数数量是否一致的更多相关文章
- static_assert enable_if 模板编译期检查
conceptC++ http://www.generic-programming.org/faq/?category=conceptcxx Checking Concept Without Conc ...
- C++17尝鲜:编译期 if 语句
Constexpr If(编译期 if 语句) 以 if constexpr 打头的 if 语句被称为 Constexpr If. Constexpr If 是C++17所引入的新的语法特性.它为C+ ...
- 数值类型中JDk的编译期检查和编译期优化
byte b1 = 5;//编译期检查,判断是否在byte范围内 byte b2 = 5+4;//编译期优化,相当于b2=9 byte b3 = 127;//编译通过,在byte范围内 byte b4 ...
- C++ 编译期封装-Pimpl技术
Pimpl技术——编译期封装 Pimpl 意思为“具体实现的指针”(Pointer to Implementation), 它通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏, 是隐藏实 ...
- c++ 编译期与运行期
分享到 一键分享 QQ空间 新浪微博 百度云收藏 人人网 腾讯微博 百度相册 开心网 腾讯朋友 百度贴吧 豆瓣网 搜狐微博 百度新首页 QQ好友 和讯微博 更多... 百度分享 转自:http://h ...
- Java编译期注解处理器详细使用方法
目录 Java编译期注解处理器 启用注解处理器 遍历语法树 语法树中的源节点 语法树节点的操作 给类增加注解 给类增加import语句 构建一个内部类 使用方法 chainDots方法 总结 Java ...
- c++ 编译期计算 (一)
编译期就是编译器进行编译,产生.obj文件的所处的那一段时间(如果是广义的编译期,那么一般还包括了链接期,因为现在很多编译器都会自动调用链接器进行链接)执行期就是你执行某个已经链接好的程序的那段时间. ...
- 读书笔记 effective c++ Item 41 理解隐式接口和编译期多态
1. 显示接口和运行时多态 面向对象编程的世界围绕着显式接口和运行时多态.举个例子,考虑下面的类(无意义的类), class Widget { public: Widget(); virtual ~W ...
- Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)
注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理.在编译期处理注解时,会处理到不再产生新的源文件为止,之后再对所有源文件进行编译. Java5中提供了apt工具来进行编译期的注解处 ...
随机推荐
- ExtJS 布局-Auto布局(Auto Layout)
更新记录 2022年5月30日 开启本篇 1.说明 auto布局是大部分容器默认的布局类型. auto布局通常是从上到下进行堆叠,auto布局不会设置子组件的宽度,默认与容器一样的宽度. 类似于HTM ...
- 使用PowerShell校验文件MD5
更新记录 2022年4月16日:本文迁移自Panda666原博客,原发布时间:2021年7月14日. 方法1:使用Get-FileHash命令 (Get-FileHash ".\SQLSer ...
- JavaScript有哪些数据类型,它们的区别?
基本数据类型:number.string.boolean.Undefined.NaN(特殊值).BigInt.Symbol 引入数据类型:Object NaN是JS中的特殊值,表示非数字,NaN不是数 ...
- 华为AppLinking中统一链接的创建和使用
运营的同学近期在准备海外做一波线下投放,涉及到海外的Google Play,iOS设备的App Store,以及华为渠道的AppGallery. 其中运营希望我们能够将三个平台的下载整合到一个链接 ...
- SAP JSON 格式化及解析。
一.首选:/ui2/cl_json {'key':'value'} /ui2/cl_json=>deserialize( EXPORTING json = json CHANGING d ...
- Java Web servlet 详解
执行原理 当服务器接收到客户端浏览器的访问时,会解析请求的URL路径,获取访问的Servlet的资源路径 查找web.xml文件,看是否有对应的<url-pattern>标签体内容 如果有 ...
- NC200190 矩阵消除游戏
NC200190 矩阵消除游戏 题目 题目描述 牛妹在玩一个名为矩阵消除的游戏,矩阵的大小是 \({n}\) 行 \({m}\) 列,第 \({i}\) 行第 \({j}\) 列的单元格的权值为 \( ...
- mybatis查询的三种方式
查询最需要关注的问题:①resultType自动映射,②方法返回值: interface EmpSelectMapper: package com.atguigu.mapper; import ja ...
- Java开发学习(十)----基于注解开发定义bean 已完成
一.环境准备 先来准备下环境: 创建一个Maven项目 pom.xml添加Spring的依赖 <dependencies> <dependency> < ...
- 千万小心,99%的Java程序员会踩这些坑
前言 作为Java程序员的你,不知道有没有踩过一些基础知识的坑. 有时候,某个bug查了半天,最后发现竟然是一个低级错误. 有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了. 有时候, ...