当一般函数与函数模板同名时,编译器不会报错(函数模板只有在类型T确定之后才会生成对应的函数体,否则它仅仅只是模板。)。当出现函数调用时,优先匹配一般函数。
double Max(double a,double b) //一般函数
{
returna>b?a:b;
}
template
T Max(T a,T b)
{
returna>b?a:b;
}
注意:在上例中,当只有函数模板时,函数调用时如果两个参数类型不一致时(如:Max(12.5, 5)),编译出错。如果在函数调用时指定模板类型(如:Max
如:
Max(12.5,8);//没有一般函数,编译出错;有一般函数,调用一般函数
Max
注意:如果在函数调用时指定模板类型(如:Max
template<typenameT>
T* Max(T&a, T& b)
{
return a<b ? &b : &a;
}
在上例中:当引用作为函数参数,传递数组时,数组长度必须相同否则编译出错。如果是值传递的话则不会出现问题。因为值传递传递的是指针,而引用则是引用了整个数组,相当于参数是数组引用,如果数组长度不一致的话,编译器将a和b解释成两种类型,而模板中是相同类型,编译出错。
函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计。它的最大特点是把函数使用的数据类型作为参数。
函数模板的声明形式为:
template
返回类型函数名(参数表)
{
函数体
}
其中,template是定义模板函数的关键字;template后面的尖括号不能省略;typename(或class)是声明数据类型参数标识符的关键字,用以说明它后面的标识符是数据类型标识符。这样,在之后定义的这个函数中,凡希望根据实参数据类型来确定数据类型的变量,都可以用数据类型参数标识符来说明,从而使这个变量可以适应不同的数据类型。例如:
template
T fuc(T x, int y)
{
Tx;
//……
}
如果主调函数中有以下语句:
double d;
int a;
fuc(d,a);
则系统将用实参d的数据类型double去代替函数模板中的T生成函数:
double fuc(double x,int y)
{
double x;
//……
}
函数模板只是声明了一个函数的描述即模板,不是一个可以直接执行的函数,只有根据实际情况用实参的数据类型代替类型参数标识符之后,才能产生真正的函数。
关键字typename也可以使用关键字class,这时数据类型参数标识符就可以使用所有的C++数据类型。
函数模板的数据类型参数标识符实际上是一个类型形参,在使用函数模板时,要将这个形参实例化为确定的数据类型。将类型形参实例化的参数称为模板实参,用模板实参实例化的函数称为模板函数。模板函数的生成就是将函数模板的类型形参实例化的过程。例如:
使用中应注意的几个问题:
⑴函数模板允许使用多个类型参数,但在template定义部分的每个形参前必须有关键字typename或class,即:
template
返回类型函数名(参数表)
{
函数体
}
⑵在template语句与函数模板定义语句<返回类型>之间不允许有别的语句。如下面的声明是错误的:
template
int I;
T min(T x,T y)
{
函数体
}
⑶模板函数类似于重载函数,但两者有很大区别:函数重载时,每个函数体内可以执行不同的动作,但同一个函数模板实例化后的模板函数都必须执行相同的动作
函数模板中的模板形参可实例化为各种类型,但当实例化模板形参的各模板实参之间不完全一致时,就可能发生错误,如:
template
T min(T &x, T &y)
{ return (x
void func(int i, char j)
{
min(i, i);
min(j, j);
min(i, j);
min(j, i);
}
例子中的后两个调用是错误的,出现错误的原因是,在调用时,编译器按最先遇到的实参的类型隐含地生成一个模板函数,并用它对所有模板函数进行一致性检查,例如对语句
min(i, j);
先遇到的实参i是整型的,编译器就将模板形参解释为整型,此后出现的模板实参j不能解释为整型而产生错误,此时没有隐含的类型转换功能。解决此种异常的方法有两种:
⑴采用强制类型转换,如将语句min(i, j);改写为min(i,int( j));
⑵用非模板函数重载函数模板
方法有两种:
①借用函数模板的函数体
此时只声明非模板函数的原型,它的函数体借用函数模板的函数体。如改写上面的例子如下:
template
T min(T &x, T &y)
{ return (x
int min(int,int);
void func(int i, char j)
{
min(i, i);
min(j, j);
min(i, j);
min(j, i);
}
执行该程序就不会出错了,因为重载函数支持数据间的隐式类型转换。
此方法在VC 6.0、Visual studio 2005——Visualstudio 2013环境下,四个调用编译都不通过。前两种必须显示使用模板(如min
(i, i);),如果是隐式的话,编译器先推测到函数声明,但函数声明没有函数体,编译报错,而显示使用模板则会根据参数实例化生成函数体。移植性太差,不建议使用。
②重新定义函数体
就像一般的重载函数一样,重新定义一个完整的非模板函数,它所带的参数可以随意。C++中,函数模板与同名的非模板函数重载时,应遵循下列调用原则:
1: 寻找一个参数完全匹配的函数,若找到就调用它。若参数完全匹配的函数多于一个,则这个调用是一个错误的调用。
2: 寻找一个函数模板,若找到就将其实例化生成一个匹配的模板函数并调用它。
3: 若上面两条都失败,则使用函数重载的方法,通过类型转换产生参数匹配,若找到就调用它。
4:若上面三条都失败,还没有找都匹配的函数,则这个调用是一个错误的调用。
如果定义有一般类,定义同名模板类时,产生编译错误
例如:
class Point //定义一般类
{
……
};
template
class Point //定义模板类
{
…… //编译出错
};
如果类模板中的静态成员变量有特定类型的初始化,那么凡是类型与该特定类型相同的实例化的模板类对象,都将使用该默认值初始化静态成员变量。
例如:
/*template
class Rect
{
public:
staticT1 nCount;//静态变量
};
//万能初始化
template
T1 Rect
//特定类型初始化
template<>
int Rect
Rect
Rect
cout << rc1.nCount << endl; //15 通过模板类对象访问
cout << Rect
cout << rc2.nCount << endl; //1 通过模板类对象访问
cout << Rect
实际上可以说没有区别。按 C++ 标准来说,template
typename T::innerClass myInnerObject;
这里的 typename 告诉编译器,T::innerClass 是一个类型名,程序要声明一个 T::innerClass 类的对象,而不是声明 T 的静态成员,而 typename 如果换成 class 则语法错误
例如:
template<typenameT>
class MyTestClass
{
public:
//这里的typename是用于通知编译器,其后的MyType是模板T定义的内部类型,从这个代码示例
//中可以看出,不管是在函数参数、返回值还是定义成员变量,都要遵守这一语法规则。
MyTestClass(typename T::MyType p) : _p(p), M(typenameT::innerClass())
{
}
typename T::MyType GetData() const
{
return _p;
}
protected:
typename T::MyType _p;
typename T::innerClass M;
};
class ParamClass
{
public:
typedef char* MyType; //内部类型
class innerClass //内嵌类
{
public:
innerClass()
{
}
~innerClass()
{
}
};
};
#if 1
int main()
{
MyTestClass<ParamClass> t("Hello"); //T为ParamClass
printf("The data is %s.\n", t.GetData()); //输出The data is Hello.
return 0;
}
#endif
注意:在高版本的编译器(如visual studio 2013)中,可以在类类型前使用class,但是在非类类型前必须使用typename。
如:定义模板template
class T::MyType _p;//Error
typename T::MyType _p;//OK
class T::innerClass M;//OK
编译器根据函数模板生成具体的函数,或根据类模板生成具体的类的过程叫做模板的实例化。
vector
在程序中,只会为int类型实例化向量模板(vector),不会为bool、float等实例化模板,因为它们从未被调用过。这种自动按需进行的模板实例化,叫做隐含实例化。
对一个类模板进行实例化时,只有它的成员的声明会被实例化。而对类模板成员函数定义的实例化也是按需进行的,只有一个函数会被使用时,它才会被实例化。类的静态数据成员的定义的实例化,同样是按需进行的。
隐式实例化是按需自动进行的,而显示实例化是由专门的代码指定的。
显示实例化的一般语法形式:
template 实例化目标的声明;
例如:
template<typenameT> //函数模板
void Print(T&value, const char *delimiter)
{
cout << value << delimiter << endl;
}
template<classT> //类模板
class Point
{
public:
Point(T x, T y) :X(x), Y(y)
{
}
const T& GetX() const
{
return X;
}
const T& GetY() const
{
return Y;
}
private:
T X;
T Y;
};
template void Print<int>(int& value,const char *delimiter);//显示实例化函数模板
template Point<int>; //显示实例化类模板
注意:对类模板进行显示实例化的同时,类模板的成员函数和静态数据成员的定义也会被实例化。
有了显示实例化的工具,就可以在一定程度上将会被其他源文件使用的函数模板、类模板成员函数或数据成员的定义放在源文件中。办法是在该源文件中对模板进行显示实例化,这样尽管该源文件没有对模板实例的需求,但目标文件中仍然会生成相关的代码,从而允许被其他源文件所生成的目标代码引用。然而,这样做也是有前提的,就是在编译模板的时候,能够穷举模板的哪些实例会被其他函数实例化。
C++允许程序员为一个函数模板或类模板在某些特定参数下提供特殊的定义,这就是模板的特化。“template<>”是进行特化时所需的固定格式。
注意:被特化的函数模板、类模板中被特化的成员函数和静态数据成员,以及被特化的类模板的成员函数和静态数据成员,像非模板的成员函数和静态数据成员那样,无论是否被使用,相关的目标代码都会被生成,因此它们的定义应当放在源文件中,而非头文件中。
C++允许在一部分模板参数固定而另一部分模板参数可变的情况下规定类模板的特殊实现,这种行为叫做类模板的偏特化。偏特化只是针对类模板,函数模板不支持偏特化。
偏特化与特化的不同之处在于,特化将所有的模板参数都固定下来了,因而对类模板、函数模板特化的结果不再是模板,而是具有普通类、普通函数的性质,但偏特化由于仍然保留了一部分未定的参数,使得偏特化的结果仍然是模板。
一个类可以定义多个偏特化版本,实例化时,最特殊的那一个总会被选中。
C++不允许将函数模板偏特化,但函数模板像普通函数那样允许被重载。
当模板函数或模板类作为其他类(可以是模板类)的友元的时候,必须将模板定义template
template<classT>
class Point
{
public:
Point(T x, T y);
Point(const Point& ref);
virtual ~Point();
bool operator <(constPoint& ref);
template<classT>//不能省略
friend void show(const T& ref);//友元模板函数
template<classT>//不能省略
friend class Test;//友元模板类
private:
T X;
T Y;
};
1:什么是泛型编程
所谓泛型编程就是以独立于任何特定数据类型的方式编写代码。使用泛型程序时,我们需要提供具体程序实例所操作的类型和值。
标准库中的容器,迭代器和算法都是泛型编程的例子。
2:模板和泛型的关系?
模板是泛型编程的基础,使用模板时无需考虑模板的定义,模板是创建类的或函数的蓝图或公式。
模板的参数应该是类型。需要通过指定某种类型来实例化一个模板。但是,在这里,会看见一些值作为模板参数的情况。这里着重强调类型和值的区别。正因为是值作为模板参数,这一章才显得很特别。
定义一个Stack模板,要求使用一个固定大小的数组作为元素的容器,并且数组的大小可以由模板的使用者自己定义。那么,对于模板的设计者,就应该提供一个接口使得使用者可以定义数组的大小。这就需要用到非类型的类模板参数。下面的代码能很好的解释这个问题:
#include
#include <string>
#include
#include
template
class Stack{
private:
T elems[MAXSIZE];
int numElems;
public:
Stack();
void push(T const&);
void pop();
T top() const;
bool isEmpty() const{
return numElems == 0;
}
bool isFull() const{
return numElems == MAXSIZE;
}
};
template
Stack
{
// 不作任何事,仅为了初始化numElems。
}
template
void Stack
{
if(numElems == MAXSIZE)
{
throw std::out_of_range("Stack<>::push()==>stack is full.");
}
elems[numElems] = elem;
++numElems;
}
template
void Stack
{
if(numElems <= 0)
{
throw std::out_of_range("Stack<>::pop: empty stack");
}
--numElems;
}
template
T Stack
{
if(isEmpty())
{
throw std::out_of_range("Stack<>::pop: empty stack");
}
// 返回最后一个元素。
return elems[numElems - 1];
}
int main()
{
try
{
Stack<int, 20> int20Stack;
Stack<int, 40> int40Stack;
Stack
int20Stack.push(7);
std::cout<<"int20Stack.top() : "<
int20Stack.pop();
stringStack.push("HelloWorld!");
std::cout<<"stringStack.top() : "<
stringStack.pop();
stringStack.pop();
}
catch(std::exception const& ex)
{
std::cerr<<"Exception: "<
return EXIT_FAILURE;
}
return 0;
}
上面的代码揭示了非类型的类模板参数的定义和使用方法。需要注意的有:
强调一下,非类型的模板参数和类型模板参数一样,也是标识一个模板的因素之一。
函数模板的非类型参数主要用来为函数提供一个运算常量。关于非类型的函数模板参数,书中有下面的例子:
//函数模板定义
template
T addValue(T const& x)
{
return x + VAL;
}
//其他代码
//函数模板的使用
std::transform(source.begin(), source.end(), dest.begin(),
(int(*) (int const&))addValue<int, 5>);
上面的代码中定义了一个函数模板,目的是对传入的参数加上一个指定的int型的5。这样的函数被普遍的使用在对一组数据进行同一处理的场合。例如,12行。这里需要注意的是:一std::transform函数本身就是一个模板函数,它的最后一个参数可以传递一个函数指针。因此,(int(*) (int const&))addValue
注意:不要函数指针{(int(*) (int const&))}类型转换也能正常工作。如:
//函数模板的使用
std::transform(source.begin(), source.end(), dest.begin(),addValue<int, 5>);
关于非类型模板参数的限制目前记住它可以是常整型(包括枚举类型)和指向外部连接对象的指针就可以了。由于历史原因,浮点型不能作为非类型模板的参数;而指针和字符串作为非类型模板的参数是有条件的。我想这与变量的作用范围和生命周期有关吧。
前言
常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天都能遇到的模板类吧,如STL, ATL, WTL, 以及Boost的模板类,都能体会到这样的滋味:接口简单,操作复杂。
本文对象是那些熟悉模板但还没有很多编写模板经验的程序员。本文只涉及模板类,未涉及模板函数。但论述的原则对于二者是一样的。
问题的产生
通过下例来说明问题。例如在array.h文件中有模板类array:
// array.h
template
class array
{
T data_[SIZE];
array (const array& other);
const array&operator = (const array& other);
public:
array(){};
T& operator[](int i) {return data_[i];}
const T& get_elem (int i) const {return data_[i];}
void set_elem(int i, const T& value) {data_[i] = value;}
operator T*() {return data_;}
};
然后在main.cpp文件中的主函数中使用上述模板:
// main.cpp
#include "array.h"
int main(void)
{
array
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}
这时编译和运行都是正常的。程序先创建一个含有50个整数的数组,然后设置数组的第一个元素值为2,再读取第一个元素值,最后将指针指向数组起点。
但如果用传统编程方式来编写会发生什么事呢?我们来看看:
将array.h文件分裂成为array.h和array.cpp二个文件(main.cpp保持不变)
// array.h
template
class array
{
T data_[SIZE];
array (const array& other);
const array& operator = (const array& other);
public:
array(){};
T& operator[](int i);
const T& get_elem (int i) const;
void set_elem(int i, const T& value);
operator T*();
};
// array.cpp
#include "array.h"
template
{
return data_[i];
}
template
{
return data_[i];
}
template
{
data_[i] = value;
}
template
{
return data_;
}
编译时会出现3个错误。问题出来了:
为什么错误都出现在第一个地方?
为什么只有3个链接出错?array.cpp中有4个成员函数。
要回答上面的问题,就要深入了解模板的实例化过程。
模板实例化
程序员在使用模板类时最常犯的错误是将模板类视为某种数据类型。所谓类型参量化(parameterized types)这样的术语导致了这种误解。模板当然不是数据类型,模板就是模板,恰如其名:
编译器使用模板,通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)。从模板类创建得到的类型称之为特例(specialization)。模板实例化取决于编译器能够找到可用代码来创建特例(称之为实例化要素,point of instantiation)。要创建特例,编译器不但要看到模板的声明,还要看到模板的定义。模板实例化过程是迟钝的,即只能用函数的定义来实现实例化。
再回头看上面的例子,可以知道array是一个模板,array
现在,编译array.cpp时会发生什么问题呢?编译器可以解析模板定义并检查语法,但不能生成成员函数的代码。它无法生成代码,因为要生成代码,需要知道模板参数,即需要一个类型,而不是模板本身。这样,链接程序在main.cpp 或 array.cpp中都找不到array
至此,我们回答了第一个问题。但还有第二个问题,在array.cpp中有4个成员函数,链接器为什么只报了3个错误?回答是:实例化的惰性导致这种现象。在main.cpp中还没有用上operator[],编译器还没有实例化它的定义。
解决方法
认识了问题,就能够解决问题:
①在使用类模板的源文件中包含类模板的声明文件和定义文件
②用另外的文件来显式地实例化类型,这样链接器就能看到该类型。
③使用export关键字。(此方法,不建议使用,移植性差)
前二种方法通常称为包含模式,第三种方法则称为分离模式。
第一种方法意味着在使用模板的转换文件中不但要包含模板声明文件,还要包含模板定义文件。在上例中,就是第一个示例,在array.h中定义所有的成员函数。或者在main.cpp文件中也包含进array.cpp文件。这样编译器就能看到模板的声明和定义,并由此生成array
第二种方法,通过显式的模板实例化得到类型。最好将所有的显式实例化过程安放在另外的文件中。在本例中,可以创建一个新文件templateinstantiations.cpp:
// templateinstantiations.cpp
#include "array.cpp"
template [class]array
array
第三种方法是在模板定义中使用export关键字,剩下的事就让编译器去自行处理了。当我在Stroustrup的书中读到export时,感到非常兴奋。但很快就发现VC 6.0不支持它,后来又发现根本没有编译器能够支持这个关键字(第一个支持它的编译器要在2002年底才问世)。自那以后,我阅读了不少关于export的文章,了解到它几乎不能解决用包含模式能够解决的问题。欲知更多的export关键字,建议读读Herb Sutter撰写的文章。
结论
要开发模板库,就要知道模板类不是所谓的"原始类型",要用其它的编程思路。本文目的不是要吓唬那些想进行模板编程的程序员。恰恰相反,是要提醒他们避免犯下开始模板编程时都会出现的错误。
模板元编程是指在模板实例化的同时利用编译器完成一些计算工作。
程序实例1:计算n的阶乘
template<int N>
struct factorial //主模板
{
//枚举元素的值在编译时确定,计算在编译期间,而不是在运行时
//枚举元素可以当作静态常量,可以通过“类名::枚举元素名”直接访问枚举元素
enum{ result = N*factorial
};
template<>
struct factorial<1> //完全特化
{
enum{ result = 1 };
};
cout<< factorial<5>::result << endl;//通过类名直接访问匿名枚举变量元素 输出5!= 120
程序实例2:实现pow的功能
template<unsigned N>
//内联函数,在编译时进行替换
inline double power(doublevalue)//主函数模板
{
//递归调用
return value*power
}
template<>
inline double power<1>(doublevalue)//函数模板的特化
{
return value;
}
cout<< power<4>(3) << endl; //输出3^4 = 81
程序实例3:实现pow功能,为模板引入类型参数T,比程序2更通用
template<unsigned N>
class Pow //主模板
{
public:
//类模板中嵌套函数模板
template<classT>
//定义为静态成员函数便于用类名直接调用,不用同具体的对象关联
static T power(T value)
{
//递归调用
return value*Pow
}
enum A{ result = N*factorial
};
template<>
class Pow<1> //偏特化类模板
{
public:
template<classT>
static T power(T value)
{
return value;
}
enum A{ result = 1 };
};
//Pow<4>::power(2)的写法不是很方便,设定一个辅助函数
//在函数中执行Pow<4>::power(2)语句,并将结果返回
//这样就可以通过power<4>(2)来求2^4的值
template<unsigned N,class T>
inline T power(T value)
{
return Pow
}
cout<< Pow<3>::result << endl; //通过类名直接访问枚举变量元素 输出3! = 6
cout<< Pow<4>::power(2)<< endl;//通过模板类直接调用,写法复杂 输出2^4 = 16
cout<< power<5>(2) << endl; //通过辅助函数调用,写法简便 输出2^5 = 32
模板编程是泛型编程的一部分,都属于泛型程序设计范畴,若想进一步了解泛型程序设计,请观看博主“泛型程序设计与STL”这篇博文,相信你会在那里找到你想知道的东西,希望对你的学习有一个推动力,哈哈……