草庐IT

通过宏封装实现std::format编译期检查参数数量是否一致

hchlqlz 2023-04-15 原文

背景

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. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  2. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  3. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  4. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  5. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  6. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  7. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  8. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  9. ruby - 检查方法参数的类型 - 2

    我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)

  10. ruby-on-rails - Enumerator.new 如何处理已通过的 block ? - 2

    我在理解Enumerator.new方法的工作原理时遇到了一些困难。假设文档中的示例:fib=Enumerator.newdo|y|a=b=1loopdoy[1,1,2,3,5,8,13,21,34,55]循环中断条件在哪里,它如何知道循环应该迭代多少次(因为它没有任何明确的中断条件并且看起来像无限循环)? 最佳答案 Enumerator使用Fibers在内部。您的示例等效于:require'fiber'fiber=Fiber.newdoa=b=1loopdoFiber.yieldaa,b=b,a+bendend10.times.m

随机推荐