引论
C++的发展史是一个不断吸收程序设计领域精华和不断积累充实语言特性的过程。它的创造者Stroustrup在这门新的编程语言草创之初就定下了几个基本的目标,二十年过去了,至今这些目标仍然是C++继续发展的指南针。其中他明确指出,这种语言不应强迫程序员使用单一程序设计形式[20];就是说C++语言应该是一种“多种花样”的语言。正是因为Stroustrup这种高瞻远瞩的构想,才使得C++逐渐发展成为一门最具综合性、用途最广泛的语言,受到世界上最多的程序员喜爱。也正是因为同样的原因,人们在使用C++的过程中,探索出许多精巧优雅的编程技法,其中不少技法依赖着C++本身丰富的特性,在别种编程语言中并没有对应。例如伴随C++的模板技术而衍生的各种各样的技法,就能生动地说明这一点。
(一)C++ 模板元编程的发展
1994年的一次C++标准委员会的会议上,Erwin Unruh提交了一个很特别的C++程序,这个程序能够产生一系列素数,但与普通程序最大的不同,运算结果的产生是在编译期完成的,为了显示这些结果,甚至故意产生了一些编译期的错误信息,也就是说,程序从未完成编译过程,却能得到结果[13]。过程如此奇异,实际上是利用了C++模板的可递归特性,或者称为“编译期递归”。
受此启发,Todd Veldhuizen在1995年的一篇文章[27]中提出了“模板元编程(Template Metaprogramming)”这个概念,不但指出模板的特化(Specialization)能力可以用于针对编译期的编程,而且提出了几种实用的控制分支结构。从此C++的编程技术分出一门旁支,C++代码拥有的编译期和运行期的两段执行能力受到人们关注。
Krzysztof Czarnecki与Ulrich W. Eisenecker在1999年出版了《Generative Programming Methods, Tools, and Applications》一书,这本书对模板元编程的贡献是实现了一个辅助库,提出了更完善的控制结构的编写方法。
2004年出版的《C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond》一书归纳了十年来模板元编程的技术动向,成为第一本论述C++模板元编程技术的的专著。
模板元编程尽管处于C++程序设计的浪尖,仍然有不少C++程序库的作者直接或间接地利用它,甚至开发出专用于模板元编程的库,当中包括著名的Boost、Loki和Blitz++等。其中Boost提供了MPL(Meta-Programming Library),用以辅助模板元编程,当中提出了许多极有创新性的概念。
目前模板元编程已经成为C++语言的一项庞大的分支,并仍然在迅速发展。许多C++程序员都十分热衷于讨论这门技术,不断提出很多新颖的概念。
(二)C++模板元编程的概念
元编程(Metaprogramming)的前缀meta-表示“处于较高发展状态”的意思,从这个意义上讲,metaprogramming指的就是高阶的编程,即编写一种程序,使得这种程序可以把某些程序作为输入数据,生成或者操作其他的程序。一般而言,这些高阶的程序运行在编译时。因此,元编程并非一个新概念,事实上任何一种高级语言的编译器都可以视为元编程的工具,因为它们的程序必须被转换为汇编语言或者机器语言以后,才能够运行在计算机上。Lex和Yacc也是很传统的元编程工具,它们能够把一种新的语言根据规范转换成对应的C语言代码。
C++强大的模板机制赋予了模板在编译时的运算能力。例如以下的代码计算n!,借助于模板的特化能力形成递归,所有运算都在编译时完成:
view plaincopy to clipboardprint?
template< unsigned n >
struct factorial
{
static const unsigned value = n * factorial < n-1 > ::value;
};
template<>
struct factorial<0>
{
static const unsigned value = 1;
};
int main()
{
std::cout<<factorial < 6 > ::value<<std::endl; //6! = 720
return 0;
}
template< unsigned n >
struct factorial
{
static const unsigned value = n * factorial < n-1 > ::value;
};
template<>
struct factorial<0>
{
static const unsigned value = 1;
};
int main()
{
std::cout<<factorial < 6 > ::value<<std::endl; //6! = 720
return 0;
}
在这个例子里面,factorial在传统意义上是一个类模板,但用元编程的观点去看待,则可以认为是一个元函数(Metafunction)。这个元函数接受一个整型的模板参数,它的返回值是元函数的一个静态成员。注意元函数与普通的函数很不一样,一方面,元函数往往需要定义一个以上的特化版本作为终结条件,另一方面,元函数的返回值需要显式地提取出来。
这个简单的程序似乎仅仅是玩弄技巧,但正如1994年Erwin Unruh所演示的那个载入史册的素数生成程序,它们其实都只是引起人们注意的冰山一角,为了深入研究模板元编程的能力、使用范围和局限性,必须清楚了解其背后的机理。
模板元编程的原理
(一)函数式编程
1.函数式编程的概念
从上面的例子可以看出,由于模板元编程借助的是C++模板的特化能力,使它的设计方法迥异于普通的C++编程习惯。比如我们不能够在元函数中使用变量,编译期显然只可能接受静态定义的常量,也就因此,我们不能够使用传统意义上的循环;要实现任何分支选择,唯一的方法是调用其它元函数或者使用递归。这种编程风格正是具有重要理论意义的函数式编程(Functional Programming)。
Kenneth C. Louden指出函数式编程有如下的特点:
l 所有的过程都是函数,并且严格地区分输入值(参数)和输出值(结果)。
l 没有变量或赋值(变量被参数替代)。
l 没有循环(循环杯递归调用替代)。
l 函数的值只取决于它的参数的值而与求得参数值的先后或者调用函数的路径无关。
l 函数是一类值。
上述的最后一点是一个很重要的性质。所谓“函数是一类值(First Class Value)”指的是函数和值是同等的概念,一个函数可以作为另外一个函数的参数,也可以作为值使用。如果函数可以作为一类值使用,那么我们就可以写出一些函数,使得这些函数接受其它函数作为参数并返回另外一个函数。比如定义了f和g两个函数,用compose(f,g)的风格就可以生成另外一个函数,使得这个函数执行f(g(x))的操作,则可称compose为高阶函数(Higher-order Function)。
2.函数式编程在C++中的常用方法
如果排除上述的最后一点,那么C语言已经能完整模拟出函数式编程的风格,但是在C语言中,函数却并不能作为一类值。也许我们会想到函数指针,但是试想如果我们的函数需要返回另一个函数的指针:
view plaincopy to clipboardprint?
//这个例子是不能通过编译的
typedef int(*IntProc) (int);
IntProc compose(IntProc f, IntProc g)
{
int tempProc(int x)
{
return f(g(x));
}
return tempProc;
}
//这个例子是不能通过编译的
typedef int(*IntProc) (int);
IntProc compose(IntProc f, IntProc g)
{
int tempProc(int x)
{
return f(g(x));
}
return tempProc;
}
这个例子是不能通过编译的,因为C语言禁止在函数中定义函数。
幸运的是,我们可以在C++中利用类和模板来解决这个问题,因为C++允许定义()操作符,建立所谓的仿函数(Functor)。所以对象既可以作为值来传递和调用,又可以像函数一样用obj(x)的语法来使用了;另一方面,利用模板对返回值的控制,就可以避免上面无法定义内部函数的矛盾了。例如在GCC的 STL中有一个不属于C++标准的compose1函数,可以接受两个定义了()操作符的对象作为函数参数,并返回一个能进行f(g(x))运算的对象:view plaincopy to clipboardprint?
#include<iostream>
#include <functional>
using namespace std;
template <class Operation1, class Operation2>
class unary_compose: public unary_function< typename Operation2::argument_type, typename Operation1::result_type >
{
protected:
Operation1 op1;
Operation2 op2;
public:
unary_compose(const Operation1& x, const Operation2& y): op1(x), op2(y)
{
}
typename Operation1::result_type operator()(const typename Operation2::argument_type& x) const
{
return op1(op2(x));
}
};
template <class Operation1, class Operation2>
inline unary_compose<Operation1, Operation2> compose1(const Operation1& op1, const Operation2& op2)
{
return unary_compose<Operation1, Operation2>(op1, op2);
}
//仿函数 f(x) = x * 10
class f
{
public:
typedef double result_type;
typedef double argument_type;
result_type operator()(const argument_type& para) const //必须带const,原因是unary_compose的operator()带const
{
return para * 10;
}
};
//仿函数 g(x) = x + 4
class g
{
public:
typedef double result_type;
typedef double argument_type;
result_type operator()(const argument_type& para) const
{
return para + 4;
}
};
int main()
{
f f1;
g g1;
cout << compose1(f1,g1)(3) << endl;//求表达式f( g(3) ) = (3 + 4) * 10 = 70
return 0;
}
#include<iostream>
#include <functional>
using namespace std;
template <class Operation1, class Operation2>
class unary_compose: public unary_function< typename Operation2::argument_type, typename Operation1::result_type >
{
protected:
Operation1 op1;
Operation2 op2;
public:
unary_compose(const Operation1& x, const Operation2& y): op1(x), op2(y)
{
}
typename Operation1::result_type operator()(const typename Operation2::argument_type& x) const
{
return op1(op2(x));
}
};
template <class Operation1, class Operation2>
inline unary_compose<Operation1, Operation2> compose1(const Operation1& op1, const Operation2& op2)
{
return unary_compose<Operation1, Operation2>(op1, op2);
}
//仿函数 f(x) = x * 10
class f
{
public:
typedef double result_type;
typedef double argument_type;
result_type operator()(const argument_type& para) const //必须带const,原因是unary_compose的operator()带const
{
return para * 10;
}
};
//仿函数 g(x) = x + 4
class g
{
public:
typedef double result_type;
typedef double argument_type;
result_type operator()(const argument_type& para) const
{
return para + 4;
}
};
int main()
{
f f1;
g g1;
cout << compose1(f1,g1)(3) << endl;//求表达式f( g(3) ) = (3 + 4) * 10 = 70
return 0;
}
3.模板元编程中的高阶函数
那么,使用C++的模板机制又是否可以满足元函数作为一类值使用呢?答案是肯定的,不过解答稍稍有点复杂,并不像上述compose1的解决方法一样优雅。
view plaincopy to clipboardprint?
//使用模板机制求表达式
//可惜double类型都不能计算
#include<iostream>
#include <functional>
using namespace std;
template<int n>
struct f
{
static const int value = n + 1;
};
template <int n>
struct g
{
static const int value = n + 2;
};
template <template <int n> class op1, template <int n> class op2>
struct compose
{
template <int x>
struct return_type
{
static const int value=op1<op2<x>::value>::value;
};
};
int main()
{
typedef compose<f, g>::return_type<6> h;
cout<<h::value<<std::endl; //6+2+1=9
return 0;
}
//使用模板机制求表达式
//可惜double类型都不能计算
#include<iostream>
#include <functional>
using namespace std;
template<int n>
struct f
{
static const int value = n + 1;
};
template <int n>
struct g
{
static const int value = n + 2;
};
template <template <int n> class op1, template <int n> class op2>
struct compose
{
template <int x>
struct return_type
{
static const int value=op1<op2<x>::value>::value;
};
};
int main()
{
typedef compose<f, g>::return_type<6> h;
cout<<h::value<<std::endl; //6+2+1=9
return 0;
}
在这里,f和g是两个元函数,compose接受f和g作为参数,生成了一个可以计算f(g(x))的新函数,看起来能够得出正确的答案,但是却仍然有两个问题。
首先,在compose的模板参数中,不能直接使用类似于template <class op1, class op2>的写法,原因是C++的模板机制严格区分模板和类,我们无法把模板直接作为另一个模板的参数,唯一可行的方法是使用“作为类模板的模板参数(Class Template Template Parameter)”,这样就把f和g的参数类型限制死了。不过我们似乎仍然可以勉强接受这个限制,事实上模板机制对非类型的模板参数本来就存在着限制,现在的C++标准禁止浮点数、类对象和内部链接对象(比如字符串和全局指针)作为模板参数,所以非类型的模板参数实际上只剩下布尔型和整型可用,写成类似composeint和composebool 的两个类仍然有可行性(模板参数是无法重载的)。 其次,同样是C++的模板机制严格区分模板和类的缘故,返回值return_type是一个模板而并不是一个元函数(或者说是类)。 上述两点都隐含着一个最大共同问题,C++对“作为类模板的模板参数”作了很严格的限制,所以一旦定义了以后,其模板参数的个数不能改变。当然,在STL里面我们似乎习惯了这个限制并用重新定义函数的方式来避开这个限制。但在作为函数式编程的模板元编程里面,似乎应该要求以更优雅的方式来解决(事实上即使是常规编程下的高阶函数,也有函数库提供了更好的组合方式[5])。 现在我们仅仅用到了模板元编程的数值计算能力,还没有引入它对类型的处理能力,稍后在分析MPL时会重新讨论到这个问题,还会显示出高阶函数更大的使用障碍。幸而MPL提供了很好的解决方案,通过增加包装层和使用lambda演算的概念,高阶函数依然能用上,使得模板元编程能够符合函数式编程的要求。
Krzysztof Czarnecki曾利用模板元编程实现了一个很简单的LISP,而LISP就是典型的函数式编程语言。
总之既然C++模板能够使用函数式编程,那么也就意味着这个C++语言的子集已经是图灵完备的,因为任何计算都可以使用函数来描述,理论上模板元编程应该能完成图灵机上的一切运算。
当然,理论上的完备并不意味着实用性。尽管在C++中能够在某种程度上使用函数式编程的风格,但是从实用性和效率来说,大概没有程序员使用纯粹的函数编程方式。不过,在进行模板元编程的时候,由于语法的特殊性,却不得不使用纯粹函数式编程。因此,模板元编程与传统的C++命令式编程相差很大,并不符合大多数C++程序员的习惯,不但会带来编写上的困难,还增加了许多程序理解上的难度。那么,为什么要使用模板元编程呢?首先我们应当了解模板元编程能够做些什么,其次,模板元编程有可能用在什么地方。
(二)模板元编程的基本用途
1.数值计算
上文提及的所有关于模板元编程的例子都是在编译时的数值计算,这是模板元编程的最简单直接的使用方法。数值计算主要利用模板的特化和局部特化能力进行递归演算,模板类被视为元函数,利用类中的一个静态常量保存结果。由于C++模板对非类型的参数有限制,一般只有整型和布尔型可以参加运算。元函数的结果一般放在一个静态的常量中,但对于整型而言还有一个更好的选择,可以放置在枚举中,其唯一的优点是静态常量是左值必须在运行期占有内存地址,也就是分配了内存空间,而枚举值不能作为左值,所以不占用内存空间,显得有些微的优势。这样,阶乘计算的例子可以改写如下:
view plaincopy to clipboardprint?
#include<iostream>
using namespace std;
template <unsigned n>
struct factorial
{
enum {
value = n * factorial<n-1>::value
};
};
template<>
struct factorial<0>
{
enum {
value = 1
};
};
int main()
{
std::cout<<factorial<6>::value<<std::endl; //6! = 720
return 0;
}
#include<iostream>
using namespace std;
template <unsigned n>
struct factorial
{
enum {
value = n * factorial<n-1>::value
};
};
template<>
struct factorial<0>
{
enum {
value = 1
};
};
int main()
{
std::cout<<factorial<6>::value<<std::endl; //6! = 720
return 0;
}
无论是编译期的递归还是运行期内存的节省,对比起模板元编程在数值计算上的不足,都显得有点得不偿失。主要有以下四点:
l 运算范围仅限于整型和布尔型,用途有限。
l 递归式的编程难于实行和理解。
l C++编译器对编译期的递归都是有一定的限制的,C++标准建议进行17层递归实例化,这无疑不能满足稍复杂的程序。
l 大量消耗编译器资源的同时,降低了编译效率。
因此,用模板元编程作编译期数值计算并不在实践中经常使用,但它作为一个“中心设施”在MPL库中发挥着重要的作用[6]。
2.解开循环(Loop Unrolling)
当计算两个向量进行点乘,而维数不定时,例如:
int a[]={1,3,5,7};
int b[]={2,4,6,8};
考虑下面计算点乘的代码:
view plaincopy to clipboardprint?
template <class T>
inline T dot_product(int dim, T* a, T* b)
{
T result(0);
for (int i=0; i<dim; i++)
{
result+=a[i]*b[i];
}
return result;
}
template <class T>
inline T dot_product(int dim, T* a, T* b)
{
T result(0);
for (int i=0; i<dim; i++)
{
result+=a[i]*b[i];
}
return result;
}
这里的代码很平常,但对于性能要求极高并大量使用点乘的应用程序,也许想再节省一点开销。如果能减少循环的计数,对性能也有比较可观的提升。这样代码应该展开以直接计算:
T result=a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3];
但是我们希望泛化这个表达式,以便应用于不同维数的向量计算,这里,模板元编程正好可以发挥出它编译时计算和生成代码的能力。我们可以把代码改写成:
view plaincopy to clipboardprint?
#include<iostream>
//using namespace std;
template <int DIM, class T>
struct DotProduct
{
static T result(T* a, T* b)
{
return *a * *b + DotProduct<DIM-1, T>::result(a+1, b+1);
}
};
//局部特化,用于结束递归
template <class T>
struct DotProduct<1, T>
{
static T result(T* a, T* b)
{
return *a * *b;
}
};
//包装函数
template <int DIM, class T>
inline T doc_product(T* a, T* b)
{
return DotProduct<DIM, T>::result(a, b);
}
int main()
{
int a[]={1,3,5,7};
int b[]={2,4,6,8};
std::cout<<doc_product<4,int>(a,b)<<std::endl; //两个向量进行点乘,结果100
std::cout<<doc_product<4>(a,b)<<std::endl; //可以省略模板的第二个参数
return 0;
}
#include<iostream>
//using namespace std;
template <int DIM, class T>
struct DotProduct
{
static T result(T* a, T* b)
{
return *a * *b + DotProduct<DIM-1, T>::result(a+1, b+1);
}
};
//局部特化,用于结束递归
template <class T>
struct DotProduct<1, T>
{
static T result(T* a, T* b)
{
return *a * *b;
}
};
//包装函数
template <int DIM, class T>
inline T doc_product(T* a, T* b)
{
return DotProduct<DIM, T>::result(a, b);
}
int main()
{
int a[]={1,3,5,7};
int b[]={2,4,6,8};
std::cout<<doc_product<4,int>(a,b)<<std::endl; //两个向量进行点乘,结果100
std::cout<<doc_product<4>(a,b)<<std::endl; //可以省略模板的第二个参数
return 0;
}
这种方法是定义了一个类模板DotProduct作为元函数,通过递归调用不断展开表达式,还定义了一个局部特化的版本,使它在维数递减到1时能够终结递归。
我们还留意到一个习惯,元函数都用struct而不是用class定义,这是因为struct中的成员可见性默认为public,在编写元函数时可以省却public:这个声明。
注意包装函数的接口已经改变,利用普通方法的函数是这样使用的:
doc_product(4, a, b);
现在的写法是:
doc_product<4>(a, b);
为什么不能使用相同的接口呢?原因是模板参数必须在编译时确定,所以DIM必须是一个常量,而不可能是一个变量。所以这是对此种编程技术的一个重大限制。当然,对于这类计算而言,向量的维数一般都能在编译时确定。
Todd Veldhuizen在1995年第一次提出了这项技术,并且把这种技术运用到高性能数学计算的Blitz++库中。此外,在Blitz++库中还大量运用到一种称为“表达式模板(Expression Template)”的技术,同样是为了减少线性代数计算中的循环次数和临时对象的开销。表达式模板尽管不属于模板元编程的范畴(因为它不是依赖编译时计算进行优化的),但它与模板元编程具有异曲同工的妙用,特别在高性能数学计算中能够发挥极大的用途。Todd Veldhuizen指出,通过这一系列的优化手段,C++在科学计算上的性能已经达到甚至超过Fortran的性能。
3.类型处理
对类型的处理是模板元编程最重要和最具有现实意义的应用。由于模板可以接受类型参数,也可以通过typedef或定义内嵌类建立模板类的成员类型,再加以强大的模板特化的能力,使得类型计算几乎能有着数值计算所有的全部能力。
(1)类型分支选择
利用模板局部特化的能力,编译时的类型选择可以很容易做到:
view plaincopy to clipboardprint?
//默认值,如果C为true就把第二个类型作为返回值
template<bool C, typename T1, typename T2>
struct if //命名为if似乎不妥
{
typedef T1 type;
};
//局部特化,如果C为false就把第二个类型作为返回值
template<typename T1, typename T2>
struct if<false,T1,T2>
{
typedef T2 type;
};
//默认值,如果C为true就把第二个类型作为返回值
template<bool C, typename T1, typename T2>
struct if //命名为if似乎不妥
{
typedef T1 type;
};
//局部特化,如果C为false就把第二个类型作为返回值
template<typename T1, typename T2>
struct if<false,T1,T2>
{
typedef T2 type;
};
不过,有某些旧式编译器并不支持模板局部特化,这种情况下增加一层包装就可以巧妙地转为使用全局特化。
view plaincopy to clipboardprint?
template< bool C >
struct if_impl
{
template< typename T1, typename T2 > struct result
{
typedef T1 type;
};
};
template<>
struct if_impl<false>
{
template< typename T1, typename T2 > struct result
{
typedef T2 type;
};
};
template<bool C, typename T1, typename T2>
struct if
{
typedef typename if_impl< C >::template result<T1,T2>::type type;
};
template< bool C >
struct if_impl
{
template< typename T1, typename T2 > struct result
{
typedef T1 type;
};
};
template<>
struct if_impl<false>
{
template< typename T1, typename T2 > struct result
{
typedef T2 type;
};
};
template<bool C, typename T1, typename T2>
struct if
{
typedef typename if_impl< C >::template result<T1,T2>::type type;
};
元函数if是模板元编程中最简单但运用得最多的基础设施。
(2)类型的数据结构
把类型作为普通数据一样管理,这初看起来有点匪夷所思:普通数据可以运用struct或者array来组织,但C++并没有为类型提供一个专用的数据结构,可以利用的唯一设施是模板的参数列表。比如我们可以定义一个类型的“数组”如下:
template <class a, class b, class c, class d, class e>
struct type_array;
但是为了使它真正像数组一样使用,还需要在其中定义一系列的typedef,比如某一下标的类型的提取等,类似于:
typedef a type1;
typedef b type2;
……
在这里,数组长度也无法动态变化是一个重大的缺陷。当然,有时候数组仍然是有用的,MPL就提供了一个框架为任何自定义的类型数据结构提供支持,下文会有所提及。现在先介绍一种更自动的类型组织方法——Typelist。
(2)Typelist
上面提到过模板元编程是函数式的编程,参照其他一些函数式编程语言对数据的组织,很容易得到一些启发。比如在Scheme(一种LISP的变体)中,基本的数据结构是表,其他数据结构都必须用表的形式来表达。一个表可以这样定义:
(”A” (“B” () (“C” () () ) ) (“D” () () ) )
这个表可以表示出一个二叉搜索树:
A
B
C
D
通过简单的表的递归,就可以构造出各种数据结构。注意到C++模板的实现体也是一种类型,利用类型的递归,同样的风格也可以吸收到模板元编程中。
Typelist的定义是很简单的:
view plaincopy to clipboardprint?
template <class T, class U>
struct Typelist
{
typedef T Head;
typedef U Tail;
};
template <class T, class U>
struct Typelist
{
typedef T Head;
typedef U Tail;
};
另外我们需要定义一个特殊的标记:
struct Nulltype;
这个无定义的类不能产生对象,它的存在仅仅为了提供一个结束的标记。现在我们定义一个Typelist,并在这一节里面反复使用:
typedef Typelist<int, Typelist<float, Typelist<long, Nulltype> > > typelist;
这样的结构比起“数组”有什么优点呢?由于它的结构是递归的,我们可以很容易写一个通用的元函数提取它某一个位置的类型,我们甚至可以插入、修改和删除元素,然后返回一个新的Typelist。
(3)提取Typelist中的类型
如果需要按照位置来提取Typelist类型,可以定义这样一个元函数:
view plaincopy to clipboardprint?
//声明
template <class List, unsigned int i> struct typeat;
//局部特化,当处理到需要提取的位置时,Head就是要返回的类型
template <class Head, class Tail>
struct typeat<Typelist<Head, Tail>, 0>
{
typedef Head result;
};
//如果未到需要提取的位置,在下一个位置继续递归
template <class Head, class Tail, unsigned int i>
struct typeat< Typelist<Head, Tail>, i>
{
typedef typename typeat<Tail, i-1>::result result;
};
//声明
template <class List, unsigned int i> struct typeat;
//局部特化,当处理到需要提取的位置时,Head就是要返回的类型
template <class Head, class Tail>
struct typeat<Typelist<Head, Tail>, 0>
{
typedef Head result;
};
//如果未到需要提取的位置,在下一个位置继续递归
template <class Head, class Tail, unsigned int i>
struct typeat< Typelist<Head, Tail>, i>
{
typedef typename typeat<Tail, i-1>::result result;
};
这里使用了局部特化,对于不能支持局部特化的编译器,可以类似上面的if元函数的处理手法,适当修改这里的代码。
这个元函数按照以下方式调用:
typedef typeat<typelist, 0>::result result;
如果试图定义一个变量: result a=1.2f; 编译器会抱怨无法把一个float类型转换为result类型,因为上面定义的typelist的第一个类型是int。而把下标0改为1以后,则可以编译通过,这证明元函数可以在编译时正确选择出所需的类型。
(3)修改Typelist中的元素
实例化以后的模板已经成为一种类型,所以是不可能进行修改的,要达到修改的效果,唯一的方法是返回一种新的类型,使它带有新的Typelist。
比如,如果要在一个已有的Typelist中添加一个类型,可以定义一个这样的元函数:
view plaincopy to clipboardprint?
//声明
template <class List, class T> struct append;
//如遇到把空类型加入空类型,只需要返回 一个空类型
template <>
struct append<Nulltype, Nulltype>
{
typedef Nulltype result;
};
//如果把一个非空类型加入空类型,那么就可以直接返回
//一个只有一个元素的Typelist
template <class T> struct append <Nulltype, T>
{
typedef Typelist<T, Nulltype> result;
};
//如果把一个Typelist加入空类型,那么就可以
//直接返回这个Typelist
template <class Head, class Tail>
struct append <Nulltype, Typelist<Head, Tail> >
{
typedef Typelist<Head, Tail> result;
};
//当未到达Typelist尾部(遇到空类型)时,递归调用append元函数
template <class Head, class Tail, class T>
struct append <Typelist<Head, Tail>, T>
{
typedef Typelist<Head, typename append < Tail, T>::result> result;
};
//声明
template <class List, class T> struct append;
//如遇到把空类型加入空类型,只需要返回 一个空类型
template <>
struct append<Nulltype, Nulltype>
{
typedef Nulltype result;
};
//如果把一个非空类型加入空类型,那么就可以直接返回
//一个只有一个元素的Typelist
template <class T> struct append <Nulltype, T>
{
typedef Typelist<T, Nulltype> result;
};
//如果把一个Typelist加入空类型,那么就可以
//直接返回这个Typelist
template <class Head, class Tail>
struct append <Nulltype, Typelist<Head, Tail> >
{
typedef Typelist<Head, Tail> result;
};
//当未到达Typelist尾部(遇到空类型)时,递归调用append元函数
template <class Head, class Tail, class T>
struct append <Typelist<Head, Tail>, T>
{
typedef Typelist<Head, typename append < Tail, T>::result> result;
};
这个append元函数不仅能插入一个类型,也可以把一个Typelist添加到另一个Typelist尾部。如果这样使用的话:
typedef append<typelist, double>::result newlist;
那么就会得到一个新的newlist,里面共有4个类型。
利用很类似的方法,还可以为Typelist编写删除或者修改某个元素的元函数,这可以参阅[18]的论述。
(4)小结
用合适的数据结构组织类型,我们可以得到这样的一些好处:
l 编译时可以根据情况自动决定选择的类型。
l 由于模板中没有数据成员,它们在运行时不占用任何空间。
l 可以运用于代码的自动生成,或者实现某种设计模式。
下面分析Boost的MPL库时,还会遇到更多和更复杂的类型数据结构。
4.自动生成代码
上面曾经提到的解开循环技术其实已经是一种代码生成了,但这个独立出来的小节专门用于说明代码生成在设计模式中的应用,这些精巧的设计承接着上文提到的Typelist,也是来源于Alexandrescu的Loki库[18]。
首先,引入一个类模板GenScatterHierarchy,这个模板通过递归的定义进行多重继承,从而构造出一种复杂而散乱的体系。
view plaincopy to clipboardprint?
//模板定义
template <class TList, template <class> class Unit>
class GenScatterHierarchy;
//当未到达Typelist尾部时,继承Head和Tail各一次
template <class T1, class T2, template <class> class Unit>
class GenScatterHierarchy<Typelist<T1, T2>, Unit>: public GenScatterHierarchy<T1, Unit> , public GenScatterHierarchy<T2, Unit>
{
};
//遇到单个Unit类型时,直接继承Unit
template <class AtomicType, template <class> class Unit>
class GenScatterHierarchy : public Unit<AtomicType>
{
};
//遇到NullType时,不继承
template <template <class> class Unit>
class GenScatterHierarchy<Nulltype, Unit>
{
};
//模板定义
template <class TList, template <class> class Unit>
class GenScatterHierarchy;
//当未到达Typelist尾部时,继承Head和Tail各一次
template <class T1, class T2, template <class> class Unit>
class GenScatterHierarchy<Typelist<T1, T2>, Unit>: public GenScatterHierarchy<T1, Unit> , public GenScatterHierarchy<T2, Unit>
{
};
//遇到单个Unit类型时,直接继承Unit
template <class AtomicType, template <class> class Unit>
class GenScatterHierarchy : public Unit<AtomicType>
{
};
//遇到NullType时,不继承
template <template <class> class Unit>
class GenScatterHierarchy<Nulltype, Unit>
{
};
另外还需要定义一个Holder来持有一个类型的对象,这也是为了包装基本类型,以便它们可以被继承。
view plaincopy to clipboardprint?
template <class T> struct Holder
{
T value;
};
template <class T> struct Holder
{
T value;
};
现在可以定义一个GenScatterHierarchy的实现体了: view plaincopy to clipboardprint?
typedef GenScatterHierarchy<Typelist<int, Typelist<string, Typelist<Widget, NullType> > >, Holder>
WidgetInfo;
typedef GenScatterHierarchy<Typelist<int, Typelist<string, Typelist<Widget, NullType> > >, Holder>
WidgetInfo;
这里的Typelist嵌套定义显得有点繁琐,在Loki库里面有定义相对应的宏来简化使用,这里为了展示的方便而展开了。WidgetInfo的继承体系如下图所示:
(这个图在《Modern c++ Design》有)
为什么要使用这样复杂的一个体系呢?这种体系对于程序设计有什么实质性的帮助呢?首先,Typelist是一个可扩展的结构,不但可以定制任意长度的Typelist,在更改Typelist以后,WidgetInfo不需要作任何改变就可以自动适应并产生新的代码。其次,WidgetInfo继承了有Typelist长度个的Holder实体,它们拥有相同的接口,可以加入一个成员模板函数用统一的接口来操纵这些实体。再次,分别继承的Holder实体并不会互相干扰,WidgetInfo可以根据需要上调成它们中的任何一种。
Alexandrescu在Loki库中把这种体系发挥得淋漓尽致[18],他以这个体系(以及其它辅助方法)构造出一个非常灵活的Abstract Factory模式,避免了Abstract Factory通常耦合度较高的缺点。
Boost中的MPL库分析
MPL(Meta-Programming Library)是由David Abrahams和Aleksey Gurtovoy为方便模板元编程而开发的库,2003年被Boost吸纳为其中的一员,此后又历经一些大幅度修改,目前已经相当完善,其最新版本于2004年11月发布。MPL的出现是C++模板元编程发展中的一大创举,它提供了一个通用、高层次的编程框架,其中包括了序列(Sequence)、迭代器(Iterator)、算法(Algorithm)、元函数(Metafunction)等多种组件,具有高度的可重用性,不但提高了模板元编程的效率,而且使模板元编程的应用范围得到相当的扩展。
(一)MPL的组织架构
一个库的组织形式有时候甚至比它的功能还重要。MPL的作者聪明地借鉴了已经取得巨大成功的STL,在MPL中保留了许多STL的概念,对函数式的编程方式进行了精巧的包装,使得任何熟悉STL的程序员都可以轻易地理解MPL的使用方法。像STL一样,MPL有一个完整的概念体系,对组件作了精心的划分,组件之间相对独立,接口具有通用性,因此将组件之间的依存度和耦合性降低到最小的限度。
STL和MPL的组件概念对照如下:
STL概念
MPL对应概念
容器(Container)
序列(Sequence)
算法(Algorithm)
算法(Algorithm)
迭代器(Iterator)
迭代器(Iterator)
仿函数(Functor)
元函数类(Metafunction)
配接器(Adaptor)
有View、Inserter Iterator和相当于仿函数配接器的Binding元函数
配置器(Allocator)
无此概念
标准中没有定义
宏(Macro)
(二)MPL对其他库的依赖
MPL是一个高层次的库,它的地位和编译期执行的特殊性决定了它需要一些特殊的辅助设施,并对其他库会有所依赖。
1.Boost的Preprocessor库
Preprocessor库是一个基于宏的元编程库[7]。预处理器的作用发生在编译以前,所以它比MPL所处的地位还要高端,能够真正实现代码生成。它的典型功能是迭代或者枚举相似的代码段,减少重复而易写错的代码段。MPL中不少代码是近似的,比如在vector的原始代码中,就需要定义n个
vectori { … }
其中i从1迭代到n。为了减少重复劳动,MPL的源代码大量使用自定义和Preprocessor库的宏对重复或具有递推性的内容进行迭代。不过,这也导致源代码难以阅读。比如上面一段展开后的源代码首先是定义在vector/aux_/numbered.cpp的:
view plaincopy to clipboardprint?
// Preprocessor的宏,得到目前属于第几次迭代
#define i_ BOOST_PP_FRAME_ITERATION(1)
...
template<
//Preprocessor的宏,枚举参数列表
BOOST_PP_ENUM_PARAMS(i_, typename T)
>
// Preprocessor的宏,拼合vector和当前次数
struct BOOST_PP_CAT(vector,i_)
{ ... }
// Preprocessor的宏,得到目前属于第几次迭代
#define i_ BOOST_PP_FRAME_ITERATION(1)
...
template<
//Preprocessor的宏,枚举参数列表
BOOST_PP_ENUM_PARAMS(i_, typename T)
>
// Preprocessor的宏,拼合vector和当前次数
struct BOOST_PP_CAT(vector,i_)
{ ... }
然后为了迭代n个上面的类模板,另一个文件则需要重复include这个文件,利用Preprocessor的文件迭代能力可以这样写:
view plaincopy to clipboardprint?
// Preprocessor的宏,其中第一个参数3表示后面的参数组有3个
//元素,0和10表示迭代的范围是从0到10,最后一个参数是文件
//迭代的文件名
#define BOOST_PP_ITERATION_PARAMS_1 /
(3,(0, 10, ))
// Preprocessor的宏,要求按照上面的指定的参数进行递归
#include BOOST_PP_ITERATE()
// Preprocessor的宏,其中第一个参数3表示后面的参数组有3个
//元素,0和10表示迭代的范围是从0到10,最后一个参数是文件
//迭代的文件名
#define BOOST_PP_ITERATION_PARAMS_1 /
(3,(0, 10, ))
// Preprocessor的宏,要求按照上面的指定的参数进行递归
#include BOOST_PP_ITERATE()
尽管如此,宏还是必需的,它不但避免了重复编写递推式的代码(比如在上述的vector类模板中,n可达50之大,如果完全手写确实是浪费时间),而且还有效控制了代码的生成(比如只需要通过定义迭代次数,即可控制实际生成的类模板个数)。实际上,在使用vector(或其他组件)时,通常我们并不需要每次编译都把这些代码重新生成一次,MPL的作者已经充分考虑到编译效率的问题,所以在MPL的代码中,为每个流行的编译器都建立了一个Processed目录,里面存放着针对编译器特点展开了的代码。仅当定义了BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS时才会强制MPL重新用宏来生成代码。
MPL的作者指出,无论喜欢还是不喜欢,目前宏必须在MPL中扮演着这个不可替代的角色。
2.Boost的Type Traits库
Type Traits库[9]用于验证传递的参数或参数之间是否符合一定的条件,比如可以判定两个参数是否有继承关系、是否可转换等。
3.Boost的Static Assert库
Static Assert库[8]用于编译时断言,用法类似于C中常用的断言assert()。如果参数经编译时的静态计算为true,则代码能通过编译,不会有任何效果,反之,则会编译出错,并且在使编译信息里面包含有“STATIC_ASSERTION_FAILURE”的字样。
Static Assert的底层是接受一个bool参数的模板STATIC_ASSERTION_FAILURE,它对true定义一个有成员的特化模板,对false的情况则只有一个特化的声明(无定义)。其接口是一个宏,它产生的代码是sizeof(STATIC_ASSERTION_FAILURE< ... >),显然当参数的实际结果为false时,编译器无法判断STATIC_ASSERTION_FAILURE的长度,因为它尚未定义。
因为MPL是只在编译时生效的库,用Static Assert来调试程序是非常合适的,它往往与Type Traits库搭配使用。
4.Boost的Config库
像STL一样,由于编译器对标准支持不同,为了使程序库具有移植性,最好是针对环境进行预先的设置。对于MPL这种先锋性的库来说,编译器问题更加让库作者相当头痛。借助于对环境的侦查,可以对预先发现的问题,比如模板的局部特化能力、已知的一些编译器的bug等等,采取相应的补救措施[4]。
(三)MPL中的序列
1.MPL序列概述
序列是MPL中的数据结构的统称,是MPL中处于中心地位的组件,其地位相当于STL中的容器。MPL对序列的性质进行了细致的划分:
性质
含义 / 主要模型
前向序列Forward Sequence
begin和end元函数能够界定其头尾范围的类型序列 / MPL中所有序列
双向序列Bidirectional Sequence
迭代器属于双向迭代器的前向序列 / vector,range_c
随机访问序列Random Access Sequence
迭代器属于随机访问迭代器的双向序列 / vector,range_c
可扩展序列Extensible Sequence
允许插入和删除元素的序列 / vector,list
前可扩展序列Front Extensible Sequence
允许在前端插入和删除元素的可扩展序列 / vector,list
后可扩展序列Back Extensible Sequence
允许在后端插入和删除元素的可扩展序列 / vector,list
关联序列Associative Sequence
可以用key值来检索元素的前向序列 / set,map
可扩展关联序列Extensible Associative Sequence
允许插入和删除元素的关联序列 / set,map
整型序列包装器Integral Sequence Wrapper
存放一系列整型常量类(Integral Constant)的一种类模板 / vector_c,list_c,set_c
不定序列Variadic Sequence
可以用给定元素个数或用不指定元素个数的形式来定义的序列 / vector,list,map
部分概念在现阶段的MPL版本中其实存在着一些冗余,但这种以概念驱动的程序库却是很清晰的:每一种概念的背后都指明了它所支持的操作。
2.vector和deque
(1)概述
MPL中最简单和最常用的序列就是vector。而deque在目前版本的MPL中相当于vector。vector的实质十分类似于前面示例的类型“数组”,逻辑上是连续线性的,由于它属于不定序列,使用时既可以指定长度,以vectornn>来定义,也可以直接用vectorn>来定义。注意n不能超过宏BOOST_MPL_LIMIT_VECTOR_SIZE的定义,目前MPL的默认值是20。vector的特点是支持尾端常数时间的插入和删除操作以及中段和前端线性时间的插入和删除操作。
(2)操作
vector支持的操作无论在命名还是逻辑上基本都与STL 一致,但有一个重大区别,STL的操作函数定义在类的内部,但是限于模板元编程的特殊性,MPL的这些元函数在容器外定义。下表列出它们的用法:
begin::type
返回一个迭代器指向v的头部
end::type
返回一个迭代器指向v的尾部
size::type
返回一个v的大小
empty::type
当且仅当v为空时返回一个整型常量类,其值为true
front::type
返回v的第一个元素
back::type
返回v的最后一个元素
at::type
返回v的第n个元素
insert::type
返回一个新的vector使其定义为[begin::type, pos), x, [pos, end::type)
insert_range::type
返回一个新的vector使其定义为[begin::type, pos), [begin::type, end::type) [pos, end::type)
erase::type
返回一个新的vector使其定义为[begin::type, pos), [next::type, end::type)
erase::type
返回一个新的vector使其定义为[begin::type, pos), [last, end::type)
clear::type
返回一个空的vector
push_back::type
返回一个新的vector使其定义为[begin::type, end::type), x
pop_back::type
返回一个新的vector使其定义为[begin::type, prior< end::type >::type)
push_front::type
返回一个新的vector使其定义为[begin::type, end::type), x
pop_front::type
返回一个新的vector使其定义为[next< begin::type >::type, end::type)
(3)源代码分析
MPL的源代码有着比较复杂的脉络,主要原因是为了保持移植性,需要针对不同的编译器问题进行规避。比如vector的底层就有三个不同的版本,第一个专门针对不支持模板局部特化的编译器,第二个用于基于类型的序列,第三个是普通版本。在预处理时会根据情况确定使用哪一个版本。它们之间的差异是什么呢?vector0的实现代码中把它们放在了一起,正好可以说明其区别:
view plaincopy to clipboardprint?
template< typename Dummy = na > struct vector0;
template<> struct vector0
{
#if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES)
typedef aux::vector_tag tag;
typedef vector0 type;
typedef long_<32768> lower_bound_;
typedef lower_bound_ upper_bound_;
typedef long_<0> size;
static aux::type_wrapper item_(...);
#else
typedef aux::vector_tag<0> tag;
typedef vector0 type;
typedef void_ item0;
typedef v_iter,0> begin;
typedef v_iter,0> end;
#endif
};
template< typename Dummy = na > struct vector0;
template<> struct vector0
{
#if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES)
typedef aux::vector_tag tag;
typedef vector0 type;
typedef long_<32768> lower_bound_;
typedef lower_bound_ upper_bound_;
typedef long_<0> size;
static aux::type_wrapper item_(...);
#else
typedef aux::vector_tag<0> tag;
typedef vector0 type;
typedef void_ item0;
typedef v_iter,0> begin;
typedef v_iter,0> end;
#endif
};
定义的上半部分是基于类型的版本,下半部分则用于另外两个版本。MPL的参考手册没有说明vector的底层是实现的原理,看起来两种实现之间的差异比较大,其中最重要的差别是vector_tag的用法。vector_tag同样是一个底层的定义,作用应该是传递给各类算法,以区别不同的序列类型。tag的定义同样有两种:
view plaincopy to clipboardprint?
#if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES)
struct vector_tag;
#else
template< long N > struct vector_tag;
#endif
#if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES)
struct vector_tag;
#else
template< long N > struct vector_tag;
#endif
大概基于类型的版本可以不必实例化一个vector_tag,性能上更优越。从MPL的config配置情况来看,似乎默认只使用基于类型的序列,也就是序列会以v_item作为基类。限于篇幅,这里仅分析基于类型的vector,下文有类似情况时也做同样的处理,不一一展开了。
前面已经指出,vector是一个不定序列,这类序列可以不必指定参数的个数直接使用。C++模板支持不定个数的参数表吗?当然不是。实际上不定序列的效果是通过模板的局部特化来实现的。而能够确定个数的vectorn则是vector的基础。因此首先要看看vectorn(n不等于0时)是怎样实现的:
view plaincopy to clipboardprint?
template< typename T0 >
struct vector1: v_item< T0, vector0<> >
{
typedef vector1 type;
};
template< typename T0, typename T1 >
struct vector2: v_item< T1, vector1 >
{
typedef vector2 type;
};
template< typename T0 >
struct vector1: v_item< T0, vector0<> >
{
typedef vector1 type;
};
template< typename T0, typename T1 >
struct vector2: v_item< T1, vector1 >
{
typedef vector2 type;
};
目前MPL对vector中元素个数的限制是20个以内,所以这段代码一直递推到vector20为止。其中的v_item是一个最底层的结构,它包含的内容类似于上面vector0中的那些成员:
view plaincopy to clipboardprint?
template< typename T, typename Base, int at_front = 0 >
struct v_item: Base
{
typedef typename Base::upper_bound_ index_;
typedef typename next::type upper_bound_;
typedef typename next::type size;
typedef Base base;
//这个空的静态函数将在at元函数中有确定类型位置的作用
static aux::type_wrapper item_(index_);
//默认的继承方式是private,重新使之可见
using Base::item_;
};
template< typename T, typename Base, int at_front = 0 >
struct v_item: Base
{
typedef typename Base::upper_bound_ index_;
typedef typename next::type upper_bound_;
typedef typename next::type size;
typedef Base base;
//这个空的静态函数将在at元函数中有确定类型位置的作用
static aux::type_wrapper item_(index_);
//默认的继承方式是private,重新使之可见
using Base::item_;
};
很容易联想到前一部分提到的Typelist的Head和Tail结构,但是这里并不像Typelist一样需要一个NullType作结束标记。
至于不定序列vector的定义,则这样给出:
view plaincopy to clipboardprint?
template<
typename T0 = na, typename T1 = na, typename T2 = na,
typename T3 = na, typename T4 = na, typename T5 = na,
typename T6 = na, typename T7 = na, typename T8 = na,
typename T9 = na, typename T10 = na, typename T11 = na,
typename T12 = na, typename T13 = na, typename T14 = na ,
typename T15 = na, typename T16 = na, typename T17 = na ,
typename T18 = na, typename T19 = na
>
struct vector;
template<>
struct vector<
na, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na
>: vector0<>
{
typedef vector0< >::type type;
};
template< typename T0 >
struct vector<
T0, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na
>: vector1
{
typedef typename vector1::type type;
};
template<
typename T0 = na, typename T1 = na, typename T2 = na,
typename T3 = na, typename T4 = na, typename T5 = na,
typename T6 = na, typename T7 = na, typename T8 = na,
typename T9 = na, typename T10 = na, typename T11 = na,
typename T12 = na, typename T13 = na, typename T14 = na ,
typename T15 = na, typename T16 = na, typename T17 = na ,
typename T18 = na, typename T19 = na
>
struct vector;
template<>
struct vector<
na, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na
>: vector0<>
{
typedef vector0< >::type type;
};
template< typename T0 >
struct vector<
T0, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na
>: vector1
{
typedef typename vector1::type type;
};
显然可以看出,参数个数之所以可以不定,只是一个特化后的假象而已,针对每一个n,vector都会继承vectorn来制造这种假象。上面代码中的na是一个特殊的类,专用于标明参数未使用。
顺道一提deque,其实现也是通过继承vectorn来实现的,比如:
view plaincopy to clipboardprint?
template< typename T0 >
struct deque<
T0, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na, na
>: vector1
{
typedef typename vector1::type type;
};
template< typename T0 >
struct deque<
T0, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na, na, na
>: vector1
{
typedef typename vector1::type type;
};
所以说deque是与vector等价的一个概念。
3.list
(1)概述
MPL的list的原理类似于STL中list,其特点是支持前端常数时间的插入和删除操作以及中段和尾端线性时间的插入和删除操作。
(2)操作
list所支持的操作与vector完全一样,参见vector的操作列表。
(3)源代码分析
MPL的list的原理上十分类似于上面提到Typelist,其底层的结构l_item是这样定义的:
view plaincopy to clipboardprint?
template<typename Size, typename T, typename Next>
struct l_item
{
typedef aux::list_tag tag;
typedef l_item type;
typedef Size size;
typedef T item;
typedef Next next;
};
template<typename Size, typename T, typename Next>
struct l_item
{
typedef aux::list_tag tag;
typedef l_item type;
typedef Size size;
typedef T item;
typedef Next next;
};
另外还需要一个结束标记l_end:
view plaincopy to clipboardprint?
struct l_end
{
typedef aux::list_tag tag;
typedef l_end type;
typedef long_<0> size;
};
struct l_end
{
typedef aux::list_tag tag;
typedef l_end type;
typedef long_<0> size;
};
可以看到,l_item接受三个参数,第一个参数表示list的长度,第二个参数相当于上文Typelist实现中的Head,第三个参数相当于Tail。由于递推式的继承关系,listn的终结标记总是为l_end。
list作为不定序列,其实现方式与vector如出一辙,也是通过继承listn,比如对于一个参数的list:
view plaincopy to clipboardprint?
template<typename T0>
struct list< T0, na, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na >: list1<T0>
{
typedef typename list1<T0>::type type;
};
template<typename T0>
struct list< T0, na, na, na, na, na, na, na, na, na, na,
na, na, na, na, na, na, na, na, na >: list1<T0>
{
typedef typename list1<T0>::type type;
};
4.set
(1)概述和操作
set保证了key值在序列中没有重复,对它的插入和删除操作都只需要常数时间。
较之于vector和list,set没有pop_back,、pop_front,、push_front、push_back、insert_range、back等几个操作,但另有几个特殊的操作:
has_key<s,k>::type
如果s中包含一个类型key值为k,则返回一个整型常量类,其值为true。
count<s,k>::type
返回s中key值为k的元素的序号。
order<s,k>::type
返回s中key值为k的元素唯一的整型常量类,其值是一个无符号整数。
at<s,k>::type
at<s,k,def>::type
返回s中含有key值为k的元素。
key_type<s,x>::type
返回类型等同于x。
value_type<s,x>::type
返回类型等同于x。
erase_key<s,k>::type
返回一个新的set,当中不包括key值k。
(2)源代码分析
MPL的序列都有一个共同点,就是都从一个sequence0开始构造,并以x_item作为存放类型的基础结构。set比起上面的两种序列都来得复杂,它的构造首先从set0开始:
本文来自优快云博客,转载请标明出处:http://blog.youkuaiyun.com/hityct1/archive/2009/05/16/4191030.aspx