文章目录
- 一、非类型模板参数
- 二、模板的特化
-
- 1. 函数模板特化
- 2. 类模板特化
- 三、模板的编译分离
一、非类型模板参数
模板参数可以分为类型参数和非类型参数。我们之前使用的都是类型参数,即出现在模板参数列表中,跟在class或typename之类的参数类型名称。
非类型参数,就是用一个常量作为模板的一个参数,在模板中可以将该参数直接当成常量使用:
template<class T, int N = 10>
class array
{
T _arr[N];
size_t _size;
};
当然,非类型参数除了可以写缺省值,当然也可以传参:
array<int, 20> a1;
array<double, 30> a2;
浮点数、类对象、字符串,不可以作为非类型模板参数。
非类型模板参数必须在编译时期就能确定值。
二、模板的特化
通常情况下,使用模板可以实现与类型无关的代码,但是对于一些特殊的类型,可能会得到一些错误的结果,比如:
template<class T>
bool less(T left, T right)
{
return left < right;
}
如果left < right则返回true,反之则返回false。当T是整型、浮点数、重载了<的自定义类型时,结果都没有问题。但是若T是指针类型,left和right的本质就是地址的值了,将它们比较只会得出地址的大小,一般没有意义。
如果我们的想法是T传成指针类型时,比较的是left和right的指向对象时,就需要对模板进行特化:在原模板的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化和类模板特化。
1. 函数模板特化
函数模板特化的步骤是:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对<>
- 函数名后接一对<>,里面指定需要特化的类型
- 函数形参表,必须和模板函数的基础参数类型完全相同
举个栗子:
//基础的函数模板
template<class T>
bool less(T left, T right)
{
return left < right;
}
//对less函数模板进行特化
template<>
bool less<int*>(int* left, int* right)
{
return *left < *right;
}
但是特化还是太繁琐了,我们其实可以直接针对需要特殊处理的类型直接写一个普通函数就好了,编译器遇到这种类型时也会自动调用它而不会去使用模板:
bool less(int* left, int* right)
{
return *left < *right;
}
这种实现简洁明了,代码可读性高,容易写。对于一些参数类型复杂的模板,特化时很麻烦,因此函数模板不建议特化。
2. 类模板特化
类模板特化的步骤大体和函数模板的步骤一样。类模板特化可以分为全特化、偏特化
- 全特化是指将模板参数列表中的所有参数都确定。
//基础的类模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 data1;
T2 data2;
};
//全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int data1;
char data2;
};
这样一来,若调用的Date类的传入类型是<int, char>,编译器就会优先匹配这个特化版本的类。其他传入则还是用类模板实例化。
- 偏特化是指任何针对模板参数进一步进行条件限制设计的特化版本。举个例子,对于模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 data1;
T2 data2;
};
我们可以将模板参数类表中的一部分参数特化:
template<class T1>
class Data<T1, char>
{
public:
Data()
{
cout << "Data<T1, char>" << endl;
}
private:
T1 data1;
char data2;
};
template<class T2>
class Data<int, T2>
{
public:
Data()
{
cout << "Data<int, T2>" << endl;
}
private:
int data1;
T2 data2;
};
这样一来,若Data传入的类型列表的第二个是char,则优先匹配上面第一种偏特化;若Data传入的类型列表的第一个是int,则优先匹配上面的第二种偏特化。也就是说,哪种版本更匹配优先用哪种。
除此之外,还可以对参数进行更进一步的限制。偏特化不仅仅是指特化部分参数,还是针对模板参数更进一步的条件限制所涉及的特化版本。如:
//两个参数偏特化为引用类型
template<class T1, class T2>
class Data<T1&, T2&> //&标明T1和T2本身是引用类型
{
public:
Data()
{
cout << "Data<T1&, T2&>" << endl;
}
private:
T1 data1;
T2 data2;
};
//两个参数偏特化为指针类型
template<class T1, class T2>
class Data<T1*, T2*> //*标明T1和T2本身是指针类型
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 data1;
T2 data2;
};
如果我们把上面所有版本特化放到一起,看看效果:
//基础的类模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 data1;
T2 data2;
};
//全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int data1;
char data2;
};
//偏特化1
template<class T1>
class Data<T1, char>
{
public:
Data()
{
cout << "Data<T1, char>" << endl;
}
private:
T1 data1;
char data2;
};
//偏特化2
template<class T2>
class Data<int, T2>
{
public:
Data()
{
cout << "Data<int, T2>" << endl;
}
private:
int data1;
T2 data2;
};
//两个参数偏特化为引用类型
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
Data()
{
cout << "Data<T1&, T2&>" << endl;
}
private:
T1 data1;
T2 data2;
};
//两个参数偏特化为指针类型
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 data1;
T2 data2;
};
int main()
{
Data<char, int> d1; //都不匹配,调用基础类模板
Data<int, char> d2; //都匹配,调用全特化
Data<int, double> d3; //第一个是int,调用偏特化版本2
Data<double, char> d4; //第二个是char,调用偏特化版本1
Data<int*, int*> d5; //都是指针类型,调用指针版本偏特化
Data<int&, int&> d6; //都是引用类型,调用引用版本偏特化
return 0;
}
没有问题~
三、模板的编译分离
通常情况下,一个项目由多个源文件组成,在运行时每个源文件单独编译生成目标文件,最后将所有目标文件链接起来生成一个可执行文件,这个过程叫做分离编译模式。
之前我们对于函数,都是将声明和定义分离,这是实际项目中的规范性。但是模板不可以声明和定义分离,这样虽然可以通过编译,但是链接过程中有无法匹配的问题。编译时期,模板定义所在的cpp文件和调用所在的cpp的文件各自独立,模板没有传入类型,也就没有生成实际的函数或类。链接时调用处无法找到调用对象,发生“链接错误”。最好的解决方法就是将模板的声明定义都写在一个头文件中,不要声明和定义分离了。因为在预处理阶段,cpp文件的头文件都会展开,相当于调用所在cpp文件里面就有了声明和定义了。就不会发生链接错误。
本篇完,感谢阅读~