+-
C++模板元编程实战:模板型模板参数与容器模板

元函数可以操作类型与数值;对于元函数来说,类型与数值并没有本质上的区别,它们都可视为一种"数据",可以作为元函数的输入与输出。

事实上, C++元函数可以操作的数据包含3类:数值、类型与模板,它们统一被称为"元数据",以示与运行期所操作的“数据“有所区别。

1 模板作为元函数的输入

模板可以作为元函数的输入函数,考虑下面的代码:

...

1-7行定义了元函数Fun ,它接收两个输入参数:一个模板与一个类型。将类型应用于模板之上,获取到的结果类型作为返回值。在第9行,使用这个元函数并以std::remove_reference与int&作为参数传入。根据调用规则,这个函数将返回int,即我们在第9行声明了一个int类型的变量h并赋予值3.

从函数式程序设计的角度上来说,上述代码所定义的Fun是一个典型的高阶函数,即以另一个函数为输入参数的函数。可以将其总结为如下的数学表达式(为了更明确地说明函数与数值的关系,下式中的函数以大写字母开头,而纯粹的数值则是以小写字母开头) :

Fun(T1,T2)=T1(t2)

2 模板作为元函数的输出

与数值、类型相似,模板除了可以作为元函数的输入,还可以作为元函数的输出,但编写起来会相对复杂一些。

考虑下面的代码:

...

代码的1~13行定义了元函数Fun_:

输入为true时,其输出Fun_<true>::type为函数模板add_Ivalue_reference ,这个函数模板可以为类型增加左值引用;

输入为false时,其输出Fun _<false>::type为函数模板remove_reference ,这个函数模板可以去除类型中的引用。

代码的15~17行定义了元函数Fun ,与之前的示例类似, Fun<bool>是Fun_ <bool>::type的简写,注意这里的using用法:为了实现Fun ,我们必须引入两层template声明:内层(第16行)的template定义了元函数Fun的模板参数;而外层(第15行)的template则表示了Fun的返回值是一个接收一个模板参数的模板--这两层的顺序不能搞错,

代码段的19-20行是应用元函数Fun计算的结果:输入为false ,输出结果保存在Res_中。注意此时的Res_还是一个函数模板,它实际上对应了std::remove_reference——这个元函数用于去除类型中的引用。而第22行则是进一步使用这个函数模板(元函数的调用)来声明int型的对象h。

如果读者对这种写法感到困惑,难以掌握,没有太大的关系。因为将模板作为元函数输出的实际应用相对较少。但如果读者在后续的学习与工作中遇到了类似的问题,可以将这一小节的内容作为参考。

与上一小节类似,这里也将整个的处理过程表示为数学的形式,如下:

Fun(addOrRemove)=T

其中的addOrRemove是一个bool值,而T则是Fun的输出,是一个元函数。

3容器模板

学习任何一门程序设计语言之初,我们通常会首先了解该语言所支持的基本数据类型,比如C++中使用nt表示带符号的整数。在此基础上,我们会对基本数据类型进行一次很自然地扩展:讨论如何使用数组。与之类似,如果将数值、类型、模板看成元函数的操作数,那么前文所讨论的就是以单个元素为输入的元函数。在本节中,我们将讨论元数据的“数组"表示:数组中的"元素可以是数值、类型或模板。

可以有很多种方法来表示数组甚至更复杂的结构。《C++模板元编程》一书讨论了C++模板元编程库MPL ( Boost C++ template Meta-Programming library ),它实现了类似STL的功能,使用它可以很好地在编译期表示数组、集合、映射等复杂的数据结构.

但本书并不打算使用MPL,主要原因是MPL封装了一些底层的细节,这些细节对于元编程的学习来说又是非常重要的。如果简单地使用MPL,将在一定程度上丧失学习元编程技术的机会。而另一方面,掌握了基本的元编程方法之后再来看MPL,就会对其有更深入的理解,同时使用起来也会更得心应手。这就好像学习C++语言时,我们通常会首先讨论int a这样的数组,并以此引申出指针等重要的概念,在此基础上再讨论vector<int>时,就会有更深入的理解。本书会讨论元编程的核心技术,而非一些元编程库的使用方式。我们只会使用一些自定义的简单结构来表示数组,就像int这样,简单易用。

从本质上来说,我们需要的并非一种数组的表示方式,而是一个容器:用来保存数组中的每个元素。元素可以是数值、类型或模板。可以将这3种数据视为不同类别的操作数,就像C++中的int与loat属于不同的类型。在元函数中,我们也可以简单地认为“数值"与“类型"属于不同的类别。典型的C++数组(无论是int*还是vector<int> )都仅能保存一种类型的数据。这样设计的原因首先是实现比较简单,其次是它能满足大部分的需求。与之类似,我们的容器也仅能保存一种类别的操作数,比如一个仅能保存数值的容器,或者仅能保存类型的容器,或者仅能保存模板的容器。这种容器已经能满足绝大多数的使用需求了。

C++ 11中引入了变长参数模板( variadic template ) ,使用它可以很容易地实现我们需要的容器 :

...

上面的代码段声明了5个容器(相当于定义了5个数组)。其中前两个容器分别可以存放int与bool类型的变量;第3个容器可以存放类型;第4个容器可以存放模板作为其元素,每个模板元素可以接收一个类型作为参数;第5个容器同样以模板作为其元素,但每个模板可以放置多个类型信息。

细心的读者可能发现,上面的5条语句实际上是声明而非定义(每个声明的后面都没有跟着大插号,因此仅仅是声明)。这也是C++元编程的一个特点:事实上,我们可以将每条语句最后加上大括号,形成定义。但思考一下,我们需要定义吗?不需要。声明中已经包含了编译器需要使用的全部信息,既然如此,为什么还要引入定义呢?事实上,这几乎可以称为元编程中的一个惯用法了-仅在必要时才引入定义,其他的时候直接使用声明即可。在后文中,我们会看到很多类似的声明,并通过具体的示例来了解这些声明的使用方式。

事实上,到目前为止,我们已经基本完成了数据结构的讨论-深度学习框架只需要使用上述数据结构就可以完成构造了。如果你对这些结构还不熟悉,没关系,在后面构造深度学习框架的过程中,我们会不断地使用上述数据结构,你也就会不断地熟悉它们。

数据结构仅仅是故事的一半,一个完整的程序除了数据结构还要包含算法。而算法则是由最基本的顺序、分支与循环操作构成的。在下一节,我们将讨论涉及元函数时,该如何编写相应的顺序、分支或循环逻辑。

本文节选自《C++模板元编程实战:一个深度学习框架的初步实现》

...

本书将以一个深度学习框架的实现为例,讨论如何在一个相对较大的项目中深入应用元编程,为系统性能优化提供了更多的可能。本书分8章,前两章讨论了一些元编程与编译期计算的基本技术,后面六章则讨论了元编程在深度学习框架中的实际应用,涉及到富类型与标签体系、表达式模板、复杂元函数的编写等多个主题,详尽地展示了如何将面向对象与元编程相结合以构造复杂系统。