1.非类型模板参数
1.1 array容器
为了引出非类型模板参数,我们先介绍一种容器,叫做array,即静态数组。
我们在之前的学习中接触过vector容器,vector容器是动态数组,在堆上动态开辟空间存放数据。而array不同,array也是数组,但是却是在栈上开辟的数组,所以这样的数组就具有了长度限制。总结来说,和vector对比,array是一个有着固定长度的静态数组,vector是一个长度可变的动态数组。array相当于把普通的静态数组进行了一定的包装,给出了一些方便使用的成员函数。
一般不使用array而使用vector,因为vector在堆中,长度可控,可以在创建时被初始化,具有越界检查;而array在栈中,长度固定,没有初始化,越界检查只在数组后的几个位置进行抽查。
1.2 非类型模板参数
我们已经介绍过关于模板的一些知识了,这次我们详细讨论一下模板参数。模板参数分为 类型模板参数 和 非类型模板参数。
类型模板参数:在模板参数列表中使用class或者typename标识。
非类型模板参数:在模板参数列表中不需要关键字,类似于函数传参的形式。非类型模板参数只能是整型家族,可以任意个且无顺序要求。
array是一个静态数组容器,所以在实例化array模板的时候,不仅仅需要告知元素类型,还需要指出开辟的数组的长度,所以就需要非类型模板参数。
template<class T,size_t N = 10>
class array
{
public:
size_t size() const
{
return _size;
}
size_t empty() const
{
return _size == 0;
}
T& operator[](size_t pos)
{
return _array[pos];
}
const T& operator[](size_t pos) const
{
return _array[pos];
}
private:
T _array[N];
size_t _size = N;
};
2.模板的特化
2.1 函数模板的特化
2.1.1 特化使用情形
函数模板一般是给定类型参数,然后类型参数被实例化后调用实例化的函数的函数体。但是如果需求发生改变,不同的类型需要实现不同的函数模板形式,那么就会出现矛盾。
举个例子:
template<class T>
bool myless(T e1, T e2)
{
cout << "bool myless(T e1, T e2)" << endl;
return e1 < e2;
}
void Test1()
{
int a = 20, b = 10;
int* pa = &a, * pb = &b;
cout << myless(a, b) << endl;
cout << myless(pa, pb) << endl;
}
对于上述代码,虽然都是调用小于比较myless,但是对于a和b而言,模板参数T被实例化为int类型,实际上比较的是a<b。但是对于pa和pb而言,模板参数T被实例化为int*类型,比较的就是&a<&b,指针比较,结果明显不可控了。
对于这种情况,我们要辩证的看待,出现这种问题模板和调用双方都有问题。
对于调用而言,调用者应该清楚模板使用方法,想要比较指针的值不应该简单的把指针作为参数,而是应该解引用后传参。
cout << myless(*pa, *pb) << endl;
对于模板而言,模板也有问题,模板考虑到指针类型的比较做出修改也可以顺利解决这个问题。因此就引出了函数模板特化:在原模板的基础上为了支持特殊类型的实例化做出的特殊化处理。
2.1.2 特化的创建
①特化是针对已经存在的函数模板做特殊化处理,所以首先要有一个函数模板。
②在函数模板之后,使用template<模板参数>,因为存在全特化和半特化的不同,所以可能在特化中也仍然需要使用模板参数,而这一句标识出了在特化中会使用到的模板参数。函数模板没有半特化,只有全特化,所以函数模板的特化没有参数,一定是template<>。
③在template<>之后,就是函数体,需要注意的是需要在函数名后使用尖括号指定特化类型,这个尖括号中应该包含所有的模板参数的说明。即这里是原模板的特化后的参数结果,被指定的类型写出,没有指定的参数仍写成模板参数的形式。
④特化函数名后<>的内容和最初的函数模板<>的内容一一对应,特化函数参数和函数模板的参数也要一一对应。函数特化可以存在多个。
//函数模板的特化
template<class T>
bool myless(T e1, T e2)
{
cout << "bool myless(T e1, T e2)" << endl;
return e1 < e2;
}
//特化首先必须要有一个函数模板,然后需要使用template<>,然后在之后定义特化的函数,在尖括号中写明特化类型,并参考模板写参数
//函数模板特化可以存在多个类型的特化
template<>
bool myless<int*>(int* e1, int* e2)
{
cout << "bool myless<int*>(int* e1, int* e2)" << endl;
return *e1 < *e2;
}
template<>
bool myless<double*>(double* e1, double* e2)
{
cout << "bool myless<double*>(double* e1, double* e2)" << endl;
return *e1 < *e2;
}
最后对函数模板特化补充一点,有很多方法可以平行替代特化,如针对指定类型写出实际的函数,或者重载其它类型的函数模板。对于如下代码中T*的模板参数,只会被推导为一级指针及更高级数的指针。
template<class T>
bool myless(T e1, T e2)
{
cout << "bool myless(T e1, T e2)" << endl;
return e1 < e2;
}
//也可以再重载一种函数模板,专门用于接受指针
//将模板参数定义为T*,在传递指针的时候会优先实例化调用这个函数
//相较于特化更简单,更建议使用
template<class T>
bool myless(T* e1, T* e2)
{
cout << "bool myless(T* e1, T* e2)" << endl;
return *e1 < *e2;
}
2.2 类模板的特化
类模板的特化流程语法和函数模板相似。
①首先也要有一个类模板。
②在类模板之后,使用template<模板参数>,因为存在全特化和半特化的不同,所以可能在特化中也仍然需要使用模板参数,而这一句标识出了在特化中会使用到的模板参数。
针对类模板特化,就有了全特化和半特化之分。全特化的参数全部确定,所以template<>没有参数;半特化(偏特化)参数未完全确定,存在缺省。半特化和半缺省一样,半特化给定的部分类型必须从后往前指定,及一个参数给定了那么其后的所有参数都应该是给定的。
除此之外,对于参数类型的进一步限制也是被允许的,如原本的T参数,在特化中被指定为只能是T*的指针参数是可以的,一般认为这种类型也是半特化(因为没有确定所有参数)。
③在template<>之后,就是类的内容,需要注意的是需要在函数名后使用尖括号指定特化类型,这个尖括号中应该包含所有的模板参数的说明。即这里是原模板的特化后的参数结果,被指定的类型写出,没有指定的参数仍写成模板参数的形式。
④特化类名后<>的内容和最初的类模板<>的内容一一对应,类的特化也可以存在多个。
//类模板特化
//和函数模板特化类似,特化首先必须要有一个类模板,然后需要使用template<>,然后在之后定义特化的函数,在尖括号中写明特化类型,并参考模板写参数
//特化同样可以不只一个,template<>的尖括号中的其实是未被特化的参数
namespace test4
{
template<class T1, int N, class T2>
class A
{
public:
A()
{
cout << "A< T1, N, T2>" << endl;
}
};
//全特化
//全特化没有未被确定的参数,所以template<>中没有内容
template<>
class A<int, 100, char>
{
public:
A()
{
cout << "A< int, 100, char>" << endl;
}
};
//半特化、偏特化
//偏特化存在未被确定的参数,所以template<>中有内容
//半特化的参数和半缺省一样,需要从后往前确定,不可以跳跃
template<class T,int N>
class A<T, N, char>
{
public:
A()
{
cout << "A< T, N, char>" << endl;
}
};
//半特化也可以是对参数进行进一步的限制
template<class T1, int N, class T2>
class A<T1&, N, T2*>
{
public:
A()
{
cout << "A< T1&, N, T2*>" << endl;
}
};
void Test1()
{
A<int, 10, int> a1; //没有特化
A<int, 100, char> a2; //全特化
A<int, 90, char> a3; //半特化
A<double, 30, int> a4; //没有特化
A<int&, 990, char*> a5; //半特化
}
}
A< T1, N, T2>
A< int, 100, char>
A< T, N, char>
A< T1, N, T2>
A< T1&, N, T2*>
3.模板分离编译
在C语言中我们曾提到过声明和定义分离的思想。对于一个函数,将其声明写在一个源文件中,或头文件中被源文件包含,而其定义写在另一个源文件中。在链接的阶段,链接器就会将其进行链接,链接每一个声明的函数名符号和其对应的地址。最后生成可执行文件。
而在C++中,对于一般的函数仍然可以采取这一套方案,但是需要注意缺省值应该在声明处给出。
在C++中,模板使用定义和声明分离就会产生问题。首先要知道,模板的实例化方式是根据具体参数要求来实例化出对应的函数或类。换言之,如果模板被实例化了,编译器才会编译出实例化所需要的函数实体或类实体,当没有被实例化就不会生成对应的实体。相当于模板实例化为int,但没有实例化为double,那么最后编译器就只会生成int类型的实体,而没有double类型的实体。
这么做是很有道理的,因为参数有着无限可能,模板不可能照顾到所有情况,它只能在发现需要何种类型的实体时才会动手生成对应的实体。
模板的实例化发生在编译阶段,在链接之前。当声明和定义分离时,有着声明的源文件具有实例化的信息,但是没有实例化的模板,所以相当于被翻译成为了指定类型的函数声明,被列在符号表内,期待在链接时找到对应的函数体。而有着定义的源文件虽然有实例化模板,但是没有实例化信息,所以不知道生成什么类型的函数,所以它能做的只有什么都不做。于是在链接阶段就会找不到对应的函数从而报错。
对于如下的声明和定义分离就会产生连接错误:
array1.cpp
namespace m_array { template<class T,size_t N = 10> class array { public: size_t size() const; size_t empty() const { return _size == 0; } T& operator[](size_t pos) { return _array[pos]; } //实例化模板时按需实例化,只有被调用了的成员才会被实例化 //但是在vs环境下会被进行语法检查 const T& operator[](size_t pos) const { return _array[pos]; } private: T _array[N]; size_t _size = N; }; }
array2.cpp
namespace m_array { //在声明处给出缺省值 //模板的声明和定义分离这样写会导致链接错误 template<class T, size_t N> size_t array<T, N>::size() const { return _size; } }
为了解决声明和定义分离的问题,可以让定义所在的源文件提前产生对应需要实例化的实体,即告诉其实例化信息。可以使用template来完成这个操作。
array2.cpp
namespace m_array { //在声明处给出缺省值 //模板的声明和定义分离这样写会导致链接错误 template<class T, size_t N> size_t array<T, N>::size() const { return _size; } //因为各个cpp文件单独编译,模板实例化发生在编译之前,所以在链接之前并不知道实例化成什么类型,所以不会生成其他cpp文件需要的实例化形式 //通过如下方式进行显式实例化后,生成了其他文件需要的实例化形式就可以在链接的时候成功调用了 template array<int, 5>; }
在array1.cpp中提到了按需实例化,因为没有被实例化的模板,和被实例化的类但是没有使用的成员函数可能为了优化并不会被编译,所以可能存在语法错误而不被检查的情况。这一点需要注意进行测试排查。