C++模板进阶

继上文

非类型模板参数:

模板参数分类类型形参与非类型形参
  • 类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称
template<class T>
  • 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
template<size_t N=10>

在此,N是一个常量,在编译器编译的时候就确定了,比起宏:

#define N 10;

来说,因为宏其实是写死的,而对于非类型模板参数,使用非类型模板参数就可以灵活使用

以下示例:

#include<iostream>

using namespace std;
namespace rose
{
	template<class T, size_t N = 10>
	class Stack
	{
	private:
		int _a[N];
		int _top;
	};
}
int main()
{
	rose::Stack<int, 5> s1;
	rose::Stack<int,10> s2;
	return 0;
}

本质还是底层生成了两个类,一个生成的是N=5,一个生成的是N=10

但是:非类型模板只能用于“整型”('int' 'short int' 'long long' 'size_t' 'bool' 'char'.....),像浮点型(double)是不支持非类型模板参数,C++20后就支持浮点型作为非类型模板参数

我们通过以上代码,我们的非类型模板参数也可以传缺省值,不过我们应该注意:

namespace rose
{
	template<size_t N = 10>
	class Stack
	{
	private:
		int _a[N];
		int _top;
	};
}
int main()
{
	rose::Stack<> s0;
	rose::Stack s00;//只有C++20过后才可以怎么写

	return 0;
}

因为C++具有向前兼容的特点,所以我还是推荐使用前者<>; 

模板的特化:

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

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

日期类的代码:here

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
	return left < right;
}
int main()
{
	cout << Less(1, 2) << endl; // 可以比较,结果正确using namespace std;
	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 指针的地址,这就无法达到预期而错误。
此时,就 需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方 。模板特化中分为 函数模板特化 类模板特化
也就是说:

在C++中,模板是一种允许你编写通用代码的方式,这些代码可以用于不同的数据类型。然而,有时候你可能想要为特定的数据类型提供特定的实现。这就是模板特化发挥作用的地方。

模板特化是告诉编译器:“对于这个特定的类型(或类型组合),我想要一个不同的实现。” 这可以通过两种方式实现:类模板特化和函数模板特化。

函数模板的特化:

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

注意:

bool Less(T left, T right);

 严格来说,在这个代码中写T是不好的,如果是内置类型还好,但如果是自定义类型的话,这个地方就会调用拷贝构造,应该这么写:

bool Less(const T& left, const T& right);

那么,我们的函数模板特化就应该改为:(因为没有与改变后的模板进行完美的匹配)

那我们改成这样如何呢:

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

不过可惜了,他会报错!问题出在哪呢?出在了const身上:

  • 在模板中,const修饰的是引用本身(left和right);
  • 在函数模板特化中,const修饰的是的并不是left/right,修饰的是在*之前,修饰的是指向的内容,相当于是修饰*left/*right,而又template<Date*>(特化的是Date*)(是个指针,是要const的是本身,即指针本身)我们应该改成:
bool Less<Date*>(Date* const& left, Date* const& right)
{
	return *left < *right;
}

如果main函数中改为: 

const Date* p1 = &d1;
const Date* p2 = &d2;

对应的,我们的函数模板特化就应该写成:

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

函数模板特化的使用场景:

  1. 类型特定的行为:当你想要为特定类型提供不同的处理逻辑时。
  2. 性能优化:为特定类型提供优化的算法实现。
  3. 兼容性:确保函数与特定类型的库函数或操作兼容。

总之:不建议使用,坑比较多(指针与const的关系)

对于函数模板:(推荐)不使用特化的方式,可以使用函数的方式:
(有函数模板和现成的,优先用现成的)

bool Less(Date* left, Date* 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; }
private:
	int _d1;
	char _d2;
};
void TestVector()
{
	Data<int, int> d1;
	Data<int, char> d2;
}

使用全特化的注意事项

  • 全特化必须在模板的原始定义之外进行。
  • 全特化可以提高代码的灵活性和性能,但也可能增加代码的复杂性。
  • 全特化应该谨慎使用,只在确实需要为特定类型提供完全不同的行为时使用。
偏特化/半特化:
偏特化有以下两种表现方式:
  • 部分特化将模板参数类表中的一部分参数特化
//类模板的偏特化/半特化
template<class T1>
class Data<T1, char>
{
public:
	Data() { cout << "Data<T1, char>" << endl; }
private:
	T1 _d1;
	char _d2;
};
  • 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout<<"Data<T1&, T2&>" <<endl;
}
private:
const T1 & _d1;
const T2 & _d2;
};
void test2 ()
{
Data<double , int> d1; // 调用特化的int版本
Data<int , double> d2; // 调用基础的模板
Data<int *, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}

全杀,只要是指针/引用,不管是int还是double通通用(针对指针,引用的特殊处理)

注意事项

  • 偏特化必须在模板的原始定义之外进行。
  • 偏特化可以与全特化共存,但全特化会优先于偏特化。
  • 偏特化应该谨慎使用,以避免过度复杂化代码。
全特化与偏特化的区别:
  • 全特化:提供了一个完全定制的实现,覆盖了所有通用模板的实现。
  • 偏特化:提供了一个定制的实现,但只针对特定的参数组合,其他参数仍然使用通用模板的实现。

在C++中,如果同时存在函数模板的全特化和偏特化,编译器将根据模板参数匹配的具体情况来决定使用哪一个特化版本。但是,通常情况下,全特化(完整特化)具有更高的优先级,因为它提供了更具体的类型匹配。

优先级规则:

  1. 全特化:如果存在一个全特化的模板,它完全匹配了调用的类型参数,编译器将优先使用这个全特化版本。

  2. 偏特化:如果不存在完全匹配的全特化,编译器将查找偏特化版本。如果有多个偏特化版本,编译器将选择最具体的(即模板参数列表中特化数量最多的)偏特化版本。

  3. 通用模板:如果没有匹配的特化版本,编译器将回退到使用通用模板。

类模板特化应用示例:

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

#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 模板中方式比较,所以只会比较指针,而不是比较指针指向空间中内容,此时可以使用类版本特化来处理上述问题:
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
	bool operator()(Date* x, Date* y) const
	{
		return *x < *y;
	}
};

如果不是Date*而是int*时,我们为了更加方便,就可以:(偏特化)

// 对Less类模板按照指针方式特化
template<class T>
struct Less<T*>
{
	bool operator()(T* x, T* y) const
	{
		return *x < *y;
	}
};
特化之后,在运行上述代码,就可以得到正确的结果

模板分离编译

模板的分离编译(Separate Compilation with Templates)是C++中一种重要的编译策略,它允许模板的定义和实例化分开在不同的编译单元(Translation Units,简称TU)中进行。这种策略可以提高编译效率,减少编译时间,并允许更好的代码组织。

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

也就是说一般的类·函数都要声明和定义分离,但是模板就不支持,因为会导致链接链不上:

(声明放在.h 定义放在.cpp)

// a.h
template<class T>
T Add(const T& left, const T& right);
int Func(const int& left, const int& right);

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

	Func(1, 2);//call Func(?)
	return 0;
}

为什么会链接错误呢?

编译过程通常包括以下几个主要阶段:(Linux下)(a.h a.cpp Test.cpp)

  1. 预处理(Preprocessing):

    这个阶段处理源代码文件中的预处理指令,如宏定义(#define)(宏替换)、文件包含(#include)(其中有头文件展开(.h))、条件编译、去掉注释等。(a.h->a.i Test.cpp->Test.i)
  2. 编译(Compilation):

    编译器将预处理后的源代码转换成汇编语言。这个阶段涉及到语法分析、语义分析、中间代码生成等步骤。(语法错误的出处)(a.i->a.s Test.i->Test.s)
  3. 汇编(Assembly):

    将汇编语言转换成二进制的机器码。汇编器读取编译生成的汇编代码并生成目标文件(Object File),包含机器指令和符号表等信息。(符号·指令变为二进制的机器码)(a.s->a.o Test.s->Test.o)
  4. 链接(Linking):

    链接器将多个目标文件以及库文件链接在一起,生成可执行文件或库文件。链接过程解决外部符号引用问题,将分散在不同文件中的代码和数据整合到一起。(目标文件合并在一起生成可执行程序,并且把需要的函数地址等链接上)(xxx.exe/ a.out)

在链接之前,文件之间(Func Test)是没有交互的,在Test.s生成的时候,汇编语言就变成了

call Add(?)
call ADD(?)

call Func(?)

然后声明和定义分离的,要call对应的函数地址,而符号表就存了当前所有函数的对应的地址,链接不上是因为Func()找到了都对应的地址,而Add()去找的话,找不到,(因为在此之前(编译)有声明,所以在链接之前就让通过了)为什么找不到呢?

因为Add作为一个模板,并没有实例化(链接之前不互相交互),模板没有实例化是生成不了对应的指令的,也就是在符号表里找不到对应的Add(),因此,我们就可以使用显示实例化到达模板的声明和定义分离了:

//显式实例化
template//为了和特化进行区分,不加<>
int Add(const int& left, const int& right);
double Add(const double& left, const double& right);
//不过太龊了

所以,模板的解决方案就是在.h中直接定义(在编译时就有定义了,就直接实例化,就可以call到对应的地址了)

总结:

  • 【优点】

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

增强了代码的灵活性

  • 【缺陷】

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

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

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值