刚着手写这篇文章的时候,实在不知道该从何处下笔,关于泛型技术有太多的东西可写。
这里我不打算详细讲解关于泛型,关于template的语法细则,单单这一部分完全可以写一本书出来,而且也确实已经有很好的资源可以供我们参考----《C++ Templates - The Complete Guide》。
除了语法,还有什么?Template?STL?是的,对于C++来说,泛型程序设计本身就是基于模块的程序设计,并且已经广泛应用于STL当中。
然而泛型并不仅限于此。
在这个系列文章里,我想展示的是一些或许有些极端的、在实际编程中很少用到的泛型idioms(惯用技法)。然而正如侯捷先生所说:由来技术的推演,并不只是问一句“它有用吗”或“它现在有用吗”可以论断价值的。牛顿发表万有引力公式,并不知道三百年后人们用来计算轨道、登陆月球。个人的成长千万别被群体的惯性绊住脚步。我们曾经鄙夷的别人的“无谓”前卫,可能只因我们故步自封,陷自己于一成不变的行为模式;或因此我们只看到自家井口的天空。深度+广度,古典+前卫,理论+应用,实验室+工厂,才能构筑一个不断进步的世界。
做为泛型的惯用技法,最常用(在STL中随处可见),最简单的恐怕就要数trait了。好了,下面我们先引入trait的定义:
Traits class: A class used in place of template parameters. As a class, it aggregates useful types and constants; as a template, it provides an avenue for that "extra level of indirection" that solves all software problems.
有些抽象吧,下面我们通过代码来说明问题。
假设我们有一组数据存储在数组当中,并且我们有一个指向第一个元素的指针和指向最后一个元素的指针,现在我们要对数组中的所有元素求和,一般的方法就不写了,下面我们直接看它的泛型做法:
- template <typename T>
- inline
- T accum (T const* beg, T const* end)
- {
- T total = T();
- while (beg != end) {
- total += *beg;
- ++beg;
- }
- return total;
- }
这里有一个小问题:如何为正确的类型生成一个0值,以便开始我们的求和计算。对于int和float等内建类型而言,T()都可以符合要求,对于其他的类型,我们稍后讲解。
下面是使用了上文模板的一段代码:
- int num[]={1,2,3,4,5};
-
- std::cout << "the average value of the integer values is "
- << accum(&num[0], &num[5]) / 5
- << '/n';
-
- char name[] = "templates";
- int length = sizeof(name)-1;
-
- std::cout << "the average value of the characters in /""
- << name << "/" is "
- << accum(&name[0], &name[length]) / length
- << '/n';
程序的输出结果是:
the average value of the integer values is 3
the average value of the characters in "templates" is -5
等一下,这里有个问题不是吗?对于前面部分,程序还算运行正常,但是后半部分由于我们以char类型实例化模板,数组中数据的和超过了它的范围,产生了越界。显然这不是我们想要的结果。
问题已经知道了,但是我们要如何解决呢?一个比较好的办法是对accum()所调用的每个T类型都创建一个关联,所关联的类型就是用来存储累加和的类型。这些关联可以看作是类型T的一个特征,因此,我们也把这个存储累加和的类型称为T的trait。
- template<typename T>
- class AccumulationTraits;
- template<>
- class AccumulationTraits<char> {
- public:
- typedef int AccT;
- };
- template<>
- class AccumulationTraits<short> {
- public:
- typedef int AccT;
- };
- template<>
- class AccumulationTraits<int> {
- public:
- typedef long AccT;
- };
- template<>
- class AccumulationTraits<unsigned int> {
- public:
- typedef unsigned long AccT;
- };
- template<>
- class AccumulationTraits<float> {
- public:
- typedef double AccT;
- };
上文中,AccumulationTraits被称为一个trait模板,因为它含有它的参数类型的一个trait。通过特化,我们为特定的类型指定其累加和类型。现在我们修改前面的模板代码如下:
- template <typename T>
- inline
- typename AccumulationTraits<T>::AccT accum (T const* beg,
- T const* end)
- {
-
- typedef typename AccumulationTraits<T>::AccT AccT;
- AccT total = AccT();
- while (beg != end) {
- total += *beg;
- ++beg;
- }
- return total;
- }
于是,现在例子程序中的输出就完全符合我们的期望了。
对了,我们还留有一个问题不是吗?那就是T()如何解决呢?方法跟上面的累加和trait类似,不过这里我们提供的是一个内联成员函数,来避开C++关于类的静态成员变量的某些限制。
- template<typename T>
- class AccumulationTraits;
- template<>
- class AccumulationTraits<char> {
- public:
- typedef int AccT;
- static AccT zero() {
- return 0;
- }
- };
- template<>
- class AccumulationTraits<short> {
- public:
- typedef int AccT;
- static AccT zero() {
- return 0;
- }
- };
- template<>
- class AccumulationTraits<int> {
- public:
- typedef long AccT;
- static AccT zero() {
- return 0;
- }
- };
- template<>
- class AccumulationTraits<unsigned int> {
- public:
- typedef unsigned long AccT;
- static AccT zero() {
- return 0;
- }
- };
- template<>
- class AccumulationTraits<float> {
- public:
- typedef double AccT;
- static AccT zero() {
- return 0;
- }
- };
之后,在accum()中把T()替换为AccT total = AccumulationTraits<T>::zero(); ,到这里大功告成。
在上面的例子中,我们把accum跟AccumulationTraits绑定在一起了,这样当我们改变主意不打算使用AccumulationTraits的时候多少有点不方便。不过没关系,我们可以通过缺省模板参数来搞定它。由于函数模板不支持缺省模板实参,那么,我们只好改用类模板了。
- template <typename T,
- typename AT = AccumulationTraits<T> >
- class Accum {
- public:
- static typename AT::AccT accum (T const* beg, T const* end) {
- typename AT::AccT total = AT::zero();
- while (beg != end) {
- total += *beg;
- ++beg;
- }
- return total;
- }
- };
最后,我们可以引入一个辅助函数,来简化上面基于类的接口:
- template <typename Traits, typename T>
- inline
- typename Traits::AccT accum (T const* beg, T const* end)
- {
- return Accum<T, Traits>::accum(beg, end);
- }
在STL中,trait又被应用在哪里呢?没错,就是迭代器。最常用到的迭代器类型有五种,分别是:value type, difference type, pointer, reference, iterator catagoly。traits这一“特性萃取机”会忠实地将它们原汗原味地榨取出来:
- template <class I>
- struct iterator_traits{
- typedef typename I::iterator_category iterator_category;
- typedef typename I::value_type value_type;
- typedef typename I::difference_type difference_type;
- typedef typename I::pointer pointer;
- typedef typename I::reference reference;
- };
然而,多了这一个间接层,会带来什么好处呢?好处是我们可以拥有特化版本,比如针对原生指针的特化,等等。从而我们使用迭代器时便有了统一的接口。
最后我们来总结一下trait:trait表述了模板参数的一些自然的额外属性(当然它可以不需要通过模板参数传递),它通常都具有很自然的缺省值,可以紧密依赖于一个或多个主参数,并通过trait模板来实现。
关于trait技术我就总结到这里了,如果你意犹未尽的话,可以继续阅读《C++ Templates》,其中包含了更错综复杂的演变过程。