在写一个项目的时候,遇到了这么一个场景:需要定义若干个字符串,想要给他们不一样的名字,但又想通过数组顺序处理。因此得出如下结构的代码:
string inputFileName, outputFileName, QueryName, KeywordsPath, keywordDir, statisticsName, statisticsBinName;
string* FilepathArr[] = { &inputFileName, &outputFileName, &QueryName, &statisticsName , &KeywordsPath, &statisticsBinName };
但是上述代码有几个小问题:
1.每当需要新增一个字符串时,就要修改两个地方;
2.插入新字符串名的时候假如不注意,就会令数组中字符串的顺序和预期的不一致。
为了解决这个问题,考虑使用宏定义来减少代码的复杂度。
在最后成功写出来之前查了很多资料也调试了很多次,总结起来有几个点:
1. 宏分为对象类宏和函数类宏两种,区分这两种宏在于宏后面是否紧跟一对括号
2. 宏展开可以迭代有限次,但不是无限次。因此可以通过展开足够的次数来满足使用要求;
3. 每次宏展开只会处理某个特定的宏,然后一遍一遍扫描直到没有宏能被处理。但宏不支持自递归或间接递归,强行递归会在一两次后终止递归,展开出来的代码中将包含未展开的宏本身,如:
#define F(x, b) x, F(x, b)
展开后会变成:x, F(x, b),因此不能使用这种方法;
4. 函数类宏有可变参数的形式,如下
#define FUN(x, ...)
其中...指代的是一系列的参数列表,类似printf或者scanf的函数定义。该类宏有两个相对应的宏:
__VA_OPT__(abc)的作用是当...对应的参数列表不为空时,括号内的符号串abc才会出现;
__VA_ARGS__ 的作用是指代... 中的参数。
利用__VA_ARGS__层层展开可以达到解包的效果,也就是利用类似
#define N_DEF(a,...) a __VA_OPT__(, N_2_DEF(__VA_ARGS__)) #define N_2_DEF(a,...) a __VA_OPT__(, N_2_DEF(__VA_ARGS__))//注意这里无法递归,只是作为解包的例子
的方法,每次只处理首个参数,从而最终令参数全部被处理。而当参数只有一个的时候,__VA_OPT__不起效,因此可以顺利结束。但要注意的是,上述例子是不对的。
5. VS2019中要使用上述宏,需要先在特定项目属性中设置两个地方:
常规属性- C++最新语言特性-预览 - 最新 C++ 工作草案中的功能 (/std:c++latest)
C/C++-命令行-其他选项,添加:/Zc:preprocessor
6. 调试宏方法:C/C++-预处理器-预处理到文件-是,一旦调试正确后需要改回否,否则无法生成exe文件。
7. 调试小技巧:直接预处理后的文件非常大,因此不容易定位宏展开后的位置。因此可使用一些不常见字符串如“asdfsdfge”来作为宏参数,然后用文本编辑器的查找功能直接定位。
写的过程中重新看了下,还是参考文献1的这一段描述宏展开的原理写得最好,原文转载如下:
To prevent recursion, cpp associates a bit with every macro that has been defined. The bit reflects whether the macro is currently being replaced with its substitution list, so let’s call it the replacing bit. Cpp furthermore associates a bit with each token in the input stream, signifying that the token can never be macro-expanded. Let’s call the latter bit the unavailable bit. Initially, the replacing and unavailable bits are all clear.
As cpp processes each input token
T
, it setsT
’s unavailable bit and decides whether or not to macro-expandT
as follows:
If
T
is the name of a macro for which the replacing bit is true, cpp sets the unavailable bit on tokenT
. Note that even ifT
is not in a context where it could be macro-expanded—because it’s a function-like macro not followed by “(
”—cpp still sets the unavailable bit. Moreover, once the unavailable has been set on an input token, it is never be cleared.If
T
is the name of an object-like macro andT
’s unavailable bit is clear, thenT
is expanded.If
T
is the name of a function-like macro,T
’s unavailable bit is clear, andT
is followed by(
, thenT
is expanded. Note, however, that ifT
is called with an invalid number of arguments, then the program is ill-formed.If cpp decides not to macro-expand
T
, it simply addsT
to the current output token list. Otherwise, it expandsT
in two phases.
When
T
is a function-like macro, cpp scans all of the arguments supplied toT
and performs macro expansion on them. It scans arguments the same as normal token processing, but instead of placing output tokens in the main preprocessor output, it builds a replacement token list for each ofT
’s arguments. It also remembers the original, non-macro-expanded arguments for use with#
and##
.Cpp takes
T
’s substitution list and, ifT
had arguments, replaces any occurrences of parameter names with the corresponding argument token lists computed in step 1. It also performs stringification and pasting as indicated by#
and##
in the substitution list. It then logically prepends the resulting tokens to the input list. Finally, cpp sets the replacing bit to true on the macro namedT
.With the replacing bit true, cpp continues processing input as usual from the tokens it just added to the input list. This may result in more macro expansions, so is sometimes called the rescan phase. Once cpp has consumed all tokens generated from the substitution list, it clears the replacing bit on the macro named
T
.
综上,递归宏的思路为,避免宏对应的不可用位被设置为真,因此要在自引用的宏定义基础上,通过引入一个括号和一个副宏,来推迟当前替换中的宏的展开中的自展开。
最终版的宏定义如下:
1 #define EXPAND(...) EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__)))) 2 #define EXPAND4(...) EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__)))) 3 #define EXPAND3(...) EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__)))) 4 #define EXPAND2(...) EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__)))) 5 #define EXPAND1(...) __VA_ARGS__ 6 7 #define PARENS () 8 9 #define N_DEF(a,...) \ 10 wstring a __VA_OPT__(, EXPAND(N_1_W_DEF(__VA_ARGS__))); \ 11 wstring* FilepathArr[] = {&a __VA_OPT__(, EXPAND(N_1_WP_DEF(__VA_ARGS__)))}; 12 13 #define N_1_W_DEF(a,...) a __VA_OPT__(, N_2_W_DEF PARENS (__VA_ARGS__)) 14 #define N_2_W_DEF() N_1_W_DEF 15 16 #define N_1_WP_DEF(a,...) &a __VA_OPT__(, N_2_WP_DEF PARENS (__VA_ARGS__)) 17 #define N_2_WP_DEF() N_1_WP_DEF 18 19 N_DEF(inputFileName, outputFileName, QueryName, KeywordsPath, keywordDir, statisticsName, statisticsBinName )
代码原理:
1. 1-5行用来确保后续展开的宏能被多次检查,最多检查4^4 = 256次;
2. 7行是递归的关键,留意PARENS和()之间有空格存在,因此该宏是个对象类宏,后面用来把15 18行中的PARENS替换成括号。以15行为例,其中的N_2_WFILEPATH_DEF由于没有紧跟括号,无法直接替换;随后PARENS被替换成括号;然后在再次扫描的时候N_2_WFILEPATH_DEF ()被替换成N_1_WPFILEPATH_DEF,进而跟后面的(__VA_ARGS__)组装到一起。如果把上述过程称为一个周期的话,那么最后根据多次重复的EXPAND可反复进行上述过程,因此最多可生成256个变量。
3. 假如15行写成
#define N_1_WFILEPATH_DEF(a,...) a __VA_OPT__(, N_1_WFILEPATH_DEF (__VA_ARGS__))
就会导致自展开时被不可用位阻止,展开失败。
4. 9行后面的\用于连接后续行;
参考资料:
1. Mazières, D. (2021). Recursive macros with C++20 __VA_OPT__. Retrieved 6 August 2021, from https://www.scs.stanford.edu/~dm/blog/va-opt.html
2. MSVC new preprocessor overview. (2020). Retrieved 6 August 2021, from https://docs.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=msvc-160
3. Colin Robertson, /Zc:preprocessor (Enable preprocessor conformance mode). (2020). Retrieved 6 August 2021, from https://docs.microsoft.com/en-us/cpp/build/reference/zc-preprocessor?view=msvc-160
4. Kevin Lynx, Automatic code generation-macro recursion(Others-Community). (2021). Retrieved 6 August 2021, from https://titanwolf.org/Network/Articles/Article?AID=78b63f9d-e76f-405a-9d78-48c576331d98#gsc.tab=0 (翻译:kanbang, 代码自动生成-宏递归思想_kanbang的专栏-CSDN博客. (2021). Retrieved 6 August 2021, from https://blog.csdn.net/kanbang/article/details/50957152)
(吐槽:最后这个我一度认为是银弹,结果看到最后发现貌似最多只能扩展两次,感觉参考价值不大)