C++ 模版进阶

目录

前言

1. 非类型模版参数

1.1 概念与讲解

1.2  array容器

2. 模版的特化

2.1 概念

2.2 函数模版特化

2.3 类模版特化

2.3.1 全特化

2.3.2 偏特化

3.模版的编译分离

3.1 什么是分离编译

3.2 模版的分离编译

3.3 解决方法

4. 模版总结

总结


前言

本篇文章主要讲解的是模版进阶的内容,其中有模版更深入的应用,内容丰富,干货多多!


1. 非类型模版参数

1.1 概念与讲解

模版参数分类类型形参非类型形参

类型形参即,出现在模版参数列表中,跟在class或者typename后面的参数类型名称。

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

我们来看下面的场景。

  • 我们要完成一个静态的栈,一般可以使用宏来定义N,需要存储多少数据,修改宏的大小即可。如果在main函数中我们想让第一个栈存储10个数据,第二个栈存储100数据。
  • 此时,宏的弊端就体现出来。因为N只能表示一个数值,为了满足上面的要求,将N定义为100。可是第一个栈只需要存储10个数据,这样就会造成空间上的浪费。
#define N 100

//静态的栈
template<class T>
class Stack
{
private:
	T a[N];
	int top;
};

int main()
{
	Stack<int> st1;  //10
	Stack<int> st2;  //100

	return 0;
}

为了解决上面出现的问题,我们可以使用非类型模版参数。

//静态的栈
template<class T, size_t N>
class Stack
{
private:
	T a[N];
	int top;
};

int main()
{
	Stack<int, 10> st1;  //10
	Stack<int, 100> st2;  //100

	return 0;
}

  • 非类型模版参数还可以给缺省值,类似于函数参数。
  • 非类型模版参数是个常量,不可以修改。
//静态的栈
template<class T, size_t N = 10>
class Stack
{
public:
	void func()
	{
		N++;//不可以修改N
	}

private:
	T a[N];
	int top;
};

int main()
{
	Stack<int> st1;  //10
	Stack<int, 100> st2;  //100
    
    st1.func();//会报错
	return 0;
}

  • C++20之前的版本只允许整型类型做非类型模版参数。
  • C++20之后的版本支持所有内置类型做非类型模版参数,但是不支持自定义类型参数做非类型模版参数
template<double X, string str>
class Unkonwn
{};

int main()
{
	Unkonwn<1.1, "xxxxx"> un;
	return 0;
}

1.2  array容器

非类型模版参数既然可以解决一些场景下的问题,在STL中的容器有没有使用的呢?array容器就是用非类型模版参数。相当于一个定长数组,进行了封装。

#include <array>
int main()
{
	array<int, 10> aa1;
	cout << sizeof(aa1) << endl;

	return 0;
}

相比于之前的数组,array容器有什么优势呢?

  • 之前的定长数组,检查越界方面使用的是抽查机制,即检查超出数组下标一两位内存空间是否发生改变,如果超出数组下标太多,可能检查不到,并且检查的成本很大。只有你进行写的操作可以检查出来,如果进行读操作,打印出来时,无法检查出来。
  • array容器是自定义类型,下标方括号访问是运算符重载,可以对方括号内的参数进行限制,不管是写还是读操作,都能检查出来。
#include <array>
int main()
{
    //严格的越界检查
	array<int, 10> aa1;
    aa1[10] = 10;
    cout << aa1[11] << endl;

	int aa2[10];
    aa2[10] = 10; //大多数编译器检查的出来
	aa2[14] = 10; //一般的编译器检查不出来,除非是新版的
    cout << aa2[15] << endl; //检查不出来

	return 0;
}

2. 模版的特化

2.1 概念

通常情况下,使用模版可以实现一些与类型无关的代码,但对于一些特殊类型的肯呢个会得到一些错误的结果,需要进行特殊处理。如下面的场景,写一个用来进行小于比较的函数模版。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}

	bool operator<(const Date& d) const
	{
		if (_year < d._year)
			return true;
		else
			if (_year == d._year)
				if (_month < d._month)
					return true;
				else
					if (_month == d._month)
						return _day < d._day;
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

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

	Date d1(2024, 7, 8);
	Date d2(2024, 7, 5);
	cout << Less(d1, d2) << endl; // 可以比较

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

运行结果如下:

如上述代码所示,Less函数使用模版后对内置类型和自定义类型都可以进行比较操作,但是Date*日期指针类型返回的结果是跟Date类型结果不同,因为p1表示d1日期类变量的地址,p2表示d2日期类变量的地址,只有对p1和p2进行解引用时,才是指向d1和d2两个变量的内容,所以指针相关类型比较特殊。

在原模版类的基础上,针对特殊类型所进行特殊的实现方式。模版特化中分为函数模版特化类模版特化

2.2 函数模版特化

函数模版特化要求:

  1. 必须要先有一个基础的函数模版。
  2. 关键字template后面接一对空的尖括号<>。
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型。
  4. 函数形参必须要和原模版函数的基础参数类型完全相同,需要注意的是,如果不同编译器可能会报一些奇怪的错误

一开始用Less函数举例子,指出对于指针类型无法比较,我们可以使用函数模版来解决。

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

int main()
{
	cout << Less(1, 2) << endl;

	Date d1(2024, 7, 8);
	Date d2(2024, 7, 5);
	cout << Less(d1, d2) << endl; 

	Date* p1 = &d1;
	Date* p2 = &d2;
	cout << Less(p1, p2) << endl; //调用特化函数模版,进行比较
	return 0;
}

运行结果如下:

不过使用模版函数会有许多坑。我们一般对函数使用模版时,函数参数会对模版使用引用,如果这个函数不进行修改操作,会在前面加上const修饰,防止该变量被修改。

  • 如下面代码所示,一般人写函数模版的特化,会写成第一种。
  • 这是错误的,因为T此时是Date*指针类型,const T& left中的const修饰的是left,且left本身存储的是变量的地址,所以是指针变量本身不能改变。如果写成第一种const加在Date*前,修饰的是指针指向的内容,跟原函数模版的函数参数类型不同,编译时会报错。
  • 第二种函数模版的特化才是正确的写法,const放在*符号之后,表示修饰left存储变量的地址。
// 函数模板 -- 参数匹配
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

//1.
template<>
bool Less<Date*>(const Date* & left, const Date* & right)
{
	return *left < *right;
}

//2.
template<>
bool Less<Date*>(Date* const & left, Date* const & right)
{
	return *left < *right;
}

2.3 类模版特化

类模版的特化分为全特化偏特化

2.3.1 全特化

全特化即是将模版参数列表中所有的参数都确定化。其中的格式跟函数模版特化类似

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; 
	}
};

void Test1()
{
	Data<int, double> d1; //走第一个构造函数
	Data<int, char> d2; //走特化的构造函数
}

运行结果如下:

2.3.2 偏特化

偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。

  • 部分特化:将模版参数表中的一部分参数特化。
//特化第二个参数int
template<class T1>
class Data<T1, int>
{
public:
	Data()
	{
		cout << "Data<T1, int> -偏特化" << endl;
	}
};

  • 进一步限制参数类型。偏特化并不仅仅是指特化部分参数,而是针对模版参数更进一步的条件限制所设计出来的一个特化版本。
// 限定模版类型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << "Data<T1*, T2*> -偏特化" << endl;
	}
};

void Test2()
{
	Data<int, double> d1;
	Data<int, char> d2;
	Data<int, int> d3;
	Data<int*, double*> d4;
	Data<int**, char*> d5;
}

运行结果如下:

再看下面的代码,先对第二种偏特化的构造函数做一些修改,其中typeid(T1).name( )是获取T1的类型名称,下面一条也是。

// 限定模版类型
template<typename T1, typename T2>
class Data<T1*, T2*>
{
public:
	Data()
	{
		cout << typeid(T1).name() << endl;
		cout << typeid(T2).name() << endl;
		cout << "Data<T1*, T2*> -偏特化" << endl;
	}
};

void Test3()
{
	Data<int*, double*> d4;
	Data<int**, char*> d5;
}

运行结果如下,T1和T2分表是原来*符号前的类型,而不是原来的指针类型。

3.模版的编译分离

3.1 什么是分离编译

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

3.2 模版的分离编译

下面的代码是模版函数和普通函数分离编译的代码,看看运行的结果如何。建立两个文件,分别是Func.h和Func.cpp。

  • Func.h文件存放函数的声明。
//Func.h

//模版函数
template<class T>
T Add(const T& left, const T& right);

//普通函数
void func();
  • Func.cpp文件存放函数的定义。
//Func.cpp
#include "Func.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;
}

//普通函数
void func()
{
	cout << "void func()" << endl;
}

写两个测试函数,运行程序看结果。

//Test.cpp
#include "Func.h"
void test1()
{
	Add(1, 2);     
	Add(1.0, 2.0); 
}

void test2()
{
	func();
}

int main()
{
    test1();
    test2();
    return 0;
}

运行test1函数,报连接错误,说有无法解析的外部符号。

运行test2函数,结果如下,没有问题。

普通函数分离编译可以正常运行,模版函数分离编译后运行会报链接错误,这是为什么呢?

  • C/C++程序要运行起来,都要经过这几个步骤:预处理—>编译—>汇编—>链接
  • 在预处理阶段,所有的.h文件都要在包含的位置展开。在编译过程中,将Func.cpp和Test.cpp文件编译成目标文件,分别是Func.o和Test.o。
  • 并且会生成一个符号表,符号表中有函数名经过不同平台规则的变化成新的名称,并且会在函数的定义中,存放函数地址,方便链接。
  • 但是模版函数在定义的部分,不知道要使用什么类型实例化,就不会将Add函数的地址存放在符号表中,那么就会出现上面报的连接错误,无法解析的符号。

 

 

3.3 解决方法

有两种解决方法:

  1. 将声明和定义放在同一个.hpp或者.h文件中
  2. 在模版函数定义的位置显示实例化。

如下面的代码所示,不过这种方法很少用。因为当你使用到这个函数其他类型的话,还需要再添加,十分麻烦。

#include "Func.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;
}

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

template
double Add(const double& left, const double& right);

4. 模版总结

模版的优点:

  1. 模版复用代码,节省资源,提高开发效率。
  2. 增强代码的灵活性。

缺点:

  1. 模版会导致代码膨胀问题,使得编译时间变长。
  2. 出现模版编译错误时,错误信息非常凌乱,难以定位错误进行纠正。


总结

通过这篇文章,对于模版的使用有了更深入的了解,如果还有某些地方不够熟悉,可以自己动手敲敲代码。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值