基于宏,实现C/C++更高效的输入input和更高效的输出print、println

Deepseek实现了本方法代码,作者只是自底向上一步步解释实现方式。若您只想获取本方法,而不想了解具体实现,也可阅读本文前面三节。

潜在问题

  1. 本方法中使用了名称 print,可能会与 C++ 23 标准中的 std::print 名称冲突。
  2. 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输出表达了。

posted @ 2025-03-28 19:02  tsunchi-wong  阅读(75)  评论(0)    收藏  举报