C++独孤九剑第九式——以静制动(模板编程探索)

在C++编程中,抽象层次最高的应该算是模板了吧。模板是泛型编程的基础。所谓泛型编程,就是以独立于任何特定类型的方式编写代码。把相关的类型也抽象出来,使我们的代码可以适应所需要任何类型!哇塞,想想都觉得好高端,有木有(*^-^*)

模板可以看成是创建特定类或者函数的蓝图或公式。模板实例化函数或者类并不是先实例化一个列表,然后再里面挑你需要的;而是在你的确需要的时候才为你实例化一个将要用到的实例。模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。

记得刚接触模板编程的时候感觉它的有些语法晦涩难懂,而且当报错的时候总是让人感觉捉襟见肘。不过,说真的,习惯就好了。一回生,两回熟,三回就成好朋友嘛。

一、模板实例化

例如,我们定义了如下的模板函数:

//其中T只是一个形式参数,可以用任何其它合法的标识符代替
template <typename T>
T sum(T t1, T t2)
{
	return t1 + t2;
}

当如下调用该模板时:

int i = sum(1,2);//1
double d = sum(1.1,2.2);//2

当编译器执行到语句1时,为我们实例化int类型的函数模板实例;执行到语句2时,为我们实例化double类型的函数模板实例。

注意,如果这样调用将会报错:

short s = 5;
int i = sum(1,s);

这是因为模板从调用处推断实参时是不会进行参数类型的隐式转换的。上面调用sum函数时,1是int型的实参,而s是short型的实参,两个实参不同,模板将不能确定如何产生对应的函数实例。

如果定义如下类模板:

template <typename T>
class A{};

使用该模板:

A<int> ai;//3
A<double> ad;//4

编译器执行到语句3和4时,会分别为我们实例化类模板的int和double类型的类实例。

函数模板和类模板实例化的区别是:函数模板的实例化是由调用处的实际参数类型来确定模板实参的;而类模板的实例化是显示指定我们需要实例化的类型。

二、非类型模板形参

模板的形参既可以使类型形参,也可以使非类型的形参。我们可以定义一个包含非类型形参的函数模板,如下:

template <typename T, size_t N>
void array_sum(T array[N])
{
	T sum = 0;
	for(int i = 0;i < N; ++i)
		sum += array[i];//计算数组元素的和
	cout<<sum<<endl;
}

然后如下调用之:

int array_int[3] = {1,2,3};
array_sum<int,3>(array_int);

其实模板参数也是可以有默认值的,设定默认值的规则和一般函数中设置形参默认值的规则相同。即从第一个默认值开始,后面就必须都是默认值(因为显示模板实参是从左至右与对应模板形参相匹配的)。

例如上面的数组求值函数也可以这样定义:

template <typename T, size_t N = 10>
void array_sum(T array[N]){/*具体操作*/}

这样我们就为其中的一个非类型形参设定了默认值。类型形参的默认值设置会在后面讨论。

三、类模板的static成员

类模板可以像任意其他类一样声明static成员。如以下代码;

template <typename T>
class A
{
public:
	static int size;
	static void name(){cout<<"class A"<<endl;}
};

类外定义成员变量:

template<typename T>//说明下面是模板相关的定义
int A<T>::size = 0;//以A<T>来指定,表示成员属于类模板A

需要注意的是,每个特定类型的实例化表示截然不同的类型,所以给定实例化的所有对象都共享一个static成员。既有,A<int>类型的任意对象共享同一static成员size,A<double>类型的对象共享另一个不同的size成员。

如下:

A<int> a1,a2;
A<double> a3;
a1.size = 5;
cout<<a2.size<<endl;//5
cout<<a3.size<<endl;//6

语句5处输出为5,因为a1、a2都是A<int>类型,共享static成员;而语句6处a3的static成员依然是0,因为a3与a1、a2是不同的类型,它们拥有各自不同的static成员。

四、模板特化

模板特化是指定义中一个或多个模板形参的实际类型或实际值是指定的。

例如我们定义如下的模板函数:

template<class T>
int compare(const T &t1,const T &t2)
{
	if(t1 < t2) return -1;
	if(t2 < t1) return 1;
	return 0;
}

当T类型绑定到指针时其实是有问题的,因为对指针做‘<’其实没有实际的意义(比较两个指针的大小),我们需要的是对指针指向的内容做‘<’操作。以T形参绑定到char*为例,我们可以做如下的特化,使函数可以满足操作需求:

template<>//7
int compare<const char *>/*8*/(const char * const &c1,
						  const char * const &c2)
{
	cout<<"In specialization template"<<endl;
	return strcmp(c1,c2);
}

语句7中因为模板形参被实例化,所以只有一个空的模板形参表。标号8处,函数名后接一对尖括号,尖括号中是指定该特化模板的模板形参值。在特化版本中,我们针对特定的类型提供处理方案,使得期望的操作得以执行。需要注意的是,特化的声明必须与对应的模板相匹配。如果不匹配将会报错,因为编译器会认为这是一个错误的语法(像模板定义,但又不是)。

当模板实参的类型可以由函数形参类型推断时也可以省略,上面的特化也可以如下定义:

template<>//7
int compare/*8*/(const char * const &c1,
			   const char * const &c2)
{/*具体操作*/}

但是语句7不能省略,如果没有了该语句,则该函数的定义将变成一个普通函数的定义,而非模板的特化操作了。

沿用上面的A类进行类特化,如下:

template <>
class A<const char *>
{
public:
	static int size;
	static void name();
};

在特化类外部定义成员格式如下:

//前面无需加:template<>
void A<const char *>::name()
{
	{cout<<" In specialization template class A"<<endl;}
}

当类中有多个模板类型形参时也可以做部分特化。模板的部分特化本身也是模板。部分特化的模板形参表中只列出未知模板实参的那些形参。

定义一个模板类如下:

template <class A, class B>
class T
{};

则其可以有如下的部分特化:

template <class A>//将特化了的模板形参移去
class T<A, int>
{};

或:

template <class B>//将特化了的模板形参移去
class T<int, B>
{};

部分特化的定义与通用模板的定义完全不会冲突。部分特化可以具有与通用模板完全不同的成员集合。类模板成员的通用定义永远不会用来实例化类模板部分特化的成员。当声明了部分特化的时候,编译器将为实例化选择最特化的模板定义,当没有部分特化可以使用的时候,就使用通用模板定义。

也可以只特化类中的某些成员,而不特化整个类。有如下的类定义:

template <typename T>
class A
{
public:
	static int size;
  void name(){cout<<"class A"<<endl;}
};

例如,特化size成员:

template<>
int A<int>::size = 10;

或者,特化name成员:

template<>
void A<string>::name()
{
   cout<< "In special one!" << endl;
}

五、类模板中的友元

在类模板中可以出现三种友元声明,每一种都声明了与一个或多个实体的友元关系:

1. 普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。

2. 类模板或函数模板的友元声明,授予对友元所有实例的访问权。

3. 只授予对类模板或函数模板的特定实例的访问权的友元声明。

第一种情况,普通友元关系:

class One
{};

void function(){}

template<class T>
class Two
{
	friend class One;
	friend void function();
};

第二种情况,一般模板友元关系:

template<class V>
class One
{};

template<class S>
void function(){}

template<class T>
class Two
{//注意此时friend关键字的位置
	template<class V> friend class One;
	template<class S> friend void function();
};

第三种情况,特定模板友元关系:

template<class V>
class One
{};

template<class S>
void function(){}

template<class T>
class Two
{
//指定特定实例为友元
	friend class One<int>;
	friend void function<string>();
  //指定与本类有相同实参的实例为友元
friend class One<T>;
	friend void function<T>();
};

六、重载与函数模板

函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:

1.  为这个函数名建立候选函数集合,包括:

(a)  与被调用函数名字相同的任意普通函数。

(b)  任意函数模板实例化,在其中,模板实参推断发现了与调用中所用函数实参相匹配的模板实参。

2.确定哪些普通函数是可行的。候选集合中的每个模板实例都是可行的,因为模板的实参推断保证了这一点。

3.如果需要转换来进行调用,根据转换的种类排列可行函数,其中,调用模板函数的实例所允许的转换是有限的。

(a)如果只有一个函数可选,就调用这个函数。

(b)如果调用有二义性,从可行函数集合中去掉所有函数模板实例。

4.重新排列去掉函数模板实例的可行函数。

如果只有一个函数可选,则调用该函数;否则该调用具有二义性。

当同时有多个匹配函数的时候,普通函数的调用要优先于模板函数,特化的模板函数要优先于普通的模板函数,越特化,优先级越高。




你可能感兴趣的:(C++独孤九剑第九式——以静制动(模板编程探索))