模板的进阶
前言
之前已经讲过了模板的基本用法,相信大家已经感受到了模板的好处,能让我们便捷地解决许多问题,接下来我们来对模板的剩余内容进行讲解。
一、非类型模板参数
其实,我们函数模板和类模板中的模板参数不只是有类型参数,还有非类型模板参数。
类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
template<class A>
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
template<class A, size_t n = 10>
这后面的 n 就是非类型模板参数,顾名思义就是不是一个类型,而是一个整型。
需要注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的(目前cpp11规定这个非类型模板参数只能是整型,但是在cpp20以后可以使用其他类型作为模板参数)。
2. 非类型的模板参数必须在编译期就能确认结果。
二、模板的特化
模板的特化分为全特化和偏特化(半特化),但同时也分为类模板的特化和函数模板的特化。
那什么是模板的特化呢,模板大家都知道就是相当于一个模具,我们通过传入不同的类型来减少我们自己写代码的工作量(其实工作量没有减少,只是将其交给了编译器进行工作),但是有些时候明明我们想要的函数或者类的模板还是这个模板,但是并没有实现我们预想的作用,于是就产生了模板的特化,即将特殊情况单独拎出来做特殊化处理。
1、函数模板的特化
模板的特化都有其固定格式,看起来会感觉怪怪的。我们先来讲讲函数模板的特化,告诉大家为什么函数模板的特化没有必要。
上面我说了有的函数或类根据模板并不能达到理想效果,所以出现了模板特化,那下面就给大家举个例子:
上述代码中可以发现结果随机的那个每次结果都不一样,为什么呢,因为我们每次都会new一个Date,此时函数比较的是他们的地址,也就是 Date*, 地址是编译器随机分配的,所以每次比较结果都不一样,但很明显我们想要比较的是他的内容,所以这时我们就可以用到模板特化。
我们先来讲讲函数模板特化的步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误(这个很重要)。
好,接着我们就写一个特化函数模板对之前的错误进行改正:
template<class T>
bool Less(T a, T b)
{return a < b;
}template<>//函数模板特化
bool Less<Date*>(Date* a, Date* b)
{return *a < *b;
}
但是不知道大家有没有发现,函数模板的特化真的有必要吗?之前我们讲过函数重载匹配的问题,函数一定会匹配最合适的,所以我们为什么不能直接写一个函数重载呢,还不用遵守模板特化的规矩,这不香吗?
如下:
template<class T>
bool Less(T a, T b)
{return a < b;
}
bool Less(Date* a, Date* b)
{return *a < *b;
}
但是我上面提到的特化函数的参数必须要和模板函数的基础参数类型完全相同,这一点大家可能不太注意,但是有一个特例:
template<class T> bool Less(const T& a, const T& b)//当参数为const引用类型时一定注意const修饰的是谁 {return a < b; }template<> bool Less<Date*>(Date* const& a, Date* const& b) {return *a < *b; }
大家不注意的话,特化的参数肯定就写成const Date*& a,你说这是严格按照上面格式写的,但编译器不这么认为,因为你函数模板const修饰的是 a 本身或者说是本体(因为a是别名),但是下面如果你写成const Date*& a,const修饰的就是a指向的内容了
虽然在函数模板这里特化有些鸡肋,但谁说类模板特化也鸡肋呢,类模板特化那可太香了。
2、类模板的特化
为什么我说类模板特化就很香呢?
函数在调用模板时会有一个最适匹配原则,可是类模板在实例化成对象时必须显式地给出你想要的类型,eg、<int, char> ,所以还是要显示实例化对象,不能省事的交给编译器去识别类型看谁合适,那这是类模板就很香了。
模板的全特化
和函数模板的使用步骤相同,我们直接举个例子就可以了:
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};template<>//特化类模板
class Data<int, char>
{
public:Data() { cout << "data<int, char>" << endl; }
private:int _d1;char _d2;
};
void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}
模板的偏特化(半特化)
偏特化分为两部分,一是只特化部分模板参数,二是对模板参数进行进一步的限制。
我们以以下例子为类模板,讲解两种类型的偏特化:
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}
private:T1 _d1;T2 _d2;
};
(1)对模板参数进行部分特化
下面我们选择对第一个参数进行特化,那么我们这时就不能在template后直接使用空尖括号了,应该像这么写:
template<class T2>//这里是你想让哪个参数不变,你就在括号写哪个,//比如你想让上面模板中第二个参数不变,你就可以写T2,但是写T1也没问题
class Data<int, T2>//因为这里会确定你具体是哪个参数没变,但是建议按照原模板顺序,容易看
{
public:Data() { cout << "Data<int, T1>" << endl; }
private:int _d1;T2 _d2;
};
void test2()
{Data<char, char> d1;Data<int, char> d2;//这里会优先调用特化模板
}
(2)对模板参数进行进一步限制
我们也可以对模板参数进行进一步限制,也就是两个参数全都保持不变,比如我们可以给类型偏特化为指针或者引用类型,如下:
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}private:T1 _d1;T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout<<"Data<T1&, T2&>" <<endl;}private:const T1 & _d1;const T2 & _d2; };
接着我们讲一个typename的特殊用法(class无法替代)。
三、typename不同于class的用法
我们之前说过typename和class虽然可以混用,但是还是有区别的,比如以下情况:
template<class T>
void Print(const vector<T>& v)
{typename vector<T>::const_iterator it = v.begin();//auto it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}template<class Container>
void Print(const Container& con)
{typename Container::const_iterator it = con.begin();//auto it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}
看上述例子,当我们要使用一个其他类模板里的成员类型时,由于我们并不知道类模板的模板参数类型,也就是类模板还没有实例化成类,那么编译到这个语句时编译器就无法进入到类内部去确定 const_iterator 到底是成员类型还是成员变量,就会编译报错,所以我们需要在类型前加上一个 typename 关键字来告诉编译器这个 const_iterator 是 vector<T> 类里的成员类型,而非成员变量。
但是,我们还有更好的办法,就是如代码中所示,使用typename的那一句代码后面的注释,我们直接使用auto来表示类型,让编译器到时候自己去分析(重命名时不可以,因为你不可能写一个typedef auto it,那谁知道你这个auto是啥),哈哈哈,是不是很好用,auto的含金量还在上升。
四、模板的分离编译
1、什么是分离编译
那么什么是分离编译呢?
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有 目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
2、为什么模板不能将声明和定义分开
之前我们讲过模板不能将声明和定义分开,但是为什么呢,其实程序的运行分为 预处理-》编译-》汇编-》链接 四个步骤,再进行链接之前,各个文件是不会互相沟通链接的,也就是说他们会各干各的。
如果模板声明和定义分离的话,由于在显式实例化之前模板并不会实例化,也就是说并不会生成具体的函数,这时我们如果把头文件(也就是包含模板函数声明的Add.h文件)包含在test.cpp和拥有模板定义的Add.cpp中,这时在链接之前,由于你显式实例化对象是在test.cpp中,所以在test.cpp中的声明会知道你实例化的类型是什么,但是Add.cpp并不知道,这时链接就会报错。
3、解决方法
1. 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长(但这是不可避免的,因为人写的代码量变少了,但是实际上机器处理的代码并不会减少)
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误