C++ -- 模板进阶详解

目录

非类型模板参数

模板的特化

类模板实例化

全特化

 偏特化

模板的分离编译

什么是分离编译

模板的分离编译

模板总结


非类型模板参数

当一个类,比如说栈类,我们要实现一个int的栈或者double的栈,只需要传一个参数模板,就可以解决。如果在这个栈的成员变量中,我们规定要写一个N个大小的数组,在C语言中,如果让定义的数字是个具体数字,那么我们能用到宏或枚举。如果要改变数值,只能改变宏或者给枚举增加数字。当实例化这个类,想要这个数组大小是可变的,可能传100,也可能传200。类型可以传模板,那么数字怎也像类型一样呢?

#define N 100
template<class T>
class MyStack
{
public:
	void Push(const T& x)
	{}
private:
	T _a[N];
	size_t _top;
};

int main()
{
	MyStack<int> st1;//100
	MyStack<int> st2;//200

	return 0;
}

这个就是非类型模板参数,它是一个常量,不是变量。而且它必须是一个整型常量,其它类型是报错的

非类型参数也可以给缺省值,但必须从右往左给缺省值,并且缺省值必须连续的。

template<class T,size_t N=100>//改变上面的模板

int main()
{
	MyStack<int,100> st1;//100
	MyStack<int,200> st2;//200

	return 0;
}

模板参数分:类型参数和非而理性参数

  • 类型参数:出现在模板参数列表总,跟在class或typename之类的参数类型名称
  • 非类型参数:就是用一个常量作为类(函数)模板的一个参数,在类模板中可将该参数当成常量来使用。

模板的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果

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

	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}

	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
	friend ostream& operator<<(ostream& _cout, const Date& d);

private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return _cout;
}


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


int main()
{
	cout << ObjLess(1, 2) << endl;
	Date* p1 = new Date(2022, 3, 26);
	Date* p2 = new Date(2022, 4, 26);
	cout << ObjLess(p1, p2) << endl;


	return 0;
}

对于这段代码有两种结果,因为他们是比地址,p1,p2是指针类型,指向Date类型的两个地址。

 而我们想要的是比指针指向的内容,需要解引用他们,第一种方法,是重载一个函数,显示实例化里面的类模板。第二种方法用到函数模板的特化,他会在函数名后面加上一个类型,然后调用这个类型来传参。这个函数的特化即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

但是对于以下两种写法,函数模板的特化还不如直接写一个固定类型的重载函数,而且在测试当中,两种函数如果同时出现,会先去调用方法一的函数,特化也不会优先调用

//方法一
bool ObjLess(Date* left, Date* right)
{
	return *left < *right;
}

//专用化函数--这个方法很鸡肋
//特化--特殊化,比如需要针对某些类型特殊化处理
template<>
bool ObjLess<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

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

类模板实例化

全特化

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

形式跟函数模板偏特化类似,也是类模板中不写参数,直接在类名后面加上具体类名,这样实例化对象如果与特化实例匹配上了,直接调这个特化实例,因为有现成的不会去调模板。

template<class T1,class T2>
class Date
{
public:
	Date()
	{
		cout << "Date<T1,T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

template<>
class Date<int, char>
{
public:
	Date()
	{
		cout << "Date<int,char>" << endl;
	}
private:
	int _d1;
	char _d2;
};

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

	return 0;
}

在我们上节写的优先级队列中,其中的仿函数就可以用到类模板特化

	template<class T>
	struct Less
	{
		bool operator()(const T& x, const T& y) const
		{
			return x < y;
		}
	};

    //类模板的全特化
	template<>
	struct Less<Date*>
	{
		bool operator()(Date* x, Date* y) const
		{
			return *x < *y;
		}
	};

调试成功

这就是特化的场景,看似是调的一样的结构,但是实际上是我对Less类里面进程特化的场景。所以如果传这种特定的类型Date*,无法直接使用类里面给的函数,第一我们可以再写一个类,像Less和Greater这样,再定义一个类名用来单独表示Date*类型。这样实例化调用直接调用这个类名即可。第二就是在原有类的基础上,写一个特化类模板,只要遇到Date*类型,系统就会自动匹配特化版本类型

 偏特化

偏特化有两种,一种是像缺省值一样,只有一部分用到了特化类型,还有一部分用到的还是模板类型。为什么不叫半特化是因为第二种情况。第二种情况是所有类型都给了模板类型,但是他们并不是真正的模板参数类型,像T类型的模板,模板参数给了T*,这样的也叫做偏特化。

偏特化也就是任何针对模板参数进一步条件限制设计的特化版本。它之所以是说限制是因为,像第二个偏特化版本,只要是指针类型的都会走到第二个版本的偏特化类模板中。所以偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

template<class T1,class T2>
class Data
{
public:
	Data()
	{
		cout << "Data<T1,T2>" << endl;
	}
private:
	T1 _d1;
	T2 _d2;
};

//偏特化
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;
	}
};

int main()
{
	Data<int, int> d1;//无特化
	Data<int, char> d2;//全特化

	Data<char, char> d3;//偏特化
	Data<double, char> d4;//偏特化

	Data<double*, char*> d5;
	Data<double*, char&> d6;

	return 0;
}

 还有一个问题,非类型模板参数能不能用特化,我们来验证一下。

template<size_t N>
class A
{
public:
	A()
	{
		cout << "A<N>" << endl;
	}
};
template<>
class A<10>
{
public:
	A()
	{
		cout << "A<10>" << endl;
	}
};

 在这里,非类型模板参数也可以特化。

模板的分离编译

什么是分离编译

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

声明和定义分离的优势是方便维护,xxx.h文件方便了解框架设计和基本功能,看xxx.cpp了解具体实现细节。

如果怕重复编译文件,第一种,我们会在xxx.h文件头部写上#pragma once,这个代表只能编译一次。第二种,是采用#ifndef/ #endif这种条件编译,在接口文件写上,防止重复编译。

模板的分离编译

.cpp是定义,.h是声明

 通过这两段代码分析可以看到,模板的分离编译会报错,这时为什么呢?

一段代码的执行需要进行以下步骤:

1.预处理 -- 头文件展开、条件编译、宏替换、去掉注释......

        生成vector.i test.i文件

2.编译 --检查语法问题,没有问题,生成汇编代码

        生成vector.s test.s文件

3.汇编 --把汇编代码转成二进制机器码

        生成vector.o test.o文件

4.链接

那么F1函数是怎样编译的呢,可以转到反汇编看到,如果里面还有一个语句++n,假如函数地址是0x12345678

首先建立栈帧指令,将n移动到寄存器eax,在eax中+1,再将eax中的值移动到n中,最后cout打印,跳转到了 0x12345678这个地址的函数

而F2的代码是不做处理的,因为它的类模板没有实例化,比如在F2中,要定义一个T类型的数,这个数没有实例化,我们都不知道开多大的空间,无法编译。所以不处理,没有实例化,也没办法编译生成汇编代码。

template<class T>
void F2(const T& n)
{
	T a;
	cout << "F2(int T& n)" << endl;
}

这时候test.cpp怎么做呢?F1和F2中分别跳转到两个函数的定义,来找他们,他们是通过函数重载,传不同的值来找到不同的函数地址,这时候直到了传的值的类型,生成汇编,汇编生成机器认识的二进制编码之后,将各个文件链接,通过符号表找对应的函数,因为他们事先不知道函数的定义在哪里,会在后面放一个问号,直到找到了,就放入函数的地址。

那么有人问,F2已经直到它的参数是int类型,为什么在vector.cpp文件中,还是无法实例化?这是因为他们只会按照编译的顺序来编译文件,在模板函数当前文件,链接之前找不到模板类型,在编译检查语法错误时,函数模板已经编译失败,找不到地址了。那么为什么不能在编译的时候就链接呢?因为这样编译就成本太高,一个项目里面每编译一次都要连接一下,这样太过复杂,还不如直接编译最后链接直接从符号表里找地址。

解决方法

 1.显式实例化:在模板的文件中显式实例化,但是这样有一个缺陷,如果换成其他类型,如double,还要再写一份显式实例化,用一个就得显式实例化一个,非常麻烦。

 2.将模板定义和声明放一个文件

因为是声明文件,所以声明还是要写的。这样test.cpp本来就包了vector.h这个文件,直接就能找到函数地址。而F1这种声明定义分离的,在test.cpp文件中的F1找函数定义地址,后面是个问号,链接的时候才找得到。

声明和定义不分离,那么使用它的地方,头文件展开以后,直接就有模板定义和实例化,那么直接就可以填上函数调用的地址,不需要链接再去找

当然函数模板不能声明定义分离

指针可以指向一个类,但是实例化不能没有实体类

所以这样的话还是要在声明里面定义,或者在定义中显示实例化,基本都是采用声明和定义不分离,第一张图片。

模板总结

优点:

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

2.增强了代码灵活新

缺点:

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

        如果要一个模板类,类本来是100行,但是实例化10份后就变成1000行,会导致代码膨胀,虽然如果不用模板的话还是要写10份1000行,但是模板并没有本质上解决这个代码膨胀问题,它只是看起来很简洁。为什么代码膨胀不好,就比如说一个软件的下载,代码膨胀会导致可执行程序变大,编译时间长,下载时间也长,这也是模板代码膨胀的缺点之一。这实际上就是实例化过程的消耗

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值