【C++详解】模板进阶 非类型模板参数,函数模板特化,类模板全特化、偏特化,模板分离编译
文章目录
- 一、非类型模板参数
- 应用场景
- 二、模板的特化
- 函数模板特化
- 类模板特化
- 全特化
- 偏特化
- 三、模板分离编译
- 解决方法
- 四、模板总结
一、非类型模板参数
先前介绍的函数模板和类模板都是针对类型的类模板参数,非类型模板参数有哪些使用场景呢?我们先来看下面这个例子:
const int N = 10;template <class T>
class stack
{
public:T _a[N];int top;int capacity;
};int main()
{stack<int> s1; //10stack<int> s2; //1000return 0;
}
这是我们定义的一个静态的栈,栈的大小由N来决定,我们想创建一个大小为10的栈s1和一个大小为1000的栈s2就会出现一个问题,由于静态栈类型大小的每次编译时是固定的,如果我们创建这两个栈那么空间只能开1000,那么s1就会确定浪费990大小的空间。所以这个场景下我们就可以用非类型模板参数,它就是用一个整型常量来作为类(函数)模板的一个参数,在类(函数)模板中可以把这个参数当作常量来使用。
template <class T, size_t N> //非类型模板参数
class stack
{
public:T _a[N];int top;int capacity;
};int main()
{stack<int, 10> s1; //10stack<int, 1000> s2; //10000return 0;
}
非类型模板参数也能给缺省值,模板参数和函数参数一样,缺省值只能从右往左给,传实参时只能从左往右依次给,避免参数匹配的歧义性。
若模板参数是全缺省,我们创建对象时模板参数可以都不传,但是还是要带<>:
stack<> s3; //全缺省
注意:
1、浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2.、非类型的模板参数必须在编译期就能确认结果。
应用场景
在标准库里array容器就用到了非类型模板参数,它是静态数组,类似C语言的arr[ ],。功能也和arr[ ]差不多,它创建出来后也不会对成员初始化,初始化值需要用它的一个接口fill。
array和arr[ ]的本质区别是array下标访问是用的重载函数operator[ ],所以有严格的越界读和越界写的检查,而arr[ ]本质是指针±和指针的解引用,只有在临界区域对越界写有抽查,完全没有越界读检查。
array<int, 10> a1;
a1.fill(1); //初始化
在C++中我们用数组容器一般优先考虑vector,array相比vector的优势在开大量固定大小的数组时array效率更高,因为array是在栈上开空间,在函数栈帧创建出来时空间就开好了,vector需要在堆上动态开辟。
二、模板的特化
当我们实现出的模板函数或者模板类不符合我们预期时就可以用到模板特化,模板特化与原模板深入绑定,只有当原模版存在时模板特化才会生效。简单来说普通模板实例化是按模板生成代码,特化本身已是具体代码,它是为特殊类型改写代码,且匹配优先级比普通模板更高。
函数模板特化
函数模板特化的返回值、参数列表需与主模板替换类型后完全匹配,
我们先来看一个例子:
Less对传过来的整型变量可以准确的比较大小,而对于传过来的整型指针变量我们本意是想让它比较指针指向的整型变量的大小,而这里底层逻辑是比较指针变量本身的大小,所以不符合我们预期,这里就可以针对整型指针变量单开一个模板的特化版本,改写比较逻辑。
这个时候可能有的读者有和我当时一样的想法,为什么不直接写一个函数用来特殊处理int*,比如:bool Less(double* left, double* right),而是用模板特化呢?
我们要知道模板是一种泛型编程的思想,模板本意是设计出一套通用逻辑,而如果我们再创建一个普通函数,就会破坏模板本身的泛型语义一致性,使得不同类型调用Less函数时行为变得零散。模板特化就是模板为特定类型做到特殊适配,模板与特化模板是强耦合的,后续要调整通用模板的逻辑时也要考虑特化模板是否要配套修改。
我们初步设计的函数特化如下:
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}
注意事项:
1.必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数模板特化参数要和原模版一致,如果不同编译器可能会报一些奇怪的错误。
前三点很好理解,最后一点我们需要借助一个例子来理解,其实前面我们写的普通模板并不是最最优的,我们T类型我们无法确定,可能是内置类型也可能是string或者自定义类型,如果对象一但过大这时我们原先普通模板是传值传参因为要拷贝效率就很低了,这里的最优解是传引用,并且比较逻辑不改变参数最好还要加const修饰:
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}
而普通模板写成这样我们再写特化版本难度就变高了,在此之前我们要先明确一点,当const修饰指针变量是当const在*左边时是修饰的指针指向的内容不能修改,当const在*右边时是修饰指针变量本身不能被修改。
const T* p1; //修饰*p1
T const * p2; //修饰*p2
T* const p3; //修饰p3
这里我们要写模板特化就要梳理普通模板的逻辑,因为特化版本的参数类型要和原模版参数类型严格一致,原模版是引用的T变量,并且const是修饰的被引用的T变量本身,我们要为int*写一个模板特化,那么int*在逻辑上就是原模版的T,所以我们不能写成const int*& left,因为const是修饰的int*变量指向的内容,就相当于const是修饰原模版中的*T,所以为了和原模版参数严格匹配这里模板特化的参数应该是int* const & left,这样才和原模版一样,const修饰的是引用变量本身。
template<>
bool Less<int*>(int* const & left, int* const & right)
{return *left < *right;
}
类模板特化
类模板特化就是将全部或者一部份参数确定化,模板参数全部确定化是全特化,模板参数部分确定化是偏特化,当然偏特化也可以是对参数的进一步限制。
类模板特化也需要以没有特化过的原模版为基础进行特化。模板名称和模板参数结构上需要与原模板保持一致,但在成员定义、实现逻辑上可以完全不同。
全特化的匹配优先级比偏特化优先级更高,因为全特化更现成,具体用全特化还是偏特化由使用场景决定。
全特化
全特化即是将模板参数列表中所有的参数都确定化。
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; }
};
偏特化
- 特化部分参数
//特化部分参数
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>" << endl; }
};
- 对参数进一步限制
//对参数的进一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }
};template<class T1, class T2>
class Data<T1&, T2*>
{
public:Data() { cout << "Data<T1&, T2*>" << endl; }
};
也可以两种混着特化:
template<class T2>
class Data<int, T2*>
{
public:Data() { cout << "Data<int, T2*>" << endl; }
};
三、模板分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。也就是我们常说的把声明放在头文件,把实现放在.cpp。
模板不建议分离编译,无论的类模板还是函数模板。这是模板的定义对调用方不可见,定义方不知道调用方的使用需求导致的。
比如一个模板定义在tep.cpp,声明在tep.h,test.cpp调用这个模板。
调用方有模板实例化成什么具体类型的信息,但是它只有模板的声明,所以它在编译阶段无法实例化出具体代码,因为模板要实例化必须要模板的完整定义。
定义方tep.cpp有模板的完整定义,但是它没有模板实例化成什么具体类型的信息,所以它也同样无法在编译阶段实例化出具体代码。
那么定义方模板在编译期间因为没有生成具体代码就不会进符号表,然后链接阶段调用方想找到调用模板的符号但是符号表里没有,就会因为找不到符号而报错。
归根结底就是因为.cpp文件在链接之前不能交互,那么发生在链接之前的编译阶段就会因为信息无法交互使得模板无法生成具体代码。
这也和内联函数不能分离编译的原因有有相似之处,都是因为分离编译导致
“定义不可见”,最终链接阶段符号缺失。但是内联函数是因为调用处没有内联函数的完整定义导致函数无法展开,内联函数又因为机制不会在定义处进符号表。模板是因为调用处没有模板的定义无法实例化出具体代码,定义处因为没有具体类型而无法生成具体代码。
类模板也一样,类模板是将它的成员函数分离到了两个文件。
解决方法
这个问题就是因为没有实例化造成的,所以解决这个问题就是要让模板在编译阶段能实例化,不管是定义方还是调用方。
1、模板定义的位置显式实例化。这种方法不实用,不推荐使用,因为你每调用一种类型都要显示实例化一份。所以这也验证了模板是可以分离编译的,只是不推荐。
template<class T>
void FuncT(const T& x)
{cout << "void FuncT(const T& x)" << endl;
}
//显示实例化
template
void FuncT(const int& x);
2、模板的最佳实践就是不要分离编译,把模板直接定义在头文件,调用模板的地方在编译期间就有了模板的定义生成了具体代码,填入了对应的符号,不用在链接期间再去找。
四、模板总结
优点
1、模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2、增强了代码的灵活性
缺陷
1、模板会导致代码膨胀问题,也会导致编译时间变长(不可避免,自己写也得写)
2、出现模板编译错误时,错误信息非常凌乱,不易定位错误
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~