C++中的模板(template)

1. 简介算法

模板是C++在90年代引进的一个新概念,本来是为了对容器类(container classes)的支持[1],可是如今模板产生的效果已经远非当初所能想象。编程

简单的讲,模板就是一种参数化(parameterized)的类或函数,也就是类的形态(成员、方法、布局等)或者函数的形态(参数、返回值等)能够被参数改变。更加神奇的是这里所说的参数,不光是咱们传统函数中所说的数值形式的参数,还能够是一种类型(实际上稍微有一些了解的人,更多的会注意到使用类型做为参数,而每每忽略使用数值做为参数的状况)。设计模式

举个经常使用的例子来解释也许模板就从你脑壳里的一个模糊的概念变成活生生的代码了:安全

在C语言中,若是咱们要比较两个数的大小,经常会定义两个宏:函数

#define min(a,b) ((a)>(b)?(b):(a))
#define max(a,b) ((a)>(b)?(a):(b))布局

这样你就能够在代码中:this

return min(10, 4);.net

或者:设计

return min(5.3, 18.6);对象

这两个宏很是好用,可是在C++中,它们并不像在C中那样受欢迎。宏由于没有类型检查以及天生的不安全(例如若是代码写为min(a++, b--);则显然结果非你所愿),在C++中被inline函数替代。可是随着你将min/max改成函数,你马上就会发现这个函数的局限性 —— 它不能处理你指定的类型之外的其它类型。例如你的min()声明为:

int min(int a, int b);

则它显然不能处理float类型的参数,可是原来的宏却能够很好的工做!你随后大概会想到函数重载,经过重载不一样类型的min()函数,你仍然可使大部分代码正常工做。实际上,C++对于这类能够抽象的算法,提供了更好的办法,就是模板:

template <class T> const T & min(const T & t1, const T & t2) {
    return t1>t2?t2:t1;
}

这是一个模板函数的例子。在有了模板以后,你就又自由了,能够像原来在C语言中使用你的min宏同样来使用这个模板,例如:

return min(10,4);

也能够:

return min(5.3, 18.6)

你发现了么?你得到了一个类型安全的、而又能够支持任意类型的min函数,它是否比min宏好呢?

固然上面这个例子只涉及了模板的一个方面,模板的做用远不仅是用来替代宏。实际上,模板是泛化编程(Generic Programming)的基础。所谓的泛化编程,就是对抽象的算法的编程,泛化是指能够普遍的适用于不一样的数据类型。例如咱们上面提到的min算法。

2. 语法

你千万不要觉得我真的要讲模板的语法,那太难为我了,我只是要说一下如何声明一个模板,如何定义一个模板以及常见的语法方面的问题。

template<> 是模板的标志,在<>中,是模板的参数部分。参数能够是类型,也能够是数值。例如:

template<class T, T t>
class Temp{
public:
    ...
    void print() { cout << t << endl; }
private:
    T t_;
};

在这个声明中,第一个参数是一个类型,第二个参数是一个数值。这里的数值,必须是一个常量。例如针对上面的声明:

Temp<int, 10> temp; // 合法

int i = 10;
Temp<int, i> temp; // 不合法

const int j = 10;
Temp<int, j> temp; // 合法

参数也能够有默认值:

template<class T, class C=char> ...

默认值的规则与函数的默认值同样,若是一个参数有默认值,则其后的每一个参数都必须有默认值。

参数的名字在整个模板的做用域内有效,类型参数能够做为做用域内变量的类型(例如上例中的T t_),数值型参数能够参与计算,就象使用一个普一般数同样(例如上例中的cout << t << endl)。

模板有个值得注意的地方,就是它的声明方式。之前我一直认为模板的方法所有都是隐含为inline的,即便你没有将其声明为inline并将函数体放到了类声明之外。这是模板的声明方式给个人错觉,实际上并不是如此。咱们先来看看它的声明,一个做为接口出如今头文件中的模板类,其全部方法也都必须与类声明出如今一块儿。用通俗的话来讲,就是模板类的函数体也必须出如今头文件中(固然若是这个模板只被一个C++程序文件使用,它固然也能够放在.cc中,但一样要求类声明与函数体必须出如今一块儿)。这种要求与inline的要求同样,所以我一度认为它们隐含都是inline的。可是在Thinking In C++[2]中,明确的提到了模板的non-inline function,就让我不得不改变本身的想法了。看来正确的理解应该是:与普通类同样,声明为inline的,或者虽然没有声明为inline可是函数体在类声明中的才是inline函数。

澄清了inline的问题候,咱们再回头来看那些咱们写的包含了模板类的丑陋的头文件,因为上面提到的语法要求,头文件中除了类接口以外,处处充斥着实现代码,对用户来讲,十分的不可读。为了能像传统头文件同样,让用户尽可能只看到接口,而不用看到实现方法,通常会将全部的方法实现部分,放在一个后缀为.i或者.inl的文件中,而后在模板类的头文件中包含这个.i或者.inl文件。例如:

// start of temp.h
template<class T> class Temp{
public:
    void print();
};

 #include "temp.inl"
// end of temp.h

// start of temp.inl
template<class T> void Temp<T>::print() {
    ...
}
// end of temp.inl

经过这样的变通,即知足了语法的要求,也让头文件更加易读。模板函数也是同样。

普通的类中,也能够有模板方法,例如:

class A{
public:
    template<class T> void print(const T& t) { ...}
    void dummy();
};

对于模板方法的要求与模板类的方法同样,也须要与类声明出如今一块儿。而这个类的其它方法,例如dummy(),则没有这样的要求。

3. 使用技巧

知道了上面所说的简单语法后,基本上就能够写出本身的模板了。可是在使用的时候仍是有些技巧。

3.1 语法检查

对模板的语法检查有一部分被延迟到使用时刻(类被定义[3],或者函数被调用),而不是像普通的类或者函数在被编译器读到的时候就会进行语法检查。所以,若是一个模板没有被使用,则即便它包含了语法的错误,也会被编译器忽略,这是语法检查问题的第一个方面,这不常遇到,由于你写了一个模板就是为了使用它的,通常不会放在那里不用。与语法检查相关的另外一个问题是你能够在模板中作一些假设。例如:

template<class T> class Temp{
public:
    Temp(const T & t): t_(t) {}
    void print() { t.print();}
private:
    T t_;
};

在这个模板中,我假设了T这个类型是一个类,而且有一个print()方法(t.print())。咱们在简介中的min模板中其实也做了一样的假设,即假设T重载了'>'操做符。

由于语法检查被延迟,编译器看到这个模板的时候,并不去关心T这个类型是否有print()方法,这些假设在模板被使用的时候才被编译器检查。只要定义中给出的类型知足假设,就能够经过编译。

之因此说“有一部分”语法检查被延迟,是由于有些基本的语法仍是被编译器当即检查的。只有那些与模板参数相关的检查才会被推迟。若是你没有写class结束后的分号,编译器不会放过你的。

3.2 继承

模板类能够与普通的类同样有基类,也一样能够有派生类。它的基类和派生类既能够是模板类,也能够不是模板类。全部与继承相关的特色模板类也都具有。但仍然有一些值得注意的地方。

假设有以下类关系:

template<class T> class A{ ... };
 |
+-- A<int> aint;
 |
+-- A<double> adouble;

则aint和adouble并不是A的派生类,甚至能够说根本不存在A这个类,只有A<int>和A<doubl>这两个类。这两个类没有共同的基类,所以不能经过类A来实现多态。若是但愿对这两个类实现多态,正确的类层次应该是:

class Abase {...};

template<class T> class A: public Abase {...};
 |
+-- A<int> aint;
 |
+-- A<double> adouble;

也就是说,在模板类之上增长一个抽象的基类,注意,这个抽象基类是一个普通类,而非模板。

再来看下面的类关系:

template<int i> class A{...};
 |
+-- A<10> a10;
 |
+-- A<5> a5;

在这个状况下,模板参数是一个数值,而不是一个类型。尽管如此,a10和a5仍然没有共同基类。这与用类型做模板参数是同样的。

3.3 静态成员

与上面例子相似:

template<class T> class A{ static char a_; };
 |
+-- A<int> aint1, aint2;
 |
+-- A<double> adouble1, adouble2;

这里模板A中增长了一个静态成员,那么要注意的是,对于aint1和adouble1,它们并无一个共同的静态成员。而aint1与aint2有一个共同的静态成员(对adouble1和adouble2也同样)。

这个问题实际上与继承里面讲到的问题是一回事,关键要认识到aint与adouble分别是两个不一样类的实例,而不是一个类的两个实例。认识到这一点后,不少相似问题均可以想通了。

3.4 模板类的运用

模板与类继承均可以让代码重用,都是对具体问题的抽象过程。可是它们抽象的侧重点不一样,模板侧重于对于算法的抽象,也就是说若是你在解决一个问题的时候,须要固定的step1 step2...,那么大概就能够抽象为模板。而若是一个问题域中有不少相同的操做,可是这些操做并不能组成一个固定的序列,大概就能够用类继承来解决问题。以个人水平还不足以在这么高的层次来清楚的解释它们的不一样,这段话仅供参考吧。

模板类的运用方式,更多状况是直接使用,而不是做为基类。例如人们在使用STL提供的模板时,一般直接使用,而不须要从模板库中提供的模板再派生本身的类。这不是绝对的,我以为这也是模板与类继承之间的以点儿区别,模板虽然也是抽象的东西,可是它每每不须要经过派生来具体化。

在设计模式[4]中,提到了一个模板方法模式,这个模式的核心就是对算法的抽象,也就是对固定操做序列的抽象。虽然不必定要用C++的模板来实现,可是它反映的思想是与C++模板一致的。

4. 参考资料

[1] 深度C++对象模型,Stanley B.Lippman, 侯捷译

[2] Thinking In C++ 2nd Edition Volumn 1, Bruce Eckel

[3] 定义-- 英文为definition,意思是"Make this variable here",参见[2] p93

[4] Design Patterns - Elements of Reusable Object-Oriented Software GOF

相关文章
相关标签/搜索