【C++】模板进阶

目录

一,非类型模板参数

二,模板的特化

1,函数模板特化

2,类模板特化

2-1,全特化

2-2,偏特化

三,模板分离编译


一,非类型模板参数

模板参数分类类型形参与非类型形参,以前我们接触最多的是类型模板参数,下面我门来认识一下非类型形参。

**类型形参即:**出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

**非类型形参:**就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。如下:

template <class T, size_t n> //这里的n直接指定类型为size_t

.........

注意:

  1. 非类型模板参数只能传入整型常量。如size_t、int、long、long long等

  2. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。

  3. 非类型的模板参数必须在编译期就能确认结果。

#include <iostream>
using namespace std;
template <class T, size_t n> //这里使用非类型形参时,若使用double、float、string等类型会报错,但支持char,因为字符是按照整型存储
class bit
{
public:
T _arr[n];
};
int main()
{

//下面进行调用,实现定量数组
bit<int, 5> v;
bit<char, 10> v2;
return 0;
}


二,模板的特化

**前述:**通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行比较的函数模板,如下:

#include <iostream>
using namespace std;
template <class T>
bool compare(T x, T y) {
return x > y;
}
int main()
{
int _x = 1;
int _y = 2;
int _z = 3;
cout << compare(_z, _y) << endl; //这里比较正确

int* x = new int(1);
int* y = new int(2);
int* z = new int(3);
cout << compare(z, y) << endl; //发现这里答案错误,因为比较的是地址,地址的大小这里每次都会变化
return 0;
}

可以看到,compare绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,x、y、z皆为地址,但是compare内部并没有比较x、y、z指向数据的内容,而比较的是它们的地址,这就无法达到预期而错误。

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式,比如以上compare比较中,当传入地址时,进行解引用的比较。

模板特化中分为函数模板特化与类模板特化。

1,函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板。

  2. 关键字template后面需要被特化的类型直接省略不写,若全部特化,在template接一对空的尖括号<>,即全部不写。如下:

template <class T, class _T>
bool compare(T x, _T y) {
return x > y;
}
//部分特化
template <class T>
bool compare<int*>(T x, int* y) {
return x > *y;
}

  1. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。

  2. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

下面,我们对以上代码进行修改,使用函数模板特化。

#include <iostream>
using namespace std;
//函数模板 -- 参数匹配
template <class T>
bool compare(T x, T y) {
return x > y;
}
//对compare函数模板进行特化成int*,即全部特化
template <>
bool compare<int*>(int* x, int* y) {
return *x > *y;
}

int main()
{
int _x = 1;
int _y = 2;
int _z = 3;
cout << compare(_z, _y) << endl; //这里比较正确

int* x = new int(1);
int* y = new int(2);
int* z = new int(3);
cout << compare(z, y) << endl; //传入的类型是int*,满足特化类型,直接传入特化
return 0;
}

通过以上我们可发现,函数模板特化比较麻烦,还不如直接给出需要特殊处理的函数,因此,一般情况下,如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。函数特化一般不建议,如下,可直接给出具体形式:

bool compare(int* x, int* y) {
return *x > *y;
}

2,类模板特化

类模板特化分为两种:全特化和偏特化。

2-1,全特化

全特化即是将模板参数列表中所有的参数都确定化,原理跟函数模板特化一样。

template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//全特化,直接特化为int,char
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};

2-2,偏特化

偏特化是将模版参数进一步进行条件限制设计的特化版本。偏特化有两种形式:部分特化和参数更进一步的限制。

部分特化

部分特化是将模板参数类表中的一部分参数进行特化,如下:

//在以上类中,只将T2进行特化,T2在模板类型列表中直接省略,跟函数特化原理一样
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};

参数更进一步的限制

参数更进一步限制是针对模板参数更进一步的条件限制所设计出来的一个特化版本。样例如下:

#include <iostream>
using namespace std;
//原模版
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为指针类型,对其进行限制,下面同理
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
//这里的_d1和_d2为T1型和T2型,不是T1*和T2*
T1 _d1;
T2 _d2;
};
//T2被特化为double类型,T1被特化为T1*类型
template <class T1>
class Data <T1*, double>
{
public:
Data() { cout << "Data<T1*, double>" << endl; }
private:
T1* _d1;
double _d2;
};
int main()
{
Data<double, int> d1; //输出Data<T1, T2>
Data<int*, double> d2; //输出Data<T1*, double>
Data<int*, int*> d3; //输出Data<T1*, T2*>
return 0;
}

这里需注意,这里将参数更进一步限制需要写上模板参数列表中的类型参数,因为这里改变其类型特征时需要类型模板列表中的类型。

模板的特化本质是一种参数的匹配。模板特化时,只要满足特化的类型就会往特化走,不满足的话走原模板。匹配的顺序为 " 全特化->偏特化->原模版 "

偏特化的使用我们需注意复杂类型的情况,请看以下问题:

template <class T>
class compare
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
//偏特化
template <class T>
class less<T*>
{
public:
bool operator()(const T*& x, const T*& y) //注意:这里的const修饰的是T*,即非const引用修饰T*类型。
{
return *x > *y;
}
};

当T类型传入类进行比较时,由于这里引用是非类型,当类中发生隐式类型转换时系统会产生临时变量,而临时变量具有常性,非const修饰会导致编译出错。

换句话说,一定要注意const修饰的类型,其它运用同理。而这里要想解决以上问题时,我们需使用const型引用,即这里应将const T*& xconst T*& y 改为const T* const & xconst T* const & y,即在 &(引用) 符号前加上const修饰。

最后,要说明的是,无论是函数模板特化还是类模板特化,它们都是在原模版基础上进行的。C++不允许没有原模板直接使用特化。


三,模板分离编译

一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件(obj文件),最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。模板的声明与定义分离开实现称为模板的分离编译。

C/C++程序要运行,一般要经历一下步骤:预处理->编译->汇编->链接

这里,重点说明编译和链接两个阶段。在编译阶段,系统会对程序按照语言特性进行词法、语法、语义分析,错误检查无误后才能生成汇编代码。这里注意,头文件可不参与编译,编译器对工程中的多个源文件吧是分离开单独编译的。在链接阶段,系统会将每个cpp经汇编形成的obj(目标文件或中间文件)文件和资源文件经链接成为可执行文件。其中,obj文件只给出了程序的相对地址,而EXE是绝对地址。

下面,我们观察模板分离编译会出现的问题。这里,我们简单实现一个加法模板函数来举例说明。

//在头文件test.h中声明
template <class T>
T Add(const T& x, const T& y);

//在test1.cpp文件中定义
#include "test.h"
template <class T>
T Add(const T& x, const T& y) {
return x + y;
}

//在test2.cpp文件中调用
#include "test.h"
int main()
{
Add(1, 5);
return 0;
}

在test1.cpp编译时,系统进行到编译阶段没有看到对Add函数模板的实例化,因此不会生成具体的加法函数。

在test2.cpp编译时,编译器调用Add<int>,但问题是编译器在链接之前对项目的每个源文件都是单独编译的,编译器不会自动去其它源文件中查找,因为如果源文件过多的话会导致编译速度过慢。也就是说编译器对每个源文件执行到链接时才会找其地址,但Add函数没有在编译阶段实例化生成具体代码,因此编译器会在链接时报错。

此问题的根源出在编译阶段。模板在编译阶段就要确定类型,否则在内部不会生成具体的函数**(模板实例化后内部将生成具体函数,即在编译器编译阶段,编译器根据传入的实参类型来推演生成对应类型的函数以供调用)**,也就导致链接时会出错。

解决方法

要想解决这类问题就要想办法在编译阶段让模板确定具体类型,这里有以下两种方法可借鉴。

1,模板定义的位置进行显示实例化,编辑阶段会直接实例化为具体定义的类型。但这种方法局限性太高,只能适用于一种类型,并不适用,所以不推荐使用。如下:

//在test1.cpp文件中定义
#include "test.h"
template <class T>
T Add(const T& x, const T& y) {
return x + y;
}
//直接显示实例化,但局限性很高,只能传入int型,传入其它类型将报错
template
int Add<int>(const int& x, const int& y);

//在test2.cpp文件中调用
#include "test.h"
int main()
{
Add(1, 5); //调用正确
Add(1.2, 1.6); //浮点型报错
return 0;
}

2,直接将声明和定义放到一个文件中。当主程序文件调用时,在预编译阶段会将此文件展开,编译阶段即可确定类型。如下:

//在test1.cpp文件中声明定义
//声明
template <class T>
T Add(const T& x, const T& y);
//定义
template <class T>
T Add(const T& x, const T& y) {
return x + y;
}

//在test2.cpp文件中调用
#include "test1.cpp"
int main()
{
Add(1, 5); //调用正确
Add(1.2, 1.6); //调用正确
return 0;
}

总而言之,模板可以声明与定义的分离,但必须保证在编译阶段要确定模板的具体类型,显式实例化为其中的一种,但这种方法可以说没意义,局限性太高。

相关推荐
菜鸟学Python4 分钟前
Python 数据分析核心库大全!
开发语言·python·数据挖掘·数据分析
C++忠实粉丝5 分钟前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
一个小坑货11 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2716 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
古月居GYH16 分钟前
在C++上实现反射用法
java·开发语言·c++
Betty’s Sweet19 分钟前
[C++]:IO流
c++·文件·fstream·sstream·iostream
敲上瘾33 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
不会写代码的ys39 分钟前
【类与对象】--对象之舞,类之华章,共绘C++之美
c++
兵哥工控41 分钟前
MFC工控项目实例三十二模拟量校正值添加修改删除
c++·mfc
在下不上天41 分钟前
Flume日志采集系统的部署,实现flume负载均衡,flume故障恢复
大数据·开发语言·python