模版与泛型编程简介
1 函数模版
函数模版:独立于类型的函数,可作为一种方式产生函数特定类型版本。
格式:template <typename T1, …> + 正常的函数声明三要素。<>中的为模形参表,使用逗号分割。
注:模版形参表不能为空,为空为模版特化形式。
示例:
template <typename T>
int compare(const T &val1, const T &val2)
{
if (v1 < v2)
return -1;
if (v2 < v1)
reutn 1;
return 0;
}
1.1 函数模版使用
使用函数模版时,编译器会推断模版实参的类型(类模版不会推断),确定了函数模版就实例化了函数模版的一个实例。即编译器承担了为我们使用的每种类型编写函数的工作。
compare(3, 4); //编译器实例化了 int compare(const int&, const int&);用int代替T
compare(string("hancm"), string("hi")); //编译器实例化了 int compare(const string&, const string&);用string代替T
1.2 inline 函数模版
inline关键字放在template之后,例如
//正确
template<typename T> inline T min(const T&, const T&);
//错误
inline template<typename T> T min(const T&, const T&);
2 类模版
同样以关键字template开头,后接模版形参表,类模版的定义与其它类相似。
类模版的使用:STL中的容器都是使用类模版定义的,可以作为使用参考。
下面以自定义一个Queue类作为实例:
template <typename Type>
class Queue {
public:
//default constructor
Queue();
//operation
void push(const Type&);
void pop();
Type& front();
const Type& front() const;
private:
//...
};
Queue<int> qi; //Type 被替换为int型,类模版必须指定现实模版参数。
3 模版形参表
模版形参:类型形参,跟在class或typename后面,代表一个未知类型。class与typename没有区别,只是typename是标准C++组成部分。
非类型形参,跟在类型说明符之后,代表一个未知的常量表达式。
注:模版形参的名称没有任何不同,就如同函数形参一样。不同之处在于函数形参类型取决于类型说明符,而模版形参取决于是类型形参还是非类型形参。
模版形参作用域
模版形参的名字:在声明为模版形参之后到模版声明或定义的末尾。遵循常规名字屏蔽规则。
使用模版形参名字的限制:模版形参的名字不能在模版内部重用。同时意味着同一模版形参表中名字只能使用一次。
//错误
template <typename T>
class example {
typedef double T; //不允许
};
template <typename T, typename T>... //错误
模版声明
模版可以只声明而不定义,但是必须指出函数或类是模版。
//ok: 声明而不定义
template <typename T> int compare(const T&, const T&);
同一个模版声明和定义中,模版形参名字可以不相同。与名字无关只取决于类型: 类型形参 or 非类型形参。
模版中typename或class不能省略:
// error: 必须有class or typename
template <typename T, U>...
3.1模版的类型形参
类型形参:由关键字class或typenaem后接说明符构成,模版形参表中两个关键字含义相同,指出后面接的名字表示未知类型。
可以作为类型说明符在模版的任何位置,与内置类型说明符或类类型说明符使用方式相同。
注:typename可以在模版内部指定类型,例如
template <class parm, class T>
parm fcn(parm *arr, T val)
{
typename parm::size_type *p; //指出p为指向parm内部的size_type的指针。
//若没有typename, 编译器默认解释size_type为parm中static类型变量,这就变成了一个乘法表达式。
}
提示:在类型前指定typename是一个好方式。
3.2 模版的非类型形参
非类型形参:用值代替,值的类型在模版形参表中指定,例如:
//T (&arr)[N]中arr为数组的应用,N为数组的长度,是模版内部的常量。
template <class T, size_t N> void arr_init(T (&arr)[N])
{
for (int i = 0; i != N; ++i) {
arr[i] = i;
}
}
int x[3];
arr_init(x); //arr类型为int[3], T = int, N = 3; 数组传递引用时,检测数组长度。
4 编写泛型函数
编写模版代码时,对实参类型的要求尽可能少是有益的。
重要原则:模版形参是引用形参。
函数体测试只用<比较。减少类型依赖,使模版中的一组有效表达式要求降低。
5 实例化
模版本身不是类或函数。编译器用模版产生类或函数的特定类型版本。
实例化:产生模版的特定类型实例的过程。
模版在使用时进行实例化:
类模版:引用实际模版类类型时
函数模版:调用函数模版时
对函数指针进行初始化或赋值时
1.类的实例化
当编写Queue<int> iq;时,编译器创建Queue<int>的类。编译器通过用int代替模版形参的每次出现重新编写Queue模版而创建Queue<int>类。
类模版每次实例化都产生一个独立的类型,各独立化的类型之间没有任何关系,相互之间也没有特殊的访问权。
注:类模版形参是必须的,不能省略。例如:Queue不是类型,而Queue<int>是类类型。
2. 函数模版实例化
使用函数模版时,编译器一般会推断模版实参。例如:
compare(3.2, 3.4); //编译器推断模版实参为double型
5.1 函数模版实参推断
模版实参推断:从函数实参确定模版实参的类型和值的过程。
1. 多个类型形参的实参必须完全匹配
template <typename T> int compare(const T&, const T&);
compare(short, int);是错误的,实参类型不匹配。推断出的模版实参必须同一个类型(能进行转换也不行)。想要这个调用成立,必须定义两个模版形参:
template <typename T, typename U> int complate(const T&, const U&);
2. 类型形参的实参的受限转换
一般,不会转换实参以匹配已有的实例化,相反产生新的实例。
编译器会执行两种转换:
(1) const转换:接受const引用或const指针的函数,可以分别用非const对象的应用或指针来调用,不产生新实例。
例如:
template <typename T> T fun(const T&, const T&);
const string s1("hancm");
string s2("hi");
fun(s1, s2); //ok: s2的非const对象转换为const的引用
接受非引用类型的函数,形参类型和实参都忽略const,即无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化。
例如:
template <typename T> T fun(T, T);
fun(s1, s2); // ok: s1为const string, s2为非const,调用fun(string, string); const被忽略,传值方式,传递一个副本。
(2) 数组或函数到指针的转换:若模版形参不是引用类型,对数组或函数类型的实参应用常规指针转换。数组实参转换为指向第一个元素的指针,函数实参被当作指向函数类 型的指针。
例如:
template <typename T> T fun(T, T);
int a[4], b[3];
fun(a, b); //ok: calls fun(int*, int*);
template <typename T> T fun&(const T&, const T&);
fun&(a, b); //error: 数组类型不匹配,实参不转换为指针,数组引用检测长度,长度作为参数类型的一部分。
3. 非模版实参的常规转换
类型转换的限制只适用于类型为模版形参的那些实参。
普通类型定义的形参可以使用常规转换。
template <class Type> Type sum(const Type&, int op2);
sum(189, double); //ok: double转换为int,instantiates sum(int, int);
sum(33, string("hancm")); //error: string 不能转换为int
4. 模版实参推断与函数指针
使用函数模版初始化或赋值函数指针,编译器使用指针的类型实例化具有适当模版实参的模版版本。
template <typename T> int compare(const T&, constructionT&);
//pf指向实例化的 int compare(cosnt int&, cosnt int&);
int (*pf) (const int&, const int&) = compare;
若不能从函数指针类型确定模版实参,就会出错。
void fun(int (*) (const string&, const string&));
void fun(int (*) (const int&, cosnt int&));
fun(compare); //error: 不知道实例化哪个版本
5.2 函数模版的显示实参
当不能推断模版实参时,必须覆盖模版实参推断机制,显示指定模版形参的类型或值。
最常出现:函数返回类型与形参表中所用的所有类型都不同时。
1. 指定显示模版实参
考虑:
template <class T, class U> ??? sum(T, U);
sum(3, 4L); //4L更大,want U sum(T, U);
sum(3L, 4); //3L更大,want T sum(T, U);
解决办法:
sum(static_cast<int>short, int); //返回类型都一样
2. 在返回类型中使用类型形参
另一个解决方法:引入第三个模版形参,由调用者显示指定
template <class T1, class T2, class T3>
T1 sum(T2, T3);
问题:没有实参的类型用于推断T1类型
解决方法:调用者显示提供实参,类似类模版使用
//ok: T1 explicitly specified: T2, T3 inferred from argument types
long val = sum<long>(int, long);
显示模版实参与模版形参表从左到右相对应。<long>对应T1。
3. 显示实参与函数模版指针
void fun(int (*) (const string&, const string&));
void fun(int (*) (const int&, cosnt int&));
fun(compare<int>); //ok: 显示指定int版本
6 模版编译模型
编译器看到模版定义时,不立即产生代码。只有当用到模版时,如调用了函数模版或定义了类模版的对象的时候,编译器才产生特定类型的模版实例。
调用函数:编译器要看到函数声明。
定义类类型对象时:类定义必须可见,成员函数的定义不是必须存在。
结果:类定义和函数声明放在头文件中,普通函数和类成员函数定义在源文件中。这是分别编译模型。
模版不同:要进行实例化编译必须能够访问定义模版的源文件。当调用函数模版或类模版的成员函数时,编译器需要函数定义,需要那些通常放在源文件中的代码。
即,模版的相关定义也放在头文件中。这是包含编译模型,所有编译器都支持。
1. 包含编译模型
编译器必须看见所有模版的定义:
//header file Queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
template <class T>
class Queue {
...
};
#include "Queue.cc"
#endif
//implemenation file Queue.cc
//成员函数定义,静态成员的定义
问题:某些包含编译模型的编译器(特别是较老的编译器),可以产生多个实例。如果多个单独编译的源文件使用同一模版,这些编译器将为每个文件中的模版产生一个实例。通常这意味着给定模版实例化超过一次。链接时或预链接阶段,编译器会选择一个实例而丢弃其它的,如果有许多实例化同一模版的文件,编译是性能会显著下降。
解决方法:看看编译器提供什么支持以避免多余的实例化,避免同一模版的多个实例化中隐含的编译时开销。
2. 分别编译模型
编译器为我们跟踪相关的模版定义。使用关键字export使编译器记住给定的模版定义。
export关键字指明给定的定义可能需要在其他文件中产生实例化。一个程序中,一个模版只能定义为导出一次。export不必在模版声明中出现。
函数模版:在函数模版的定义中template之前包含export关键字,指明函数模版为导出的。
// 在分别编译源文件的函数模版定义中
export template <typename Type>
Type sum(Type t1, Type t2);
函数模版的声明放在头文件中,不必声明为export。
类模版:类模版声明放在头文件中,头文件的类定义体不应该使用关键字export,若用了,该头文件只能被程序的一个源文件使用。
应该在类的实现文件中使用export
//类模版头文件Queue.h
template <typename T> class Queue {};
//类模版实现文件Queue.cc
export template <class Type> Queue;
#include "Queue.h"
//Queue成员函数定义
导出类模版的成员自动生明为导出的。类模版的个别成员可以声明为导出的,此时,export不再类模版本身指定,在要导出的特定成员定义上指定。导出成员函数的定
义不必在使用成员时可见。所有非导出成员的定义必须定义在头文件中。
7 类模版成员
下面以Queue类模版为例介绍类模版成员。
注:本Queue以低级数据结构链表实现,标准库默认以deque实现。
1. 模版作用域内引用模版类型
在类模版的作用域内部,可以用类模版的非限定名。例如:
Queue (const Queue<Type> &q); //Queue的复制构造函数
编译器推断:引用类的名字时,引用的是同一个版本。
Queue的复制构造函数等价于:
Queue<Type> (const Queue<Type> &q);
注:编译器不会为类中使用的其它模版的模版形参进行这样的推断。例如:在伙伴类Queueitem中,必须指定形参类型。
Queueitem<Type> *head;
Queueitem<Type> *tail;
<Type>不能去掉。
2. 类模版成员函数
形式如下:
(1) 以关键字template开头,后接类的模版形参表。
(2) 必须指定是哪个类的成员。
(3) 类名必须包含去模版形参。
template <class T> return_val Queue<T>::member_function_name;
3. 类模版成员函数的实例化
类模版成员函数本身是函数模版,需要使用类模版的成员函数产生成员的实例化。实例化类模版成员函数时,编译器不进行模版实参推断,模版形参由调用该函数的对象确定。
注:模版形参定义的函数形参的实参允许常规类型转换。
4. 何时实例化类和成员
类模版的成员函数: 为程序所用时进行实例化。若成员函数从未使用,则不进行实例化。
定义模版类型对象: 实例化类模版。实例化用于初始化该对象的任一构造函数(此时构造函数被调用),以及构造函数调用的任意成员。
例如:
Queue<string> sq; //实例化类模版,实例化默认构造函数。
sq.push_back("hancm"); //实例化成员函数push_back。
7.1 非类型形参的模版实参
考虑标准库中的bitset,使用的就是非类型形参。
template <int n> class bitset { };
bitset<10>定义一个10为的bitset。
注:非类型模版形参: 编译时常量表达式。
7.2 类模版的友元声明
三种友元声明:
(1)1->n: 普通非模版类型或函数的友元声明,将友元关系授予明确指定的类或函数。
例如:
template <class Type>
class bar {
//授予对普通的非模版类或函数的访问权
friend class foobar;
freind void fun();
};
(2)n->n: 类模版或函数模版的友元声明,授予对友元所有实例的访问权。
例如:
template <calss Type>
class bar {
//授予任意类模版或函数模版访问权,使用<class T>指明模版形参可以和<class Type>不同。
template <class T> friend class foobar;
template <class T> friend void templ_fun(const T&);
};
(3)1->1: 只授予对类模版或函数模版的特定实例的访问权的友元声明。
例如:
template <class T> class foo;
template <class T> void temp_fun(const T&);
template <calss Type>
class bar {
//授予特定的实例访问权
//前面必须有模版的声明,否则编译器会认为该友元是一个普通非模版或非模版函数。
friend class foo<char*>;
friend void temp_fun<char*>(char* const&);
更常见的: 声明相同实参的友元。只授予相同类型的模版访问权。
friend class foo<Type>;
friend void temp_fun<Type>(const Type&);
};
7.3 成员模版
成员模版:类(模版或非模版)的成员,该成员为类模版或函数模版。
例如:
template <class Type>
class Queue {
public:
//成员模版:本身就是一个模版,只不过是一个类的成员,它的形参类型与所属类的形参类型无关。
template <class Iter> void assign(Iter, Iter);
};
在类外定义成员模版:
template <class T> //所属类的模版形参
template <class Iter> //成员模版本身的模版形参
void Queue<T>::assign(Iter beg, Iter end)
{
}
注:成员模版遵循常规访问控制
实例化:类模版形参由调用函数的对象类型确定
成员模版的模版形参由模版实参推断出。
7.4 类模版的static成员
例如:
template <class T>
class foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};
1.使用
//ok
foo<int>::ctr;
foo<int>::count();
//error
foo::ctr; //foo是类模版不是类,只有foo<int>这种类才可以
2.定义static成员
template <class T>
size_t foo<T>::ctr = 0; //与普通类的static类似
8 Queue的完整实现
//Queue.h头文件
#ifndef __Queue_H__ #define __Queue_H__ #include <iostream> //类模版声明,定义在Queue.cc文件夹中 template <typename Type> class Queue; //重载输出操作符,不能为类的成员函数 template <typename Type> std::ostream& operator<<(std::ostream&, const Queue<Type>&); //伙伴类模版,用于实现底层数据结构,标准库使用deque实现 template <typename Type> class Queueitem { //需要访问Queueitem的构造函数,next指针 friend class Queue<Type>; //需要访问item和next friend std::ostream& operator<< <Type>(std::ostream&, const Queue<Type>&); //private section Queueitem(const Type &t): item(t), next(0) { }; Type item; Queueitem *next; }; template <class Type> class Queue { //Needs access to head //需要访问Queue的head
//使用与类模版相同类型的实参,前面必须有operator<<的声明 //否则编译器会认为这是一个非模版类型 friend std::ostream& operator<< <Type> (std::ostream&, const Queue<Type>&); public: //默认构造函数 Queue(): head(0), tail(0) { } //使用一对迭代器的构造函数,属于成员模版 template <class It> Queue(It beg, It end): head(0), tail(0) { copy_elems(beg, end); } //复制构造函数 Queue(const Queue &q): head(0), tail(0) { copy_elems(q); } //赋值操作符 Queue& operator=(const Queue&); //析构函数 ~Queue() { destroy(); }; //使用一对迭代器的赋值成员模版,标准queue没有这个函数 template <class Iter> void assign(Iter, Iter); //对列尾部添加一个元素 void push(const Type&); //从对头删除元素 void pop(); //取对头元素 Type &front() { return head->item; }; const Type &front() const { return head->item; }; //队列是否空 bool empty() const { return head == 0;}; private: Queueitem<Type> *head; Queueitem<Type> *tail; //utility functions used by copy constructor, assignment, and destructor //供析构函数调用 void destroy(); //供复制构造和复制操作符调用 void copy_elems(const Queue&); //供template <class It> Queue(It beg, It end)调用 template <class Iter> void copy_elems(Iter, Iter); }; //Include Compilation Model: include member function definitions as we;; //包含编译,大多数编译器不支持export分别编译 #include "Queue.cc" #endif
//Queue.cc实现文件
/*编写要考虑以下内容 * 1.一般先编写基本功能函数, * 基本功能函数有增加,删除,访问, 是否空, * 构造函数和赋值操作符要调用复制元素的函数,析构函数要调用的析构元素的函数 * 对应Queue中的push(), pop(), front()(inline), empty()(inline), copy_elems(), destroy() * 其他函数调用它们 * 2.注意哪些函数可以是inline的,可以的尽量满足, 并且放在头文件中。 * Queue中inline有front(), empty(),构造函数和复制构造函数,析构函数 * 3.编写函数时从需求条件最少的开始, * 以功能型函数形式进行编写,例如push()只需要inline的empty(),pop()都不需要可以先编写 */ #include <iostream> using std::ostream; template <typename Type> void Queue<Type>::push(const Type &val) { Queueitem<Type> *p = new Queueitem<Type>(val); if (empty()) { head = tail = p; } else { tail->next = p; tail = p; } } template <typename Type> void Queue<Type>::pop() { Queueitem<Type> *p = head; head = head->next; delete p; } template <typename Type> void Queue<Type>::destroy() { while (!empty()) { pop(); } } template <typename Type> void Queue<Type>::copy_elems(const Queue &orig) { for (Queueitem<Type> *p = orig.head; p; p = p->next) { push(p->item); } } template <typename Type> Queue<Type>& Queue<Type>::operator=(const Queue &rhs) { if (this != &rhs) { destroy(); copy_elems(rhs); } return *this; } /* * 注意成员模版的编写方式 * 好处:可以应用隐式类型转换, * 即只要*Iter可以转换为Type,就可以使用assign,不需要*Iter一定和Type相同 */ template <typename Type> //类模版的模型形参 template <typename Iter> //成员模版的模版形参 void Queue<Type>::assign(Iter beg, Iter end) { destroy(); while (beg != end) { push(*beg); ++beg; } } template <typename Type> ostream& operator<<(ostream &os, const Queue<Type> &q) { os << "< "; Queueitem<Type> *p; for (p = q.head; p; p = p->next) { os << p->item << " "; } os << ">"; }
//Queue_main.cc : 使用Queue的主函数
1 #include "Queue.h" 2 #include <iostream> 3 #include <vector> 4 5 using std::vector; 6 using std::cout; 7 using std::endl; 8 9 int main(void) 10 { 11 Queue<int> iq; 12 13 cout << "在Queue中添加0..9十个元素:" << endl; 14 for (int i = 0; i != 10; ++i) { 15 iq.push(i); 16 } 17 cout << "实例化oprator<<() 输出元素:" << iq << endl << endl; 18 19 cout << "实例化front()(用于输出元素), pop(), empty().pop后iq变为空:" << endl; 20 for (int i = 0; i != 10; ++i) { 21 cout << iq.front() << " "; 22 iq.pop(); 23 } 24 cout << endl << "iq 是否为空" << endl; 25 cout << "iq.empty() == " << iq.empty(); 26 cout << endl << endl; 27 28 cout << "重新初始化iq为0..4:" << endl; 29 for (int i = 0; i != 5; ++i) { 30 iq.push(i); 31 } 32 cout << "输出iq:" << iq << endl << endl; 33 34 Queue<int> iq2; 35 vector<int> ivec; 36 cout << "初始化vector 0..12:" << endl; 37 for (int i = 0; i != 13; ++i) { 38 ivec.push_back(i); 39 cout << ivec[i] << " "; 40 } 41 cout << endl << "实例化assign(), 用vector初始化Queue:" << endl; 42 iq2.assign(ivec.begin(), ivec.end()); 43 cout << "After assign(vector); iq2:" 44 << iq2 << endl << endl; 45 46 Queue<int> iq3 = iq2; 47 cout << "实例化复制构造函数Queue(const Queue&), Queue<int> iq3 = iq2; iq3: " << endl; 48 cout << iq3 << endl << endl; 49 50 iq = iq2; 51 cout << "实例化复制操作符operator=(const Queue&),After iq = iq2:" << endl; 52 cout << iq << endl << endl; 53 54 return 0; 55 }
使用G++编译:
g++ Queue_main.cc //使用GCC编译
./a.out //运行
输出:
注:辅助函数可以改写为list,deque,基本框架不变。
9 模版特化
略