Deepseek实现了本方法代码,作者只是自底向上一步步解释实现方式。若您只想获取本方法,而不想了解具体实现,也可阅读本文前面三节。
潜在问题
- 本方法中使用了名称
print
,可能会与C++ 23
标准中的std::print
名称冲突。 - println中最终的换行用的是
'\n'
而不是endl
,若不用endl
或其他方式刷新缓冲区,可能无法看到输出结果。可以把'\n'
改成std::endl
解决这一问题。
概览
方法实现具体代码在本节文末,本节简要介绍更简便的输入输出的具体表现。
input()
input(val1, val2, val3)
会展开为 std::cin >> val1 >> val2 >> val3
,最多支持 20 个参数。
print()
print(val1, val2, val3)
会展开为 std::cout << val1 << val2 << val3
,最多支持 20 个参数。
println()
println(val1, val2, val3)
会展开为 std::cout << val1 << ' ' << val2 << ' ' << val3 << ' ' << '\n'
,最多支持 20 个参数。
修改参数数量方法
若您想支持更多参数,请同时修改第四行与第三行两行有关 NUM
的代码,并增加 _FOREACH_
的数量。下面假设我要修改为 25。
_NUM
增加数字到 24。注意是 24,而不是25。如下
#define _NUM(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, N, ...) N
NUM
增另数字到25。注意是从左往右递减,如下
#define NUM(...) EVAL(_NUM(__VA_ARGS__, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))
_FOREACH_
增加更多数量,直到25,每一个 _FOREACH_
都调用前一个,如下
#define _FOREACH_21(m, x, ...) m(x) EVAL(_FOREACH_20(m, __VA_ARGS__))
#define _FOREACH_22(m, x, ...) m(x) EVAL(_FOREACH_21(m, __VA_ARGS__))
#define _FOREACH_23(m, x, ...) m(x) EVAL(_FOREACH_22(m, __VA_ARGS__))
#define _FOREACH_24(m, x, ...) m(x) EVAL(_FOREACH_23(m, __VA_ARGS__))
#define _FOREACH_25(m, x, ...) m(x) EVAL(_FOREACH_24(m, __VA_ARGS__))
具体代码
#define CONCAT(a, b) a##b
#define EVAL(...) __VA_ARGS__
#define _NUM(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, N, ...) N
#define NUM(...) EVAL(_NUM(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))#define _FOREACH_1(m, x) m(x)
#define _FOREACH_2(m, x, ...) m(x) EVAL(_FOREACH_1(m, __VA_ARGS__))
#define _FOREACH_3(m, x, ...) m(x) EVAL(_FOREACH_2(m, __VA_ARGS__))
#define _FOREACH_4(m, x, ...) m(x) EVAL(_FOREACH_3(m, __VA_ARGS__))
#define _FOREACH_5(m, x, ...) m(x) EVAL(_FOREACH_4(m, __VA_ARGS__))
#define _FOREACH_6(m, x, ...) m(x) EVAL(_FOREACH_5(m, __VA_ARGS__))
#define _FOREACH_7(m, x, ...) m(x) EVAL(_FOREACH_6(m, __VA_ARGS__))
#define _FOREACH_8(m, x, ...) m(x) EVAL(_FOREACH_7(m, __VA_ARGS__))
#define _FOREACH_9(m, x, ...) m(x) EVAL(_FOREACH_8(m, __VA_ARGS__))
#define _FOREACH_10(m, x, ...) m(x) EVAL(_FOREACH_9(m, __VA_ARGS__))
#define _FOREACH_11(m, x, ...) m(x) EVAL(_FOREACH_10(m, __VA_ARGS__))
#define _FOREACH_12(m, x, ...) m(x) EVAL(_FOREACH_11(m, __VA_ARGS__))
#define _FOREACH_13(m, x, ...) m(x) EVAL(_FOREACH_12(m, __VA_ARGS__))
#define _FOREACH_14(m, x, ...) m(x) EVAL(_FOREACH_13(m, __VA_ARGS__))
#define _FOREACH_15(m, x, ...) m(x) EVAL(_FOREACH_14(m, __VA_ARGS__))
#define _FOREACH_16(m, x, ...) m(x) EVAL(_FOREACH_15(m, __VA_ARGS__))
#define _FOREACH_17(m, x, ...) m(x) EVAL(_FOREACH_16(m, __VA_ARGS__))
#define _FOREACH_18(m, x, ...) m(x) EVAL(_FOREACH_17(m, __VA_ARGS__))
#define _FOREACH_19(m, x, ...) m(x) EVAL(_FOREACH_18(m, __VA_ARGS__))
#define _FOREACH_20(m, x, ...) m(x) EVAL(_FOREACH_19(m, __VA_ARGS__))#define _FOREACH_N(N, m, ...) CONCAT(_FOREACH_, N)(m, __VA_ARGS__)
#define FOR_EACH(m, ...) EVAL(_FOREACH_N(NUM(__VA_ARGS__), m, __VA_ARGS__))#define COUT_STREAM(x) << x
#define print(...) std::cout FOR_EACH(COUT_STREAM, __VA_ARGS__)
#define CIN_STREAM(x) >> x
#define input(...) std::cin FOR_EACH(CIN_STREAM, __VA_ARGS__)
#define COUT_STREAM_2(x) << x << ' '
#define println(...) std::cout FOR_EACH(COUT_STREAM_2, __VA_ARGS__) << '\n'
接下来开始自底向上,逐步分析整个宏的实现。
CONCAT
简单地连接两个元素
EVAL
EVAL(...)
的直接作用在于把传入的内容展开,就是怎么拿来的,怎么摆出来。在一般情况下,用 EVAL
和不用 EVAL
似乎没有区别,但用 EVAL
能保证你的内容中有宏时,会先展开内部的宏,而不是错误地不展开。下面给个例子
#define A(x) a##x
#define B(x) A(x)
#define EVAL(...) __VA_ARGS__B(x) // 有可能展成A(x)而不是进一步展开成ax,为什么是“有可能”请见后文
EVAL(x) // 先展开内部的B变成EVAL(A(x)),由于有EVAL的存在,会继续展开内部的A变成EVAL(ax),最后变为ax
虽然理论上如此,但本人知识有限,没有测出过错误展开的情况。可能在更低版本的编译器会有错误的情况。
NUM与_NUM
通过NUM可以直接测出传给宏的参数个数,_NUM是NUM的辅助,但这个测试有数量上限,NUM中最大的数字,此处即20,就是这个上限。但可以人为增加它的上限。
先看NUM,当参传入NUM时,会把参数和一群数字进一步传给内置NUM,注意到此处数字是倒着递减到0。
我们再观察_NUM的传入参数形式,先给了比最大数少1的参数位置,再是个N,再是个变长参数。而后面展开的内容只需要N,说明前面那些数字参数,和变长参数,都是辅助作用。
这里的原理有点像游标卡尺,传给NUM有多少个参数,NUM会贴一个「标尺」,或者说「尺子」,在那具体参数的末尾,这时,传入_NUM的整体,就由「原具体参数+标尺」构成。然后N会卡住贴上的标尺中的某个数,这个数就是原具体参数的数量。原具体参数越多,标尺被卡住的位置就越靠前,所以标尺前面的数理应越大。
这里要注意,不要忘了会有0个具体参数的情况。
FOREACH
_FOREACH_[NUMBER]
先来看带数字的_FOREACH_[NUMBER]。
先看_FOREACH_1,它需要两个参数m和x,m是某个函数,x是要给m传入的参数,所以它的意思就是,给一个函数、它要的参数(只要一个参数),然后把函数表达式展出,或者说把它变成能用的函数调用写法,把函数用出来。
再看其他的,对于更高阶的,观察获取参数的列表,首先会截取一个目标函数m,再截取一个参数x,后面的是更低阶_FOREACH要用的参数。它先展开 m(x)
,然后空一格,再把后面那些参数,以及函数m,交给后面更低阶的展开。
这样,就把_FOREACH_[NUMBER](m, x1, x2, x3, ..., x_number)
展开成了 m(x1) m(x2) m(x3) ... m(x_number)
,注意中间有空格分隔。
_FOREACH_N
接收的参数,相比_FOREACH_[NUMBER],在最前面多出了个N。后面使用拼接。拼接了两个符号:_FOREACH_ 和N。容易看出,这个N,就指明了我们应该调用的第一个_FOREACH_[NUMBER],其实也是代表了参数m后面,要被应用到m中的参数有几个。
FOR_EACH
它只接收m,和需要被应用到m的参数,而不接收数量。所以在后面,它调用了NUM测量了参数的个数,把它作为_FOREACH_N中的N,再接上m和参数,传给_FOREACH_N。
总结
把一个函数,和若干个参数给FOR_EACH,然后把这若干个参数,放进m,并展开,在空间加入了空格。
COUT_STREAM、CIN_STREAM、COUT_STREAM_2
这几个效果都差不多,这里以较复杂的COUT_STREAM_2为例。
若一个参数x传入了该函数,则它会被展开成 << x << ' '
。
print、input、println
这里我们还是以最为复杂的println为例。
前面说过,COUNT_STREAM_2只接收一个参数,但我们之前拥有了能展开多个参数到同一个函数中的工具FOR_EACH,所以我们能想到这样的应用:FOR_EACH(COUT_STREAM_2, x1, x2, x3, ..., xn)
展开成 COUT_STREAM_2(x1) COUT_STREAM_2(x2) COUT_STREAM_2(x3) ... COUT_STREAM_2(xn)
进而展开成 << x1 << ' ' << x2 << ' ' << x3 << ' ' ... << xn << ' '
。在此基础上,在头加上 std::cout
,在尾加上 << '\n'
,就能完成完整的cout输出表达了。