您的位置:首页 > 编程语言 > C语言/C++

C++学习笔记(七) 模板与泛型编程

2012-07-24 10:36 148 查看

模板简介

模板是C++一个非常重要的特性,它是C++泛型编程的基础。某些对C++持极度偏见的人甚至说模板是C++对这个世界的唯一贡献(当然,我是不赞同的),可见模板在C++中的重要性,而整个STL都是基于模板的,可见其应用之广泛。
C++引入模板的一个重要原因是算法的重用,比如下面一个例子:

bool mless_than(const int& v1, const int& v2) {
return v1 < v2;
}
程序很简单,就是比较第一个参数是否小于第二个参数而已,这个算法在我们的程序可以说是非常常见,这个是int版本的,如果我们还需要一个string版本的,一个double版本的,甚至是一个自定义类版本的怎么办呢?如果没有模板,我们就不得不为其定义多个实现,即使他们的代码都是一样的,如果算法很长,例如一个排序算法,写这么多个版本将是一件冗长而乏味的事情,况且我们是无法预见未来需要为什么样的类型定制算法的,写算法库的人也无法知道使用者可能会定义什么样的类型。说了模板的必要性,现在来看一个它的实现吧,还是刚才那个函数:

template<typename T>
bool mless_than(const T& v1, const T& v2) {
return v1 < v2;
}


使用的方式也很简单,直接调用就是了,聪明的编译器会自动为我们推导出模板的参数类型:

bool result = mless_than(2.8, 4.1);//double version


在某些情况下,编译无法从调用参数推到出所有的模板类型,或是我们传入的参数类型不是我们希望用于实例化函数的模板参数类型时,我们也可以手动的指定模板参数类型,调用方式如下:

bool result = mless_than<int> (2.8, 4.1);//double version


模板除了用在函数上,还可以用在类中,例如:

template<typename T>
class A{
//...other definition
private:
T v;
//...other definition
}


如果经常使用STL的话,使用方式我们应该已经习惯了:

A<int> a;


这个是用一个int版本的类A去定义了一个对象a.

需要注意的是,无论是函数模板,还是类模板,它们都不是真正的函数或是类,它只是告诉编译器该如何生成真正的函数实例或是类实例(这里并非对象哦),也就是说A并不是类,A<int>才是类。

关于模板的简介就说到这吧,有了这个初步概念后,我们来看看class和typename的区别。

class和typename

在上面的模板定义中,我都是使用typename关键字来定义模板参数的,以前学习过或了解过模板的同学可能还会发现另一个关键字class被重用在这里用于定义模板参数,那么它们究竟有上面区别呢?答案是在定义模板参数这里,它们是没有区别的,由于typename是后引进的关键字,所以,在一些比较旧的代码中,class关键可能会更加常见些。
我说了它们在这里是没有区别的,但既然我单独列了一个小标题,说明这中间还是有点内容滴。typename在模板中还有一些别的作用。在谈这个之前,我们先来了解一个概念:nested dependent type names(嵌套依赖类型名)。考虑下面的定义:
template<typename T>
void test(const T& c) {
T::key_type *ptr;
//other implementation
}
其中,c::key_type *ptr代表什么意思呢?如果你对STL比较熟悉的话,你可能会说,嘿,这是利用c::key_type定义了一个指针ptr,在map和set里面都有这个类型,它表示的是其封装的键的类型。可是,只是可是,如果有哪个傻瓜自定义了一个类,并且他恰好在这个类内部又定义了一个名叫key_type的静态变量,那么这句话就不再是变量定义了,而是一个乘法运算。那么编译器是如何看待这条语句的呢?首先还是说一下潜逃依赖类型名这个概念,像这种定义在类内部的类型,而外部类的类型又是依赖于模板参数的,就是所谓的潜逃依赖类型名,默认情况下,编译器是把它当做是变量名,而非类型名的,也就是说默认下,编译器会将其当做乘法运算的。如果我们想让编译器把它当做是类型名,可以在其前面加上typename关键字进行修饰。


typename c::key_type *ptr;


对于像这种潜逃依赖类型名,我们在使用的时候都应该加上typename关键字进行修饰。但是,还是有例外的,例如在基类列表里面,如果有用到嵌套依赖类型,则不用typename关键字,因为这里出现的标示符只可能是类型名。

Nontype 模板

前面所谈及的都是类型模板,实际上C++的模板机制还支持非类型模板。为了更直观的说明它是什么,我们先来看一个它在STL中一个实际运用的例子,那就是位图类:
bitset<32> b;


如上的定义方式是定义了一个大小为32的位图类,它利用非类型参数去指定其大小。我们来看一个非类型模板的简单实现:
template<int SIZE>
class A{
//some definition
int data[SIZE];
}


可以看出,非类型模板参数在这里的作用是指定A<SIZE>内部维护的一个数组变量的大小。在我们进行参数传递时,非类型参数有时候会先得非常有用,例如:
template <class T, size_t N> void array_init(T (&parm)
)
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}


该函数的作用是对任意大小的数组进行初始化,这里巧妙的利用非类型参数指定了所传递的数组引用的大小。

模板的特化与偏特化

模板的作用是为任意的类型提供统一的实现方式,或是算法逻辑,或是数据结构。但有时候,对于某些类型,统一的实现方式并不能满足我们的要求,考虑最开始的那个模板函数,如果我们传递的是常量字符串,编译器会为我们实例化出这样的函数代码:
bool mless_than(const char* const& v1, const char* const& v2) {
return v1 < v2;
}


编译运行都木有问题,可关键是,它比较的两个字符串的地址,而非字符串本身,这显然不是我们想要的。在这种情况下,我们就需要利用模板的特化功能为它定制一个专门的版本用于处理C_style的字符串,特化的实现方式如下:
template<>
bool mless_than(const char* const& v1, const char* const& v2) {
return strcmp(v1, v2) < 0;
}


当我们传递C_style的字符串时,编译器就会调用我们特化的这个版本,而不是利用模板去给我们呢生成。
模板的特化也可以用在类模板上,在特化的类中,我们不必遵循原先模板的定义方式。类模板的特化定义方式和函数模板大致类似,不过,它必须在类名后面显示的指定特化的模板参数类型。
除了模板特化,C++还允许我们对类模板进行偏特化(函数模板不行),就是只针对部分模板参数进行特化,例如:
template<typename T, typename V>
class A{
//other definition
T d1;
V d2;
};

template<typename T>
class A<T, int>{
//other definition
T d1;
int d2;
};


在偏特化中,我们将模板参数V特化为int,需要注意的是,偏特化后的模板仍然是一个模板类,而非实际的类。

模板元编程

所谓模板元编程实际上并没有引入新的C++特性,它是C++非类型模板与模板特化的一个非常奇妙的用法。它能够将一些运行时计算的任务放到编译期来完成,从而提高运行效率。例如,我们希望以常量的阶乘作为一个静态数组的大小,就可以利用模板元编程了:
template<unsigned N>
class Factorial {
public:
unsigned VALUE = N*Factorial<N-1>::VALUE;
};

template<>
class Factorial<0> {
public:
unsigned VALUE = 1;
};


上面的模板类Factorial用于计算阶乘,它巧妙的利用递归在编译器就可以计算出我们所需要的阶乘值,值得注意的是,这里的递归出口是一个偏特化模板,很神奇吧。

模板的编译机制

谈完模板的一些基本特性与使用方式,我们最后来看一下模板编译机制。我们知道,模板只是提供被编译器供编译器生成实例的一种方式。而模板是按需进行实例化的,也就是说,如果我们按照我们的习惯将类模板的定义放在头文件里,而将其成员函数的实现放在源文件中,在对定义源文件进行编译时是不会生成任何代码的,这样在其他使用文件中因为只包含器类的定义而不包含实现,在连接过程中就会发生找不到类的错误。所以我们的模板代码必须全部放在头文件中,如果你为了方便管理,也可以讲实现放在源文件中,再通过头文件进行反包含。有些童鞋可能会担心重定义的问题,实际上把模板放在头文件中,这种C++标准中是运行的,编译器对它做了特殊处理,所以不会有普特类的定义放在头文件中的担心。
实际上在C++的标准中还有一个关键字export可以解决这个问题,不过由于目前还没多少编译器支持,所以这里也就不谈了。
模板的编译机制是按需的,不仅对于类型按需,对于那些并没有使用到的成员函数,编译器也不会对它进行编译的,这点很重要。前段时间,在网上翻博客时就碰见一位仁兄讲述的一个例子,他自己做了一个简单的模板库,在测试的时候没有问题,但在实际的使用过程中却碰到了大麻烦。因为他在测试时,对于有些类型并没有用到所有的成员函数,而这些成员函数编译器就没有对它进行编译,这才使问题在使用期才暴露出来。所以,了解模板的编译机制,对于我们以后开发自己的模板库有很重要的作用。

(不得不吐槽:以后再也不相信CSDN的草稿功能了,昨晚上一晚上的编辑全浪费了!)

参考文献:

《C++ Primer》Stanley B.Lippman Barbara
E.Moo
《C++语言程序设计》 郑莉 董渊 何江舟

                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: