C++学习笔记(七) 模板与泛型编程
模板简介
模板是C++一个非常重要的特性,它是C++泛型编程的基础。某些对C++持极度偏见的人甚至说模板是C++对这个世界的
唯一贡献(当然,我是不赞同的),可见模板在C++中的重要性,而整个STL都是基于模板的,可见其应用之广泛。
C++引入模板的一个重要原因是算法的重用,比如下面一个例子:
[cpp]
bool mless_than(const int& v1, const int& v2) {
return v1 < v2;
}
程序很简单,就是比较第一个参数是否小于第二个参数而已,这个算法在我们的程序可以说是非常常见,这个是int
版本的,如果我们还需要一个string版本的,一个double版本的,甚至是一个自定义类版本的怎么办呢?如果没有模
板,我们就不得不为其定义多个实现,即使他们的代码都是一样的,如果算法很长,例如一个排序算法,写这么多个
版本将是一件冗长而乏味的事情,况且我们是无法预见未来需要为什么样的类型定制算法的,写算法库的人也无法知
道使用者可能会定义什么样的类型。说了模板的必要性,现在来看一个它的实现吧,还是刚才那个函数:
[cpp]
template<typename T>
bool mless_than(const T& v1, const T& v2) {
return v1 < v2;
}
使用的方式也很简单,直接调用就是了,聪明的编译器会自动为我们推导出模板的参数类型:
[cpp] view plaincopy
bool result = mless_than(2.8, 4.1);//double version
在某些情况下,编译无法从调用参数推到出所有的模板类型,或是我们传入的参数类型不是我们希望用于实例化函数
的模板参数类型时,我们也可以手动的指定模板参数类型,调用方式如下:
[cpp]
bool result = mless_than<int> (2.8, 4.1);//double version
模板除了用在函数上,还可以用在类中,例如:
[cpp]
template<typename T>
class A{
//...other definition
private:
T v;
//...other definition
}
如果经常使用STL的话,使用方式我们应该已经习惯了:
[cpp]
A<int> a;
这个是用一个int版本的类A去定义了一个对象a.
需要注意的是,无论是函数模板,还是类模板,它们都不是真正的函数或是类,它只是告诉编译器该如何生成真正的
函数实例或是类实例(这里并非对象哦),也就是说A并不是类,A<int>才是类。
关于模板的简介就说到这吧,有了这个初步概念后,我们来看看class和typename的区别。
class和typename
在上面的模板定义中,我都是使用typename关键字来定义模板参数的,以前学习过或了解过模板的同学可能还会发现
另一个关键字class被重用在这里用于定义模板参数,那么它们究竟有上面区别呢?答案是在定义模板参数这里,它
们是没有区别的,由于typename是后引进的关键字,所以,在一些比较旧的代码中,class关键可能会更加常见些。
我说了它们在这里是没有区别的,但既然我单独列了一个小标题,说明这中间还是有点内容滴。typename在模板中还
有一些别的作用。在谈这个之前,我们先来了解一个概念:nested dependent type names(嵌套依赖类型名)。考
虑下面的定义:
[cpp]
template<typename T>
void test(const T& c) {
c::key_type *ptr;
//other implementation
}
其中,c::key_type *ptr代表什么意思呢?如果你对STL比较熟悉的话,你可能会说,嘿,这是利用c::key_type定义
了一个指针ptr,在map和set里面都有这个类型,它表示的是其封装的键的类型。可是,只是可是,如果有哪个傻瓜
自定义了一个类,并且他恰好在这个类内部又定义了一个名叫key_type的静态变量,那么这句话就不再是变量定义了
,而是一个乘法运算。那么编译器是如何看待这条语句的呢?首先还是说一下潜逃依赖类型名这个概念,像这种定义
在类内部的类型,而外部类的类型又是依赖于模板参数的,就是所谓的潜逃依赖类型名,默认情况下,编译器是把它
当做是变量名,而非类型名的,也就是说默认下,编译器会将其当做乘法运算的。如果我们想让编译器把它当做是类
型名,可以在其前面加上typename关键字进行修饰。
[cpp]
typename c::key_type *ptr;
对于像这种潜逃依赖类型名,我们在使用的时候都应该加上typename关键字进行修饰。但是,还是有例外的,例如在
基类列表里面,如果有用到嵌套依赖类型,则不用typename关键字,因为这里出现的标示符只可能是类型名。
Nontype 模板
前面所谈及的都是类型模板,实际上C++的模板机制还支持非类型模板。为了更直观的说明它是什么,我们先来看一
个它在STL中一个实际运用的例子,那就是位图类:
[cpp]
bitset<32> b;
如上的定义方式是定义了一个大小为32的位图类,它利用非类型参数去指定其大小。我们来看一个非类型模板的简单
实现:
[cpp]
template<int SIZE>
class A{
//some definition
int data[SIZE];
}
可以看出,非类型模板参数在这里的作用是指定A<SIZE>内部维护的一个数组变量的大小。在我们进行参数传递时,
非类型参数有时候会先得非常有用,例如:
[cpp]
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}
该函数的作用是对任意大小的数组进行初始化,这里巧妙的利用非类型参数指定了所传递的数组引用的大小。
模板的特化与偏特化
模板的作用是为任意的类型提供统一的实现方式,或是算法逻辑,或是数据结构。但有时候,对于某些类型,统一的
实现方式并不能满足我们的要求,考虑最开始的那个模板函数,如果我们传递的是常量字符串,编译器会为我们实例
化出这样的函数代码:
[cpp]
bool mless_than(const char* const& v1, const char* const& v2) {
return v1 < v2;
}
编译运行都木有问题,可关键是,它比较的两个字符串的地址,而非字符串本身,这显然不是我们想要的。在这种情
况下,我们就需要利用模板的特化功能为它定制一个专门的版本用于处理C_style的字符串,特化的实现方式如下:
[cpp]
template<>
bool mless_than(const char* const& v1, const char* const& v2) {
return strcmp(v1, v2) < 0;
}
当我们传递C_style的字符串时,编译器就会调用我们特化的这个版本,而不是利用模板去给我们呢生成。
模板的特化也可以用在类模板上,在特化的类中,我们不必遵循原先模板的定义方式。类模板的特化定义方式和函数
模板大致类似,不过,它必须在类名后面显示的指定特化的模板参数类型。
除了模板特化,C++还允许我们对类模板进行偏特化(函数模板不行),就是只针对部分模板参数进行特化,例如:
[cpp]
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++非类型模板与模板特化的一个非常奇妙的用法。它能够将一
些运行时计算的任务放到编译期来完成,从而提高运行效率。例如,我们希望以常量的阶乘作为一个静态数组的大小
,就可以利用模板元编程了:
[cpp]
template<unsigned N>
class Factorial {
public:
unsigned VALUE = N*factorial<N-1>::VALUE;
};
template<>
class Factorial<0> {
public:
unsigned VALUE = 1;
};
上面的模板类Factorial用于计算阶乘,它巧妙的利用递归在编译器就可以计算出我们所需要的阶乘值,值得注意的
是,这里的递归出口是一个偏特化模板,很神奇吧。
模板的编译机制
谈完模板的一些基本特性与使用方式,我们最后来看一下模板编译机制。我们知道,模板只是提供被编译器供编译器
生成实例的一种方式。而模板是按需进行实例化的,也就是说,如果我们按照我们的习惯将类模板的定义放在头文件
里,而
补充:软件开发 , C++ ,