C++模板进阶 非类型模板参数 模板的特化 分离编译的深入探索

🔥个人主页:爱和冰阔乐

📚专栏传送门:《数据结构与算法》C++

🐶学习方向:C++方向学习爱好者

⭐人生格言:得知坦然 ,失之淡然


🏠博主简介

文章目录

  • 前言
  • 一、非类型模板参数
  • 二、模板的特化
    • 2.1 概念
    • 2.2 函数模板的特化
    • 2.3 类模板特化
    • 2.4 类模板特化应用
  • 三、模板分离编译
    • 3.1 分离编译是什么
    • 3.2 模板的分离编译
  • 总结

前言

在学完模板初阶时埋下的伏笔------模板不可分离它来了,它来了!我们将深入了解模板参数分为哪些类型,函数模板特化,以及通过了解编译原理来探究模板不可分离的底层原因!!!

一、非类型模板参数

模板参数分为类型形参非类型形参,而类型形参在模板初阶便有所介绍

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

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

如下代码的N便就是非类型形参,N是常量(模板在编译的时就实例化,即N在编译时确定是常量)

cpp 复制代码
template<size_t N>
class stack
{
private:
	int _a[N];
	int _top;
};

那么这与在C语言阶段使用的宏的区别就是,如下代码,希望s1存5个数据,s2存10个数据,但由于宏是写死的,s1和s2只能为5,只能将N修改为10,给s1多开五个空间(有空间牺牲)

cpp 复制代码
#define N 5
class stack
{
private:
	int _a[N];
	int _top;
};

int main()
{
	//希望s1存5个数据,s2存10个数据,宏做不到
	stack s1;
	stack s2;
}

因此我们发现宏给我们带来不便,C++的非类型形参便解决了该问题,实现s1和s2传各自大小空间,本质是模板在编译时生成了两个类,分别实例化s1和s2

cpp 复制代码
template <size_t N>
class stack
{
  private:
  int _a[N];
  int _top;
    
};
int main()
{
    stack<5> s1;
    stack<10> s2;
}

非类型模板参数也可以给缺省值,但是需要注意,在C++14如果不传数据则会报错(stack<> s3才不会报错),但是在C++20及其之后可以不传
由于新语法有向前兼容的规定,默认给了缺省值不传参数可以:stack<> s3保证不会出错

cpp 复制代码
template<size_t N=10>
class stack
{
private:
	int _a[N];
	int _top;

};

int main()
{

	//希望s1存5个数据,s2存10个数据,宏做不到
	stack<5> s1;
	stack<10> s2;
	//报错
	//stack s3;
	stack<> s3;
}

注意:

1.浮点数、类对象以及字符串是不允许作为非类型模板参数的(只能用于整型,bool char也算整型)

  1. 非类型的模板参数必须在编译期就能确认结果。
    C++20才支持double类型的非类型模板参数

二、模板的特化

2.1 概念

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

cpp 复制代码
//函数模板------参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}

int main()
{
	cout << Less(1, 2) << endl;//可以比较,结果正确

	Date d1(2022, 7, 7);
	Date d2(2022, 7, 8);
	cout << Less(d1, d2) << endl; // 可以比较,结果正确

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl;// 可以比较,结果错误
	return 0;
}

可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误

此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

2.2 函数模板的特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
cpp 复制代码
template<class T>
bool Less(T left, T right)
{
 return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
 return *left < *right;
}
int main()
{
 cout << Less(1, 2) << endl;
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 8);
 cout << Less(d1, d2) << endl;
 Date* p1 = &d1;
 Date* p2 = &d2;
 cout << Less(p1, p2) << endl;  // 调用特化之后的版本,而不走模板生成了
 return 0;
}

可以这样理解函数模板的特化,首先需要先有个可以涵盖一般情况下的方法,然后在该方法上需要对特殊条件进行处理,也就是先有了基本的函数模板,再对特殊条件如Date*进行特殊处理
推荐:但是我们对函数模板特化更倾向不用特化,对于Date*是现成的函数,先考虑现成的,其次考虑模板(模板需要实例化)

cpp 复制代码
template <class T>
bool less(T left,T right)
{
   return left<right;
}
bool less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

下面我们来看下这段代码是否有问题:

cpp 复制代码
template<class T>
bool less(const T& left,const T& right)
{
   return left<right;
}

template<>
//这里便会导致指向的内容不能被修改,那么无法解引用
bool less<Date*>(const Date*& left, const Date*& right)
{
	return *left < *right;
}

我们发现在传const引用时,函数模板特化报错,这里代码看起来似乎没毛病?

我们仔细看下,在函数模板中,const修饰的是left,而在函数模板特化中const修饰的是 * left(const在 * 之前修饰的是内容,在 之后修饰的是指针本身 ,引用在const前后均一样

const修饰普通类型,前后无影响

cpp 复制代码
//结果均一样
const int i=0;
int const j=0;
//结果一样
const int& rx=i;
int const &ry=i;

因此我们需要如下写法

cpp 复制代码
template<>
//指向的指针本身不能被修改
bool less<Date*>(Date* const & left,  Date* const & right)
{
	return *left < *right;
}

但是如果我们再穿个对象如下:

cpp 复制代码
int main()
{
  const Date*p1=&d1;
  const Date*p2=&d2;
cout<<less(p1,p2)<<endl;  
  

}

我们发现结果又不对了,这也是为什么不推荐写函数模板特化的原因,有时候稍微不注意便进了大坑

总结·:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

cpp 复制代码
bool Less(Date* left, Date* right)
{
 return *left < *right;
}

该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化

2.3 类模板特化

类模板特化分为:全特化,偏特化(半特化)

全特化:全特化即是将模板参数列表中所有的参数都确定化。

cpp 复制代码
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;
};

int main()
{
    Data<int, int> d1;
	Data<int, char> d2;
}

d1匹配原模板,d2匹配全特化版本

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:

cpp 复制代码
//偏特化/半特化
//特化T2,T1不特化,无论T1是什么类型,如果T2是double便走该特化
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Data<int, double>" << endl; }
private:
	int _d1;
	char _d2;
};
int main()
{
  Data<int, int> d1;
  Data<int, char> d2;
  Data<int,double> d3;
}

如果实例化的对象既满足全特化也满足偏特化,编译器会选择谁?

cpp 复制代码
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;
};

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

特化的本质是把某些模板参数具体化,写成具体类型

这里我们便看出编译器选择全特化(编译器更愿意选择具体的参数更多的)
偏特化除了上面的表现方式为部分特化(将模板参数类表中的一部分参数特化),还可以对参数进行更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

如下,传的类型是指针也是偏特化,只要传指针,就走该函数

cpp 复制代码
//偏特化,传的类型是指针两个参数偏特化为指针类型 
template <class T1, class T2>
class Data <T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }

private:
	T1 _d1;
	T2 _d2;
};

int main()
{
 Data<char*,char*> d6;
 Data<int*,char*> d7;

  return 0;
}

除了指针还可以特化引用

cpp 复制代码
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
	Data()		
	{
		cout << "Data<T1&, T2&>" << endl;
	}
};

那么当然指针和引用一起特化也是可以的

cpp 复制代码
template <typename T1, typename T2>
class Data <T1&, T2*>
{
public:
	Data()		
	{
		cout << "Data<T1&, T2*>" << endl;
	}
};

2.4 类模板特化应用

有如下专门用来按照小于比较的类模板Less:

cpp 复制代码
#include<vector>
#include<algorithm>
template<class T>
struct Less
{
 bool operator()(const T& x, const T& y) const
 {
 return x < y;
 }
};
int main()
{
	 Date d1(2022, 7, 7);
	 Date d2(2022, 7, 6);
	 Date d3(2022, 7, 8);
	 
	 vector<Date> v1;
	 
	 v1.push_back(d1);
	 v1.push_back(d2);
	 v1.push_back(d3);
	 // 可以直接排序,结果是日期升序
	 sort(v1.begin(), v1.end(), Less<Date>());
	 vector<Date*> v2;
	 v2.push_back(&d1);
	 v2.push_back(&d2);
	 v2.push_back(&d3);
 
	 // 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
	 // 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
	 // 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
	 sort(v2.begin(), v2.end(), Less<Date*>());
 return 0;
}

通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。因为:sort最终按照Less模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:

cpp 复制代码
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
 bool operator()(Date* x, Date* y) const
 {
 return *x < *y;
 }
};

再入在前一章节的栈和队列中,可以传不同类型的指针

cpp 复制代码
hxx::priority_queue<int*> q1;
q1.push(new Data(2025,10,30));
q1.push(new Data(2025,10,16));
q1.push(new Data(2025,9,30));
q1.push(new Data(2025,7,6));

cout<<*q2.top()<<endl;

这里我们打印结果发现似曾相识,这不和栈和队列章节中的优先级队列一样吗,由于指针每次申请的地址大小都是随机的,因此每次打印的top都不一样,因此我们便可以通过偏特化实现(只要传的是指针,那么便按指向的内容去比较)

cpp 复制代码
template<class T>
class Less<T*>
{
public:
 bool operator()(T* const& x,T* const& y)
 {
   return *x<*y;
 }
}

除此之外,我们还需注意一个小点,请问T1是什么类型

cpp 复制代码
template<typename T1,typename T2>
class Data<T1*,T2*>
{
public:
 Data()
 {
   cout<<"Data<T1*,T2*>"<<endl;
   T1 x;
   cout<<typeid(x).name()<<endl;
 }
};

三、模板分离编译

3.1 分离编译是什么

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

3.2 模板的分离编译

假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

cpp 复制代码
// Func.h
template<class T>
T Add(const T& left, const T& right);
// Func.cpp
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}
// main.cpp
#include"a.h"
int main()
{
 Add(1, 2);
 Add(1.0, 2.0);
 
 return 0;
}

模板分离编译会报错

那么普通函数的申明与定义分离是否会报错?

cpp 复制代码
//Func.h
void func(const int& left, const int& right);
//Func.cpp
void func(const int& left, const int& right)
{
	cout << "void func(const int& left, const int& right)" << endl;
}
//test.cpp
int main()
{
func(1,2);
return 0;
}

要想知道为什么模板会报链接错误,那么我们就需要明白编译和链接的过程

编译会分为几个过程:

1 . 预处理执行的操作:1.将头文件展开 2.宏替换 3.条件编译 4.去掉注释 ... ... ...

在这里.h被展开后,就生成对应的Func.i / test.i

2.编译:

检查语法,生成汇编代码

在这里就把Func./test.i生成对应的Func.s/test.s

3.汇编

将汇编代码转换成二进制机器码

在这里会生成对应的文件Func.o/test.o(或者是.obj)

编译过程完成后便是链接了,链接是把目标文件合并在一起生成可执行程序(Windows是xxx.exe/Linux是a.out),并且把需要的函数地址等链接上去

注意:在链接前,各自文件不进行交互,只在链接进行交互

因此我们便知道,模板没有实例化时在编译中是不会出现具体的代码的,因此在main.cpp中无法找到Add函数的具体代码,导致链接时报错,那么是否存在一种方式支持模板的分离定义?

那么显示实例化就该登场了!!!

cpp 复制代码
template<class T >
T Add(const T& left, const T& right)
{

	cout << "T Add(const T& left, const T& right)" << endl;
	return left + right;
	 
}

//显示实例化
template
int Add(const int& left,const int& right);

最终编译成功!但是每次传参数时都需要显示实例化,过于复杂,那么我们可以再Func.h中直接定义

cpp 复制代码
//直接在.h定义
template<class T>
T Add(const T& left, const T& right)
{
	cout << "T Add(const T& left, const T& right) " << endl;

	return left + right;
}

总结

文章到此便结束了,学完模板的初阶与进阶,我们需要知道

模板的优点:

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生

  2. 增强了代码的灵活性
    模板的缺点:

  3. 模板会导致代码膨胀问题,也会导致编译时间变长

  4. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

资源分享:gitee模板进阶源码

C++模板初阶

坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了解模板!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆

相关推荐
海梨花3 小时前
今日八股——JVM篇
jvm·后端·面试
charlie1145141919 小时前
精读C++20设计模式:行为型设计模式:中介者模式
c++·学习·设计模式·c++20·中介者模式
楼田莉子9 小时前
Qt开发学习——QtCreator深度介绍/程序运行/开发规范/对象树
开发语言·前端·c++·qt·学习
oioihoii9 小时前
超越 std::unique_ptr:探讨自定义删除器的真正力量
c++
Gohldg9 小时前
C++算法·贪心例题讲解
c++·数学·算法·贪心算法
天若有情67310 小时前
C++空值初始化利器:empty.h使用指南
开发语言·c++
远远远远子10 小时前
类与对象 --1
开发语言·c++·算法
无敌最俊朗@10 小时前
C/C++ 关键关键字面试指南 (const, static, volatile, explicit)
c语言·开发语言·c++·面试
利刃大大11 小时前
【高并发服务器】三、正则表达式的使用
服务器·c++·正则表达式·项目