首发于vczh的日常
考不上三本也能懂系列——实现C++类型系统(二)

考不上三本也能懂系列——实现C++类型系统(二)

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

上一篇文章居然已经一整年了。Hitman2,垃圾游戏,就我钱财,害我姓名,这东西开发完之前我绝对不会再玩游戏(骗你的

由于C++类型转换的规则实在过于复杂,昨天我添加了新的test case之后发现改不下去了,于是重写。当然我这个实现是不准确的,有些条款我就没有实现,譬如这一条:

struct A{};
struct B : A{};
struct C {};
struct D : B, C{};

如果你有两个重载函数,一个接受A&,一个接受B&,那你给一个D&它必然选择第二个,因为D距离B比较近。但是如果是A&和C&呢,就要报错,因为他不在同一条线上。好麻烦,所以我直接忽略了,我的编译器会认为D&转A&、B&和C&都是一样的,然后resolve到多个符号上,反正只是为了code index,重载不准确问题不大,被C++选中的那个函数在里面就好了。

前些日子终于做到了模板类了。模板类还没做完,接下来还有偏特化,还有模板函数的类型推导。其实偏特化问题不是很大,反正我没有做constant folding,根据一个tag来选择偏特化的分支的这个做法会被我忽略,同时返回所有可能的分支。每一个偏特化的类型在我看来都是不同的类型,所以都返回就好了(逃。

模板函数的类型推导,其实也不是特别复杂,C++的推导是先到先得的。譬如说你两个参数都有T,第一个参数推导出来T=int,第二个参数推导出来T=double,选择int会导致编译错误,选择double不会,但是C++仍然返回编译错误,因为他认为写C++的都是猛男,不能朝三暮四,说int就必须int到底。这个规则大大简化了实现。

最麻烦的还是模板。模板的问题在于模板类是可以嵌套的,再加上C++的模板参数也接受模板,导致了模板本身变成了一个闭包,就跟JavaScript的函数一样,可以把上下文封装在里面。到底这是怎么做的呢?可以看下面的例子。

template<typename T>
struct Fuck
{
    template<typename U>
    struct Shit
    {
        using Bitch = T(*)(U);
    };
};

template<typename<typename> TShit>
auto MakeShit() -> typename TShit<int>::Bitch
{
    return nullptr;
}

// MakeShit<Fuck<double>::Shit>() 会返回 double(*)(int)

闭包就是这个意思,你把double包装进去,能在里面被读到。这跟lambda表达式的捕获如出一辙。

obviously,我的数据结构也得把他做成一个闭包的样子。所以下面几个符号分别会返回这些类型:

  • Fuck: function(typename Fuck::T) -> ::Fuck<typename Fuck::T>
  • Fuck<double>: Fuck<double>
  • Fuck<double>::Shit: function(typename Fuck::Shit::U) -> Fuck<double::Shit<typename Fuck::Shit::U>
  • Fuck<double>::Shit<int>: Fuck<double>::Shit<int>

你一直延续到Fuck<double>::Shit<int>::Bitch<void*>也一样。这个类型由三部分构成

  • 主要类型:Bitch
  • 类型参数:void*
  • 闭包:Fuck<double>::Shit<int>

之所以要带上闭包,是因为Bitch里面也可以用到Fuck和Shit的类型参数,如果你不告诉他这个Bitch是在Fuck<double>::Shit<int>里面而不是什么Fuck<FacebookPipSuicide>::Shit<Alibaba996SuddenDeath>里面的,那到时候你看到T和U就懵逼了。

这个时候你可能会问,那为啥我不直接写成Fuck::Shit::Bitch<double, int, void*>呢,其实一样的是不是?这个就要回到上一篇文章了,我每次创造一个类型之后都是cache下来的,你无论什么时候构造出Fuck<double>::Shit<int>,无论多少遍,他都返回给你同一个指针。这样我就可以把闭包本身作为一个链表保存进去。

Fuck<double>内部有一个链表节点,记录了T=double这个事实。Shit<int>里面有一个链表节点,不仅记录了U=int,还记录了他的上一个链表节点是Fuck<double>。这样就节约了一点内存。当然节约内存并不是重点,重点在于这一个类型可能会在分析程序的过程中被构造无数遍,你反复构造这些数据结构就显得很浪费。

说到这里大家都知道要怎么做了(就在TsysDeclInstant类里)。这个链表节点最终可以被一个类型推导的上下文类ParsingArguments引用。这个ParsingArguments主要是解决你再看到一条表达式的时候,还知道“我现在在哪”的这么一个问题。看下面的一个例子:

template<typename T>
struct Fuck
{
    template<typename U>
    struct Shit
    {
        template<typename V>
        struct Bitch
        {
            auto Method() { return this; }
        };
    };
};

auto x = Fuck<double>::Shit<int>::Bitch<void*>().Method(); // 这是UB,不要学
auto y = Fuck<float>::Shit<short>::Bitch<char*>().Method();

显然,x的类型是Fuck<double>::Shit<int>::Bitch<void*>*,而y则是另一个类。这个类型都是在考察Method的时候得到的,你要知道Method的返回值,你就得进到里面看他到底return什么。然后你发现他return this;,说明返回值跟this的类型是一致的。这里用的是auto,所以你还要remove_reference+remove_cv。

那你怎么知道this是什么呢?代码分析到这里的时候,你就得看一眼ParsingArguments里面都保存了什么,譬如说T、U和V分别是什么。进入第一个函数的时候,这个Method是属于Fuck<double>::Shit<int>::Bitch<void*>的,所以这个类型最终会成为构造ParsingArguments的其中一个来源,这个类型内部保存的三个节点的链表最终就进了ParsingArguments里。

当你看到this的时候,你知道this在这里指的是Fuck<typename Fuck::T>::Shit<typename Fuck::Shit::U>::Bitch<typename Fuck::Shit::Bitch::V>,然后发现还有个链表,你就挨个搜索上去把T、U和V都替换掉,最后就变成了Fuck<double>::Shit<int>::Bitch<void*>。

只要把这个做完,就可以搞定GacUI的codeindex,GacUI文档里面的实例代码也就全都有链接了!Hitman2,垃圾游戏,就我钱财,害我姓名,这东西开发完之前我绝对不会再玩游戏(骗你的

编辑于 2019-10-27

文章被以下专栏收录