引言
自 C 以来,宏为代码生成工具。至 C++98 则有模板,泛型编程日益盛行。迄于 C++20,引编译期表达式,添 Concepts,元编程基础支持始渐完善。由此元编程之技稍简。而静态反射乃元编程系统之核心,却迟久未至,产生式元编程遂仍繁复。
所述元编程之书文,指不胜屈,其间也以编译期计算为主,奇技淫巧,小大靡遗。而于产生式元编程,言者寥寥,常见于库中直用。于是有此系列,略述浅见,供同道者读阅之。
产生式元编程,即为编译期代码生成的技术,各类系统,特性不侔,用法与能力亦有所殊。是以本系列跨度稍大,然只涉标准,不论自成之法,预计十章左右,从旧有到未来之特性,俱可囊括。
问题
代码生成以宏为先,虽是旧工具,然方今模板元编程的能力尚有不足,在产生式元编程的核心特性「源码注入」进入标准之前,它仍是一种常用的代码生成工具。
宏编程得从可变宏参数开始谈起,可变模板参数的个数可以通过 sizeof...(args)
获取,宏里面如何操作呢?便是本章讨论的问题。
接着便逐步分析问题,实现需求,其间穿插宏编程之原理。
分析与实现
宏只有替换这一个功能,所谓的复杂代码生成功能,都是基于这一核心理念演绎出来的。故小步慢走,循序渐进,便可降低理解难度。
问题是获取宏参数包的个数,第一步应该规范过程。
过程的输入是一系列对象,对象类型和个数是变化的;过程的输出是一个值,值应该等于输入对象的个数。如果将输入替换为任何类型的其他对象,只要个数没变,结果也应保持不变。
于是通过宏函数表示出过程原型:
#define COUNT_VARARGS(...) N
根据宏的唯一功能可知,输出只能是一个值。依此便可否定遍历迭代之类的常规方式,可以考虑多加一层宏,通过由特殊到普遍的思想来不断分析,推出最终结果。
#define GET_VARARGS(a) 1
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__)
目前这种实现只能支持一个参数的个数识别,可通过假设,在特殊的基础上逐次增加参数。于是得到:
#define GET_VARARGS(a, b) 2
#define GET_VARARGS(a) 1
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__)
若假设成立,通过暴力法已能够将特殊推到普遍,问题遂即解决。但是宏并不支持重载,有没有可能实现呢?通过再封装一层,消除名称重复。得到:
#define GET_VARARGS_2(a, b) 2
#define GET_VARARGS_1(a) 1
#define GET_VARARGS(...) GET_VARARGS_X(__VA_ARGS__)
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__)
至此可知,若要实现宏重载效果,必须确定 GET_VARARGS_X
中的 X
,而它又和参数个数相同,问题绕了一圈又回到起点,说明此路不通。
因此,回到特殊情况重新分析,函数名称已确定不能重复,唯一能够改变的就只剩下输入和输出。既然不能重载,那么输出也就不能直接写出,先同时改变输入和输出,满足特殊情况再说。
#define GET_VARARGS(N, ...) N
#define COUNT_VARARGS(...) GET_VARARGS(1, __VA_ARGS__)
已经支持一个参数,尝试写出两个参数的情况。
#define GET_VARARGS(N1, N2, ...) N?
#define COUNT_VARARGS(...) GET_VARARGS(1, 2, __VA_ARGS__)
输出是唯一确定的,这种尝试也以失败告终,于是排除改变输出的可能性,只余下输入是可以改变的。继续尝试:
#define GET_VARARGS(N1, N2, ...) N2
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__, 1, 2)
稍微改变输入顺序,便有了新的发现:当 __VA_ARGS__
个数为 1 时,N2 此时为 1;当为 0 时,N2 为 2。这表明间接层的输入参数之间具备着某种关系,接着扩大样本,寻找规律。
#define GET_VARARGS(N1, N2, N3, N4, N5, ...) N5
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__, 1, 2, 3, 4, 5)
列出表格分析:
参数个数 | N5 |
---|---|
0 | 5 |
1 | 4 |
2 | 3 |
3 | 2 |
4 | 1 |
由此可知,参数个数和输出顺序相反且少 1。故修改实现为:
#define GET_VARARGS(N1, N2, N3, N4, N5, ...) N5
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__, 4, 3, 2, 1, 0)
通过发现的规律,我们实现了由特殊到普遍的过程,函数的参数个数一般是有限的,只要再通过暴力法扩大数值范围,便能够为该需求实现一个通用的工具。
检验与优化
普遍性的解决方案还需要实践的检验,因为实现当中可能还会存在技术问题。这里的 __VA_ARGS__
就是一个问题,当输入参数个数为 0 时,替换之后会留下一个 ,
,这又打破了普遍性。
通过查阅资源,发现 ##__VA_ARGS__
可以消除这种情况下的逗号,但是它不能写在参数的开头。根据这个约束,改变参数,进一步优化实现。得到:
#define GET_VARARGS(Ignored, N1, N2, N3, N4, N5, ...) N5
#define COUNT_VARARGS(...) GET_VARARGS("ignored", ##__VA_ARGS__, 4, 3, 2, 1, 0)
至此,该实现终于具备普遍性,接着整理接口名称,使其更加规范。变为:
#define GET_VARARGS(_0, _1, _2, _3, _4, N, ...) N
#define COUNT_VARARGS(...) GET_VARARGS("ignored", ##__VA_ARGS__, 4, 3, 2, 1, 0)
在此基础上,将它由 4 推到 20、50、100…… 都不成问题,只要选择一个合适够用的数值就行。
再进一步检测,将其扩展到其他编译器进行编译,并尝试切换不同的语言版本编译,观察结果是否依旧一致。经过检测,发现 ##__VA_ARGS__
的行为并不通用,不同语言版本和编译期的结果都不尽相同。此时就需要进一步查找解决方案,增强实现的通用性。
最终查找到 C++20 的 __VA_OPT__
已经解决了这个问题,于是替换实现。
#define GET_VARARGS(_0, _1, _2, _3, _4, N, ...) N
#define COUNT_VARARGS(...) GET_VARARGS("ignored" __VA_OPT__(,) __VA_ARGS__, 4, 3, 2, 1, 0)
int main() {
printf("zero arg: %d\n", COUNT_VARARGS());
printf("one arg: %d\n", COUNT_VARARGS(1));
printf("two args: %d\n", COUNT_VARARGS(1, 2));
printf("three args: %d\n", COUNT_VARARGS(1, 2, 3));
printf("four args: %d\n", COUNT_VARARGS(1, 2, 3, 4));
}
若要支持 C++20 之前的版本,那么只需要再寻找办法,增加预处理即可。
通用性增强
经上述分析实现,「计算可变宏参数个数」的需求已基本由 COUNT_VARARGS
实现。
在此先总结一下用到的思想和技术,再往前行。
1.1 通过增加一个间接层,能够解决无法直接解决的问题。
1.2 小步快走,由特殊逐渐扩展到普遍,能够降低问题的解决难度。
1.3 规范过程,确认变与不变,逐步控制变量,能够全面分析问题。
1.4 尝试改变输入的顺序、大小、个数…… 也许能有新发现。
1.5 初步发现规律时,扩大样本验证,能够将特殊推到普遍。
1.6 可变宏参数作为输入和欲输出结果组合起来,其间规律可以表达条件逻辑。
1.7 扩展编译器及语言版本,能够更全面地测试解决方案的普遍性。
下面就来进一步完善解决方案,使 COUNT_VARARGS
支持 C++20 以下版本。
问题的关键在于 ##__VA_ARGS__
不具备通用性,此时一种精妙的技术是分情况讨论,即零参和多参分别处理。考虑起初分析时的一条失败道路:
#define GET_VARARGS_2(a, b) 2
#define GET_VARARGS_1(a) 1
#define GET_VARARGS(...) GET_VARARGS_X(__VA_ARGS__)
#define COUNT_VARARGS(...) GET_VARARGS(__VA_ARGS__)
这是函数重载的思路,因必须确定 GET_VARARGS_X
中的 X
,而 X
又和可变宏参数相关,可变宏参数又属于无限集,遂无法确定这个 X
,致此路不通。但若改变前提条件,使 X
的值属于有限集,这条路便可走通,这里便能够利用起来。只考虑零参和多参的情况,X
的取值范围就可以定在 0 和 1 之间。只需设法判断可变宏参数属于哪种情况,就可借此实现重载,分发处理逻辑。
根据假设,写出基本代码:
#define _COUNT_VARARGS_0(...) N
#define _COUNT_VARARGS_1() 0
#define COUNT_VARARGS_0(...) _COUNT_VARARGS_1(__VA_ARGS__)
#define COUNT_VARARGS_1(...) _COUNT_VARARGS_0()
#define OVERLOAD_INVOKE(call, version) call ## version
#define COUNT_VARARGS(...) OVERLOAD_INVOKE(COUNT_VARARGS_, IS_EMPTY(__VA_ARGS__))
定义两个重载宏函数 COUNT_VARARGS_1
和 COUNT_VARARGS_0
,前者处理无参情况,后者处理多参情况。如此一来,空包时 IS_EMPTY
返回 1,调用分发到 COUNT_VARARGS_1
,最终结果直接置为 0;多参时 IS_EMPTY
返回 0,调用分发到 COUNT_VARARGS_1
,使用前面的方法处理即可。
于是主要问题就为如何实现 IS_EMPTY
。根据结论 1.3 分析新的需求,其输入是可变宏参数,过程是判断空或非空,结果是 1 或 0。过程依旧是条件逻辑,而结论 1.6 正好可以表达条件逻辑,于是初步有了实现思路。
根据设想,继续写出基础代码:
#define HAS_COMMA(_0, _1, _2, ...) _2
#define IS_EMPTY(...) HAS_COMMA(__VA_ARGS__, 0, 1)
空包时,第一步替换结果为 HAS_COMMA(, 0, 1)
,第二步替换结果为 1。列出表格,分析更多样本。
IS_EMPTY() 输入 | 替换结果 |
---|---|
1 | |
1 | 1 |
, | 0 |
1,2 | 0 |
可以发现,空包和 1 个参数情况的结果相等,为 1;多参情况的结果相等,为 0。扩大 HAS_COMMA
的参数之后,结果依然成立。
#define HAS_COMMA(_0, _1, _2, _3, _4, _5, ...) _5
#define IS_EMPTY(...) HAS_COMMA(__VA_ARGS__, 0, 0, 0, 0, 1)
如今难题变成如何区分空包和 1 个参数,这个问题依旧不好处理。宏的唯一功能只有替换,可以尝试将空包替换成多参的同时,保持 1 个参数不变,这样就可以区分。下面的代码用于说明这个技巧:
#define _TRIGGER_PARENTHESIS(...) ,
#define TEST(...) _TRIGGER_PARENTHESIS __VA_ARGS__ ()
若参数为空,TEST()
第一步被替换为 _TRIGGER_PARENTHESIS ()
,第二步被替换为 ,
。而 ,
等价于 a, b
,属于两个参数,便达到了将空包变为多参的需求。若是参数为 1,TEST
第一步被替换为 _TRIGGER_PARENTHESIS 1 ()
,宏不会继续展开,只要不使用该参数,它也不会报错,还是当作 1 个参数。如此一来,问题解决,开始将基础代码合并。
#define _TRIGGER_PARENTHESIS(...) ,
#define _COMMA_CHECK(_0, _1, _2, _3, _4, _5, ...) _5
#define HAS_COMMA(...) _COMMA_CHECK(__VA_ARGS__, 0, 0, 0, 0, 1)
#define IS_EMPTY(...) HAS_COMMA( _TRIGGER_PARENTHESIS __VA_ARGS__ () )
重新列表格分析:
IS_EMPTY() 输入 | 替换结果 |
---|---|
0 | |
1 | 1 |
, | 0 |
1,2 | 0 |
() | 0 |
当前问题完美解决!暂停下来,调整接口,让它更加规范。一般 1 为真,0 为假,而现在 1 和 0 的意义完全相反,无法顾名思义,先把它改变过来。调整代码为:
#define _TRIGGER_PARENTHESIS(...) ,
#define _COMMA_CHECK(_0, _1, _2, _3, _4, _5, ...) _5
#define HAS_COMMA(...) _COMMA_CHECK(__VA_ARGS__, 1, 1, 1, 1, 0)
#define IS_EMPTY(...) HAS_COMMA( _TRIGGER_PARENTHESIS __VA_ARGS__ () )
调整后的表格为:
IS_EMPTY() 输入 | 替换结果 |
---|---|
1 | |
1 | 0 |
, | 1 |
1,2 | 1 |
() | 1 |
随后分析新产生的问题,可以发现多参之间又无法区分是否为空包。解决这个问题的思路是多条件约束,从结果集中依次剔除不满足的元素。先把使用技巧前后的表格汇总起来,寻找规律。
HAS_COMMA() 参数 \ 输入 | 1 | , | 1,2 | () | |
---|---|---|---|---|---|
__VA_ARGS__ | 0 | 0 | 1 | 1 | 0 |
_TRIGGER_PARENTHESIS __VA_ARGS__ () | 1 | 0 | 1 | 1 | 1 |
这两个条件组合起来,便可进一步排除包含 ,
的情况。在此基础上,充分利用排列组合,再增加了两个条件,表格变为:
HAS_COMMA() 参数 \ 输入 | 1 | , | 1,2 | () | |
---|---|---|---|---|---|
__VA_ARGS__ | 0 | 0 | 1 | 1 | 0 |
_TRIGGER_PARENTHESIS __VA_ARGS__ | 0 | 0 | 1 | 1 | 1 |
__VA_ARGS__ () | 0 | 0 | 1 | 1 | 0 |
_TRIGGER_PARENTHESIS __VA_ARGS__ () | 1 | 0 | 1 | 1 | 1 |
首先,使用 _TRIGGER_PARENTHESIS_ __VA_ARGS__ ()
区分了空包和 1 参的情况,得到的结果集包含空包和多参。其次,借助 __VA_ARGS__
区分了空包和 ,
(即为多参)的情况,两相组合,结果集便只剩下空包。
但还有一些特殊情况,比如第一个输入参数包含 ()
,此时 HAS_COMMA((1))
展开为 HAS_COMMA(, ())
,一个参数替换后变成两个参数会造成误判,以 _TRIGGER_PARENTHESIS __VA_ARGS__
能够进一步排除这类结果。
最后,如果输入为一个宏或无参函数,例如 HAS_COMMA(Foo)
,替换后就变成了 HAS_COMMA(_TRIGGER_PARENTHESIS Foo ())
,结果可能会出乎意料,通过 __VA_ARGS__ ()
能够排除这类结果。
对于无参宏/函数,举个例子,#define Foo() 1, 2
,结果加入表格中如下。
HAS_COMMA() 参数 \ 输入 | 1 | , | 1,2 | () | Foo | |
---|---|---|---|---|---|---|
__VA_ARGS__ | 0 | 0 | 1 | 1 | 0 | 0 |
_TRIGGER_PARENTHESIS __VA_ARGS__ | 0 | 0 | 1 | 1 | 1 | 0 |
__VA_ARGS__ () | 0 | 0 | 1 | 1 | 0 | 1 |
_TRIGGER_PARENTHESIS __VA_ARGS__ () | 1 | 0 | 1 | 1 | 1 | 1 |
这四个条件组合起来,也就是说四个条件的结果为 0001
,就可以唯一确认空包。现在便可运用老办法,添加重载,当重载为 0001
时,处理空包,返回 1,其他所有情况返回 0。实现如下:
#define _COMMA_CHECK(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, R, ...) R
#define HAS_COMMA(...) _COMMA_CHECK(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0)
#define _IS_EMPTY_CASE_0001 ,
#define _IS_EMPTY_CASE(_0, _1, _2, _3) _IS_EMPTY_CASE_ ## _0 ## _1 ## _2 ## _3
#define _IS_EMPTY_IMPL(_0, _1, _2, _3) HAS_COMMA(_IS_EMPTY_CASE(_0, _1, _2, _3))
#define IS_EMPTY(...) \
_IS_EMPTY_IMPL( \
HAS_COMMA(__VA_ARGS__), \
HAS_COMMA(_TRIGGER_PARENTHESIS_ __VA_ARGS__), \
HAS_COMMA(__VA_ARGS__ ()), \
HAS_COMMA(_TRIGGER_PARENTHESIS_ __VA_ARGS__ ()) \
)
四个条件的处理结果进一步传递到 _IS_EMPTY_IMPL
,它又通过 _IS_EMPTY_CASE
组装成重载特化 _IS_EMPTY_CASE_0001
。其他所有情况都没有定义相应的重载,因而经由 HAS_COMMA
调用 _COMMA_CHECK
的参数个数都相同,最终只会返回 0。而 _IS_EMPTY_CASE_0001
被替换为 ,
,相当于参数个数加一,最终返回 1。
子问题解决,现在回到原问题,看看最初的实现:
#define _COUNT_VARARGS_0(...) N
#define _COUNT_VARARGS_1() 0
#define COUNT_VARARGS_0(...) _COUNT_VARARGS_1(__VA_ARGS__)
#define COUNT_VARARGS_1(...) _COUNT_VARARGS_0()
#define OVERLOAD_INVOKE(call, version) call ## version
#define COUNT_VARARGS(...) OVERLOAD_INVOKE(COUNT_VARARGS_, IS_EMPTY(__VA_ARGS__))
IS_EMPTY
只有空包时才会返回 1,其他情况都返回 0。因此当前的重载版本已经可以使用,空包时最终的参数直接由 _COUNT_VARARGS_1
将结果替换为 0。对于 _COUNT_VARARGS_0
多参版本,只要把上节的实现添加起来便可以:
#define _COUNT_VARARGS_0_IMPL(_0, _1, _2, _3, _4, N, ...) N
#define _COUNT_VARARGS_0(...) _COUNT_VARARGS_0_IMPL(__VA_ARGS__, 5, 4, 3, 2, 1)
到这一步,这个问题就解决了。根据总结 1.7,进一步切换编译器检验一下方案的普遍性,发现 MSVC 结果错误。
原来是 MSVC 每次传递 __VA_ARGS__
时,都会将其当作整体传递。例如:
#define A(x, ...) x and __VA_ARGS__
#define B(...) A(__VA_ARGS__)
B(1, 2); // 替换为 1, 2 and
一种解决方案是强制宏展开,实现很简单:
#define EXPAND(x) x
#define A(x, ...) x and __VA_ARGS__
#define B(...) EXPAND( A(__VA_ARGS__) )
B(1, 2); // 替换为 1 and 2
为了适配 MSVC,我们需要将所有传递的 __VA_ARGS__
都强制展开。最终的实现为:
#include <cstdio>
#define EXPAND(x) x
#define _TRIGGER_PARENTHESIS(...) ,
#define _COMMA_CHECK(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, R, ...) R
#define HAS_COMMA(...) EXPAND( _COMMA_CHECK(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0) )
#define _IS_EMPTY_CASE_0001 ,
#define _IS_EMPTY_CASE(_0, _1, _2, _3) _IS_EMPTY_CASE_ ## _0 ## _1 ## _2 ## _3
#define _IS_EMPTY_IMPL(_0, _1, _2, _3) HAS_COMMA(_IS_EMPTY_CASE(_0, _1, _2, _3))
#define IS_EMPTY(...) \
_IS_EMPTY_IMPL( \
EXPAND( HAS_COMMA(__VA_ARGS__) ), \
EXPAND( HAS_COMMA(_TRIGGER_PARENTHESIS __VA_ARGS__) ), \
EXPAND( HAS_COMMA(__VA_ARGS__ ()) ), \
EXPAND( HAS_COMMA(_TRIGGER_PARENTHESIS __VA_ARGS__ ()) ) \
)
#define _COUNT_VARARGS_0_IMPL(_0, _1, _2, _3, _4, N, ...) N
#define _COUNT_VARARGS_0(...) EXPAND( _COUNT_VARARGS_0_IMPL(__VA_ARGS__, 5, 4, 3, 2, 1) )
#define _COUNT_VARARGS_1() 0
#define COUNT_VARARGS_0(...) EXPAND( _COUNT_VARARGS_0(__VA_ARGS__) )
#define COUNT_VARARGS_1(...) _COUNT_VARARGS_1()
#define OVERLOAD_INVOKE(call, version) call ## version
#define OVERLOAD_HELPER(call, version) OVERLOAD_INVOKE(call, version)
#define COUNT_VARARGS(...) EXPAND( OVERLOAD_HELPER(COUNT_VARARGS_, IS_EMPTY(__VA_ARGS__))(__VA_ARGS__) )
int main() {
// 0 1 2 3 4
printf("%d %d %d %d %d\n",
COUNT_VARARGS(), COUNT_VARARGS(1), COUNT_VARARGS('a', 'b'),
COUNT_VARARGS('a', 'b', 'c'), COUNT_VARARGS('a', 'b', 1, 2));
}
这种解决方案不仅支持 C++ 所有版本,而且在 C 中也能使用。
合并方案
最后,将两种方案合并起来,便能得到一个通用的实现。借助预处理分语言版本选择不同的实现,C++20 以上使用新的简洁做法,C++20 以下使用这种通用但复杂的做法。
#include <cstdio>
#if defined(__cplusplus) && __cplusplus >= 202002L
#define GET_VARARGS(_0, _1, _2, _3, _4, N, ...) N
#define COUNT_VARARGS(...) GET_VARARGS("ignored" __VA_OPT__(,) __VA_ARGS__, 4, 3, 2, 1, 0)
#else
#define EXPAND(x) x
#define _TRIGGER_PARENTHESIS(...) ,
#define _COMMA_CHECK(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, R, ...) R
#define HAS_COMMA(...) EXPAND( _COMMA_CHECK(__VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0) )
#define _IS_EMPTY_CASE_0001 ,
#define _IS_EMPTY_CASE(_0, _1, _2, _3) _IS_EMPTY_CASE_ ## _0 ## _1 ## _2 ## _3
#define _IS_EMPTY_IMPL(_0, _1, _2, _3) HAS_COMMA(_IS_EMPTY_CASE(_0, _1, _2, _3))
#define IS_EMPTY(...) \
_IS_EMPTY_IMPL( \
EXPAND( HAS_COMMA(__VA_ARGS__) ), \
EXPAND( HAS_COMMA(_TRIGGER_PARENTHESIS __VA_ARGS__) ), \
EXPAND( HAS_COMMA(__VA_ARGS__ ()) ), \
EXPAND( HAS_COMMA(_TRIGGER_PARENTHESIS __VA_ARGS__ ()) ) \
)
#define _COUNT_VARARGS_0_IMPL(_0, _1, _2, _3, _4, N, ...) N
#define _COUNT_VARARGS_0(...) EXPAND( _COUNT_VARARGS_0_IMPL(__VA_ARGS__, 5, 4, 3, 2, 1) )
#define _COUNT_VARARGS_1() 0
#define COUNT_VARARGS_0(...) EXPAND( _COUNT_VARARGS_0(__VA_ARGS__) )
#define COUNT_VARARGS_1(...) _COUNT_VARARGS_1()
#define OVERLOAD_INVOKE(call, version) call ## version
#define OVERLOAD_HELPER(call, version) OVERLOAD_INVOKE(call, version)
#define COUNT_VARARGS(...) EXPAND( OVERLOAD_HELPER(COUNT_VARARGS_, IS_EMPTY(__VA_ARGS__))(__VA_ARGS__) )
#endif
int main() {
printf("version: %ld\n", __cplusplus);
// 0 1 2 3 4
printf("%d %d %d %d %d\n",
COUNT_VARARGS(), COUNT_VARARGS(1), COUNT_VARARGS('a', 'b'),
COUNT_VARARGS('a', 'b', 'c'), COUNT_VARARGS('a', 'b', 1, 2));
}
总结
本章以可变宏参数计数为例,穿插讲解宏编程的核心原理,又涵盖问题分析思路与完善步骤,小步慢走,终是实现需求。
由此原理,宏以能表示基本逻辑,亦能演绎出诸多新工具,反过来简化如今的实现。其间细节,请俟后文。