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

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

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

前言

距离上一篇文章已经有一年了,这次我终于支持到了写在类外面的模板函数,所以来更新一下。有些人可能不知道要怎么把模板成员函数的实现写到类的外面去,这里贴个简单的例子:

template<typename T>
struct X
{
    template<typename U>
    T Method(U u);
};

template<typename T>
struct X<T*>
{
    template<typename U>
    shared_ptr<T> Method(U u);
};

template<typename T>
template<typename U>
T X<T>::Method(U u) { throw 0; }

template<typename T>
template<typename U>
shared_ptr<T> X<T*>::Method(U u) { throw 0; }

大概就是这样。这里面一共有三个问题,首先是多个template<...>要怎么处理,其次是你要知道你分析的东西是一个成员函数,最后你要知道有那么多个X,X里面还有那么多个Method,到底这个声明对应的是哪个。由于我现在还没做偏特化,所以第三个问题其实还没处理完。

C++模板

C++的模板,只有在处理写在类外的模板成员函数的时候,才允许你叠加多个template<...>。于是我们只要连续parse完几个template<...>之后,看看后面是什么关键字就懂了。template后面接的关键字只能是class, struct, union, enum, using。如果你发现它不是,那一般有三种情况。

第一种情况就是变量,譬如说

template<typename T>
constexpr Zero = static_cast<T>(0);

第二种情况就是函数了,譬如说上面的例子。

第三种情况是C++17新出的class template argument deduction guide。这个比较有意思,在这里说两句。这个功能的出发点是,ISO觉得make_shared_ptr啊make_tuple这些东西太烦了,要是构造类型的实例的时候,能够跟调用模板函数一样省略类型参数,那该多好啊。但是有一个问题没法解决。链接里面就提到了一个例子,STL里面经常用到的iterator总是要成对出现,如果你想做一个类把begin和end包围进去,你就重新发明了新标准里面的range(也就是山寨Linq(逃)。于是我们可以写一个这样的类:

template<typename T>
struct container
{
    template<typename TIterator>
    container(TIterator begin, TIterator end);
};

用的时候很简单。譬如说我们有

  • int xs[] = {1,2,3,4,5};

我们就可以这么做:

  • auto c1 = container<int>(begin(xs), end(xs)); // begin和end分别返回&xs[0]和&xs[5],标准库里的
  • container<int> c2 { begin(xs), end(xs) };

更进一步,要是能这样写就更好了是不是:

  • auto c3 = container(begin(xs), end(xs));

但是C++做不了。为什么呢,因为你给两个int*参数进container的构造函数里,我怎么知道你外面的T想要int?所以要写一个make_container,大概长这个样子:

template<typename TIterator>
auto make_container(TIterator begin, TIterator end)
    -> container<typename std::iterator_traits<TIterator>::value_type>
{
    return {begin, end};
}

auto c4 = make_container(begin(xs), end(xs));

多麻烦啊(其实并不觉得(逃),所以C++17就加入了CTAD Guide,把make_container跟struct container写在了一起:

template<typename T>
struct container
{
    template<typename TIterator>
    container(TIterator begin, TIterator end);
};

template<typename TIterator>
container(TIterator, TIterator)
    -> container<typename std::iterator_traits<TIterator>::value_type>;

auto c3 = container(begin(xs), end(xs));

CTAD Guide就说到这里了,其实就是很简单的一个东西,让你不用写一堆make函数。我还没了解过CTAD Guide能不能偏特化,但是我觉得完全没有理由不行。

好了,但是GacUI并没有用上CTAD Guide,所以我的C++编译器并不支持,哈哈哈哈。我们回到正文。现在我们手上拿到了几个template<...>,接下来的token也不是那几个关键字,所以我还得判断它是变量还是函数,然后才可以接着做下一步。

分析declarator

我们当务之急是找到这个declarator的名字在哪里。根据上一篇文章的介绍,我们可以把这个过程提炼成一个递归的算法。首先我们要parse出一个类型,譬如说我们看到:

  • shared_ptr<T> ...

在这后面还可以有很多东西,譬如说*、&、&&、const、volatile等等。我们把这一堆东西全部处理完,然后看下一个token。这个时候我们可能会看到若干种情况。

  • 第一种是看到了__stdcall这样的东西,那我们把它保存起来,然后接着往下看。
  • 第二种是看到了一个名字,bingo!这就是我们要找的
  • 第三种是看到了一个类型。如果shared_ptr<T>后面接着是X<T>,你觉得可能是什么呢?非常明显了,要么这个declarator没有名字,要么他就只能是shared_ptr<T> X<T>::Method。总之都是类成员没错了。
  • 第四种是看到了一个括号,这就麻烦了,因为这个declarator是嵌套的。在这里我们可以递归调用一下自己,把“首先parse出一个类型”用shared_ptr<T>代替,然后跳过那个左括号,然后搞出一个declarator,然后跳过一个右括号,然后接着看后面是(还是[,我们就知道这是个函数类型还是数组类型。把这个类型组装起来之后,整个类型换掉declarator里面某一个部位的shared_ptr<T>,你就得到了正确的类型。

关于第四种情况我们举一个例子:

  • shared_ptr<T> (* const X<T>::Method)(void);
    • 第一步,我们拿到了shared_ptr<T>
    • 第二步我们看到了一个左括号,先不管
    • 第三步我们继续parse下去,得到了shared_ptr<T> * const X<T>::Method
    • 第四步就必须是一个右括号了,然后继续看后面的(或者[,我们得到了shared_ptr<T>(void)
    • 最后一步了,是个骚操作,我们用“shared_ptr<T>(void)”去替换掉“shared_ptr<T> * const X<T>::Method”里面的“shared_ptr<T>”,就得到了正确的declarator:
      • X<T>::Method
      • shared_ptr<T>(*const)(void)

所以针对我们一开始的例子

template<typename T>
template<typename U>
T X<T>::Method(U u) { throw 0; }

我们终于parse出来了,这个declarator的名字是X<T>::Method,他的函数类型是T (U u),后面还跟了一个函数体。这个时候后简单了,我们只要找到X<T>,然后再找合适的Method就行了。

匹配实现和声明

大家需要注意的是,因为这写在类外面,所以实际上我们可以不用T和U,我们改成A和B效果也是一样的。所以我们要顺着X<T>::Method(它也可以是A::B<T>::C::D<U>::E::F)这一串名字里面的X和Method,看一下他们是不是模板。如果是,我们就把一个template<...> assign给他,最后得到了下面的对应关系:

  • X<T> -> template<typename T>
  • Method -> template<typename U>

X我们已经知道是什么了,所以我们主要看后面的<T>,就可以知道他到底是哪个偏特化的分支了。譬如说我们这里写的是X<A*, B*>,然后我们匹配template<typename T, typename U> struct X<T*, U*>,我们就知道T的对应关系分别是T->A, U->B。然后我们考察一下两个模板头template<typename T, template U>和template<typename A, typename B>,欸,顺序也对,样子也对,好了就是你了!

当所有的class都分配掉template<...>之后,如果还有剩,那肯定就是这个Method的。我们如法炮制,就可以在一大堆重载函数里面挑出正确的Method。

尾声

文章里面轻描淡写一笔带过了很多东西,其实真的做出来超级复杂。C++的语法还是太自由了,譬如收我们熟知的“<是generic还是operator”的这个歧义,C#看那个表达式的样子我们就能直接看出来。当然C#里面的一个变量,有可能是泛型,也有可能不是泛型。但是语法分析器是不管的,你表达式那么写,我就那么分析,最后匹配上去发现不是就报错。

C++就不一样了,他会真的在语法分析的过程中去看你这个变量是模板还是非模板,然后给你决定后面的<到底是什么意思。

过两天等我把一些杂事搞定,就可以更新我的编译器输出的HTML了。上次还是在test case里面直接输出了个网站,这次我打算输出一些元数据,然后用TypeScript去把网页实现出来,然后更新一下demo。

做完这些之后,再把剩下的C++功能都实现了,然后就可以用它来parse整个GacUI了。想想都刺激!这个做出来之后,我甚至可以给文档里面的实例代码加链接。这就是我做这个C++前端的最终目标。

编辑于 2019-11-02

文章被以下专栏收录