首发于vczh的日常
考不上三本也能懂系列——处理声明(三)

考不上三本也能懂系列——处理声明(三)

你现在所阅读的并不是第一篇文章,你可能想看目录和前言

为了生成文档用的C++前端写写停停已经一年半了,终于做到了偏特化。偏特化写完,lambda写完,再把文档的生成程序搞定,最后写GacUI的文档,1.0就大功告成了。不过短短的一句话,估计又得花大半年。Hitman 2、Sekiro、Assassin Creed Odyssey、Mutant Year Zero和Disco Elysium真是太好玩了(逃

上一篇文章讲到了如何把处理多级模板声明的声明和实现互相拼接。这一篇文章就来讲偏特化。C++可以做偏特化/全特化的,只有类、函数和模板变量(也就是下面这一堆)。下面是我的其中一个测试用例,在这里我们给所有的模板变量编号,然后下一步当然是要把偏序关系处理出来,最终才能在实例化模板变量的时候找到那一个(或多个)具体的实现。

template<typename... Ts>
struct Types;

template<typename... Ts>
constexpr auto Value = 0;

template<>
constexpr auto Value<> = 1;

template<typename A>
constexpr auto Value<A> = 2;

template<typename A, typename B, typename C>
constexpr auto Value<A, B, C> = 3;

//-------------------------------------------------------------

template<typename A, typename B>
constexpr auto Value<A*, B> = 4;

template<typename A, typename B>
constexpr auto Value<A, B*> = 5;

template<typename A, typename B>
constexpr auto Value<A*, B*> = 6; // 4, 5, 23

template<typename A, typename B>
constexpr auto Value<const A*, B> = 7; // 4

template<typename A, typename B>
constexpr auto Value<A, const B*> = 8; // 5

template<typename A, typename B>
constexpr auto Value<const A*, const B*> = 9; // *6, *7, *8, *24

//-------------------------------------------------------------

template<typename A, typename B, typename C, typename... Ts>
constexpr auto Value<Types<Ts...>, A, B, C> = 10;

template<typename A, typename B, typename C>
constexpr auto Value<Types<A, B, C>, A, B, C> = 11; // 10

template<typename A, typename B, typename C>
constexpr auto Value<Types<A, B, C>, C, B, A> = 12; // 10

template<typename A, typename B>
constexpr auto Value<Types<A, B, A>, A, B, A> = 13; // *11, *12

template<typename A>
constexpr auto Value<Types<A, A, A>, A, A, A> = 14; // *13

template<typename A, typename B, typename C, typename... Ts, typename... Us>
constexpr auto Value<void(*)(Ts..., A, Us...), B, C, A, Ts...> = 15;

template<typename A, typename B, typename... Ts>
constexpr auto Value<void(*)(Ts..., A, Ts...), A, B, A, Ts...> = 16; // 15

template<typename A, typename B>
constexpr auto Value<void(*)(float, double, A, char, wchar_t), A, B, A, float, double> = 17; // 15

template<typename A, typename B>
constexpr auto Value<void(*)(float, double, A, float, double), A, B, A, float, double> = 18; // *16

template<typename A>
constexpr auto Value<void(*)(float, double, A, char, wchar_t), A, A, A, float, double> = 19; // *17

template<>
constexpr auto Value<void(*)(float, double, bool, char, wchar_t), bool, bool, bool, float, double> = 20; // *19

template<>
constexpr auto Value<void(*)(float, double, bool, char, wchar_t), bool, int, bool, float, double> = 21; // *17

template<>
constexpr auto Value<void(*)(float, double, bool, char, wchar_t), char, wchar_t, bool, float, double> = 22; // 15

//-------------------------------------------------------------

template<typename... Ts>
constexpr auto Value<Ts*...> = 23;

template<typename... Ts>
constexpr auto Value<const Ts*...> = 24; // 23

template<typename A>
constexpr auto Value<A*, A*> = 25; // *6

template<typename A>
constexpr auto Value<const A*, const A*> = 26; // *25, *9

template<>
constexpr auto Value<float*, double*> = 27; // *6

template<>
constexpr auto Value<float*, float*> = 28; // *25

template<>
constexpr auto Value<const float*, const double*> = 29; // *9

template<>
constexpr auto Value<const float*, const float*> = 30; // *26

偏序关系是什么意思呢?我们在做偏特化的时候,有一些是比较具体的,有一些是比较不具体的。如果我们匹配到了那个具体的,那我们就要放弃那个不具体的,否则就有二义性了。我来举一个二义性的例子:

template<typename A, typename B> constexpr auto Value = 0;
template<typename T> constexpr auto Value<int, T> = 1;
template<typename T> constexpr auto Value<T, int> = 2;

显然,当我们使用Value<int, int>的时候,就会在1和2中徘徊,因为哪一个都没有“更具体”,就出现了二义性的错误。显然,为了在不该出现二义性的时候不要出现二义性,我们就得给这一大堆声明排序,先弄明白哪个具体哪个不具体。下面我给一张图,就是上面那一堆声明的“谁更具体关系”的偏序图:

看起来还挺复杂的,偏特化东西一多,图就不太好整理(逃。举个例子,如果我们的一个声明命中了13,那么10、11、12肯定也都命中。但是由于图中的关系我们可以得知,13比他们都更具体,所以这是没有二义性的。

在这里必须指出,如果我们写了两个一模一样的偏特化,这是不算错误的,只是会在使用的时候才出现二义性。在这种时候,虽然任何一个都可以当成比另一个“更具体”,但是因为这个关系不能对称,所以我们定义这种情况是不连线。

为了完成这个工作,我们要分两步走。第一步是决定这个箭头的方向。譬如说13,可以同时指向10、11、12,但是反过来不行,这个判断的算法得先写出来。第二部就是组合成偏序图,因为11、12一样可以指向10,所以13没有必要指向10。这两部做完,偏序图就构造出来了。

第二步还没写所以就先不说了,我现在可以预见到一个困难,就是C++处理的时候,有可能所有的Value都还没发现完就要先用到,所以当然是发现一个就要往图中插入一个,而不能等所有的关系都搞清楚了之后再一次性构造出这个图来。算法复杂度可能不是很好降。不过没关系,GacUI最大型的那个偏特化是反射用的一个巨大的文件,超过99%以上都是全特化,就算瞎JB写最多也就退化成O(n²),还是可以接受的。

第一步是比较有意思的。譬如说里面的15和17:

template<typename A, typename B, typename C, typename... Ts, typename... Us>
constexpr auto Value<void(*)(Ts..., A, Us...), B, C, A, Ts...> = 15;

template<typename A, typename B>
constexpr auto Value<void(*)(float, double, A, char, wchar_t), A, B, A, float, double> = 17; // 15

17比15更具体,所以如果我们可以求出来,15的代码A、B、C、Ts、Us在替换成什么的时候可以变成17的代码,那我们就证实了这个关系。

首先我们看void(*)(Ts..., A, Us...)对void(*)(float, double, A, char, wchar_t),我们发现根本不知道15的A是17里面5个的哪个,所以只好先放弃,后面再说

然后很容易就可以求出,B替换成A、C替换成B、A替换成A、Ts替换成{float, double}。

一轮下来,我们发现我们曾经放弃过一些匹配,那么我们重新来尝试一下

我们再看一遍void(*)(Ts..., A, Us...)对void(*)(float, double, A, char, wchar_t),现在我们知道Ts...对应的是{float, double},那么A当然要替换成A。在这里我们注意到,第一轮的计算结果A的结果已经算出来了,现在我们得到一个A的新结果,比较一下,发现竟然是一样的,太好了!然后就是Us替换成{char, wchar_t}。

两轮下来,我们发现我们没有放弃过匹配,没有失败,A、B、C、Ts、Us全都算出来了,匹配成功!所以17可能比15更具体。

当然这个关系暂时还不能确定,我们得选一下15是否比17更具体,当然我们很容易知道答案是“否”。既然17比15更具体,15不比17更具体,他不对称,所以答案就出来了:17比15更具体。

方法很简单,不过写成代码还是比较烦的,因为我们有几个问题需要处理。首先是函数参数的类型,T*和T[]是一样的,但是在其他地方就不一样,这个需要注意。第二个是缺省参数的问题。如果我们匹配X<A, B>和X<C, D, E>,我们就得用X的第三个参数的缺省参数先把第一个给补上,然后再算。

不过好消息是,因为我不这么用,所以我可以以后需要再实现(逃

剩下的细节就比较琐碎了。比较两个类型不难,譬如说A*跟B*,都是指针,所以接下来就比A和B,递归下去。如果两个类型不能这样递归,那就匹配失败。如果A已经到了一个具体的模板类型参数,那么B就是A的结果。如果A已经被匹配过了,那么每一次匹配的结果都必须一样,否则就匹配失败。

C++的事情真是多啊。我已经忽略了很多东西了,譬如说我这个前端当然是不会去求一个常量具体的值的,所以所有常量我都当成是一样的。就这样还搞得这么麻烦,而且只完成了C++17的大部分内容。不过反正已经没剩下多少了,很快就可以继续投入到TypeScript的怀抱了,因为文档是TypeScript写的,到时候C++生成TypeScript一把梭(逃

编辑于 03-29

文章被以下专栏收录