目录

前言

非类型模板参数

模板特化

函数模板特化

类模板特化

全特化

偏特化

部分特化

对参数的限制

分离编译问题

解决方法


前言

🍒以前,在 C++ 入门的时候我们曾说过模板的基础操作。简单讲了函数模板与类模板,今天我们来进一步学习模板的其他操作

非类型模板参数

🍒一个类中,我们定义了一个数组,数组的长度事先由一个常量决定,便可以简单封装一个限定大小的数组。

#define N 10

template<class T>
class arry
{
public:
	T arr[N];
};

int main()
{
	arry<int> a1;
	return 0;
}

🍒但是,若我们想以这个模板进行实例化,且一个数组长度为 10,另一个数组长度为 20 该怎么办呢?

🍒显然,此时一个 N 已经无法满足我们的使用需求了,但定义多个常量又显得代码十分冗余

🍒于是我们引入非类型模板参数来解决这个问题。

🍒我们在模板的参数列表后再增加一个参数,以整型的类型进行定义,这种参数实例化出来的实际数据为常量,因此可以用于对数组的定义。

template<class T,size_t N> 
class arry
{
public:
	T arr[N];
};

int main()
{
	arry<int, 10> a1;
	arry<int, 10> a2;
	return 0;
}

🍒值得注意的是, 非类型模板参数只能使用整型,浮点型是无法使用的。这个模板操作更多用于位图的开辟或是开辟静态数据结构。

🍒由于实例化出来的值为常量,因而无法被修改,

模板特化

🍒在泛型编程中,我们当前实现的函数可能并不符合某种类型,例如比较函数的实现中,大部分类型都能够使用,但是当类型为指针时,我们想要比较的是指向的内容,而实际上的结果为地址的比较。于是我们便可以使用模板特化进行特殊处理

[注意]:使用模板特化的前提条件是要有一个原模板,模板特化不能单独使用

函数模板特化

🍒原来实现的函数是这样的,借助模板实现比较函数,但遇到指针的时候就变成了地址的比较。

template<class T>
bool Less(T a1, T a2)
{
	return a1 < a2;
}

int main()
{
	int b = 4;
	int a = 3;
	cout << Less(a, b) << endl;
	cout << Less(&a, &b) << endl;
}

🍒此时我们便可以用全特化的方式,对一种类型进行特殊处理。

template<>
bool Less<int*>(int* a1, int* a2)
{
	return *a1 < *a2;
}

🍒其实,模板特化在函数模板中的作用并不明显,不如直接进行函数重载,而更多的优点体现在类模板之中。 

类模板特化

🍒类模板特化还分作全特化偏特化,我们可以根据模板参数直接观察出二者的区别。

全特化

🍒当模板参数都确定化即为全特化,当模板参数为特化的指定类型时,就使用特化的模板,否则使用原模板

template<class T1,class T2>
class A
{
public:
	A() { cout << "A(T1,T2)" << endl; }
	T1 t1;
	int t2;
};

//全特化
template<>
class A <int, char>
{
public:
	A() { cout << "A(int,char)" << endl; }
	int t1;
	char t2;
};

int main()
{
	A<int, char> a1;
    A<char, char> a2;
	return 0;
}

🍒如当前创建了实例化了两个类,其中第一个使用的就是全特化模板,而第二个使用的为原模板。

🍒之前我们就说过,模板的本质其实是将重复的代码实现交由编译器完成,但此时已经有一个准备好的代码,因此编译器就没必要自己实现一个,直接调用即可。

偏特化

🍒偏特化又分作两种情况,分别是部分特化于对参数进一步的限制。

部分特化

🍒部分特化会将模板参数列表的一部分参数特化。

🍒例如,当我们要对第二个模板参数为 int 的类进行特殊化处理时,模板参数列表中只写不特化处理的参数,接着在类名旁边标注完整的模板参数类型。

//部分特化,只有第二个模板参数为int才会使用这个模板
template<class T1>
class A<T1, int>
{
public:
	A() { cout << "A(T1,int)" << endl; }
	T1 t1;
	int t2;
};

🍒此时,a1 的第二个模板参数为 int,因此便使用偏特化的模板进行实例化。

对参数的限制

🍒对参数的限制这里,我们模板列表照写,在类名后限定参数类型,例如这里分别限定了指针和引用,由于这里有两个模板参数,因此传入的指针可以是一同一种也可以是两种不同的指针。

//偏特化,限定类型为指针
template<class T1,class T2>
class A<T1*, T2*>
{
public:
	A() { cout << "A(T1*,T2*)" << endl; }
	T1 t1;
	T2 t2;
};

//偏特化,限定类型为引用
template<class T1, class T2>
class A<T1&, T2&>
{
public:
	A() { cout << "A(T1&,T2&)" << endl; }
	T1 t1;
	T2 t2;
};

分离编译问题

🍒模板是无法分离编译的,若将模板声明放在一个 .h 文件实现放在另一个文件,则会出现链接错误

🍒这里,我们在头文件中定义了两个函数,第一个是模板函数,第二个是普通的函数。

//.h
template<class T>
bool Less(T t1, T t2);

bool Grate(int t1, int t2);

//.cpp
template<class T>
bool Less(T t1, T t2)
{
	return t1 < t2;
}

bool Grate(int t1, int t2)
{
	return t1 > t2;
}

🍒虽然此时编译器没有报错,但运行代码时,便会直接出错,指明找不到该函数。

🍒这里刚好就涉及到我们上一篇文章中所讲的动态库的链接原理,编译器之所以有语法错误的检测是因为它在后台会不断地编译代码,此时文件内部的函数名实则是用该函数在文件中的位置结合代替的,当链接的时候才会将二者关联起来,因此在编译阶段才不会有错误提示。

🍒还有一件事,上面也讲到了,模板的本质是编译器会自动帮我们生成函数,若是编译器也没有帮我们生成,那不就等于不进行操作了。同理,一直到链接前库文件都没有接收到实例化的请求,于是编译器便没有帮我们生成函数,于是在连接阶段,调用的函数文件拿着地址去库文件中找,便找不到对应的函数,因而出现链接失败。

🍒而另一个函数却是实实在在存在的,链接阶段主函数一下就找到了对应的文件,并发生函数跳转因而不会出现问题。

解决方法

🍒解决方法有两种:

  • 模板特化
  • 声明定义都在头文件中

🍒模板特化出来的函数是已经存在的,在链接时编译器就能够直接找到该函数完成链接。

🍒但这种方法直接破坏了模板的泛型效果,因此不推荐使用。

template<class T>
bool Less(T t1, T t2)
{
	return t1 < t2;
}

template<>
bool Less(int t1,int t2)
{
	return t1 < t2;
}

🍒我们将模板的声明与定义都放在同一个头文件中,函数实现时要记得加上模板,因此在预处理的时,头文件在原代码上方展开,主函数发出需要实例化的信息时,编译器便能帮助我们实例化出代码,因此程序可以成功运行。

template<class T>
bool Less(T t1, T t2);

bool Grate(int t1, int t2);


template<class T>
bool Less(T t1, T t2)
{
	return t1 < t2;
}

🍒好了,今天 【C++】模板进阶 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

Logo

助力合肥开发者学习交流的技术社区,不定期举办线上线下活动,欢迎大家的加入

更多推荐