类模版

模版

​ 模版是C++中一个相对重要的特性了,直接来吧

类模版

​ 由于这种东西还是得上手去敲,所以我们在这里对很多内容将会进行简化。

​ 模版这种东西最直观的作用就是进行方法与所使用的数据类型之间的解耦,使得在设计方法时能够更加专注于特定功能的方法的构建而不必要去关注所使用的数据类型。

使用

​ 一个模版的声明,无论是类模版,还是函数模版,都是基本一致的。通过template关键字来进行

1
template<class T>		||		template<typename T>

​ 这样就完成了一个模版的创建,但仅仅声明一个模版是没有意义的,我们需要去使用这个模版。

​ 总的来说,通过关键字template和尖括号的组合,我们能够创建出一个模版,这个模版中,可以去使用class关键字或者typename关键字,俩者等效,主要是习惯上的差别。接着,在class/typename之后会跟上一个变量名,这一个就是我们模版的一个符号,我们在接下来的作用域中使用模版的直观体现,就是使用这里的符号。

在使用前,我们需要了解一些简单的模板所具有的性质

作用域

​ 一个模版的作用域是有限的,简单的说,其只会在距离其最近的一个作用域中生效,并且在离开改作用域时,其他作用域中不可见。特别需要注意的是,在部分的模版使用中,可以看到系列的嵌套模版,这些的作用域可以用局部自动变量来进行类比,这里就不进行赘诉了。

代码举例

​ 接下来通过一个简单的demo来了解一下简单的类模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
template <class T>
class stack
{
private:
enum {MAX=10};
T data[MAX];
int top;

public:
stack(){top=0;}
bool isempty();
bool isfull();
bool push(const T& item);
bool pop(T& item);
};

template<typename T>
bool stack<T>::isempty()
{
return top==0;
}

template<class T>
bool stack<T>::isfull()
{
return top==MAX;
}

template<typename T>
bool stack<T>::push(const T& item)
{
if(!isfull())
{
data[top++]=item;
return true;
}
return false;
}

template<typename T>
bool stack<T>::pop(T& item)
{
if(isempty())return false;

item=data[--top];
return true;
}

​ 从上面可以了解到几点,一个template是一个独立的模版的声明。在这个关键字声明的模版之后紧邻的一个作用域中,该模版一直可见,直到出去了该作用域,该模版失效。

​ 除此之外,我们还需要看到一点的是,模版类的一些特殊性质。


模版类

实例时机

​ 对于一个使用模版的类来说,这个类其实并不是一个严格意义上实例的类,换句话说,这个类其实是不存在的,因为我们现在并没有一个实际的类声明可以供给我们去进行类的实例化。这是模版的设计上导致的。毕竟模版的性质就导致了程序能够根据不同的数据类型来进行对于的数据类型的类进行创建。

​ 正如那句经典的话:不要为不需要的特性去浪费时间,这里其实也利用了一种类似于懒加载的机制。程序并不会在一开始就进行所有的类型的模版类的实例创建。具体的等下再继续阐述。

​ 总的来说,现在需要知道的就是一个模版类在编写阶段其实是一种虚的阶段,在编译器层面不会存在任何模版类这种东西,毕竟带入实际的数据类型一个模版类才有存在的价值。

模版类方法

​ 可以观察到,在我们进行类方法的补充时,如果我们的定义是在类内进行的,其实跟普通的没什么区别。但是当我们需要再类外去进行类内方法的定义时,这里有几个需要注意的点。

​ 首先,我们知道我们如果想要在类外去进行类方法的定义,那么我们需要再对于的方法前去加上类解析符(类名::)。在模版类中也是如此,但是前面说过了,一个模版类stack其本身不是一个完整的类,并不存在一个实际上的类声明表示这个类。模版类的正确解析符则是在类名之后加上我们的模版参数,通过这样,之后的各种方法,才能够被绑定到创建的各种模版类实例中去。就比如**stack< int >**就代表着一种使用int数据类型进行填充的stack类,这样的类在编译器层面才会生成对于的类定义,也就才有意义。

​ 那我们这里再来观察下模版在类内的使用,前面我们已经知道创建的模版会以对于的声明的typename/class后面的符号名来进行使用。在类内的体现就是**T data[MAX];**可以看到的是,一个模版的使用,其实就是跟一个基本数据类型的使用别无二致,我们只需要记住这个模版在未来会被替换为具体的数据类型来进行类的声明即可。

​ 除了这种模版的基础使用之外,还有俩种比较常用的模版使用。

多功能

多参数模版

​ 一个template能够承载多个参数,这个其实没有什么好说的,就是一个使用的扩展。例如

1
template<class T1,class T2>;

​ 这样的模版声明就是一个多参数模版的构建,通过这样的声明,我们能在接下来的作用域中,去使用这俩个模版参数。这来个模版参数的使用与一个模版参数的使用并没有什么显著的区别。

递归使用模版

​ 其实上面那个代码实例就是一个递归使用模版的例子。我们需要知道的是string其实是一个模版实例化后再进行一个名称转换后的产物,就是一个basic_string<char*>模版。

​ 要了解这种递归的使用会产生什么效果,我们需要去观察到这里模版类生成的最后的具体的类声明到底是一个什么样的,理解了这个,我们接下来的很多关于类的使用都能减轻负担。

模版具体化

​ 前面已经提到过了,一个模版类不是一个真正的类,只有在一个模版类被指定特定的模版参数时,它才可能进行对应的模版类的构建,具体的表现就是使用对于的类型参数去替换对于的模版类中的模版参数,只有在进行了替换之后,这个生成的类才是一个真正意义上的类。这个阶段通常是由编译器在编译阶段自动执行的。

​ 模版的具体化存在着多种情况,一种种来进行分析。

隐式实例化

​ 这是我们最常见的一种形式,也是我们前面所说的,通过编译器自动判断我们传递给模版类的类型参数来进行对于的类定义的生成的方式。例如

1
ArrayTP<int,100> stuff;

​ 需要注意的是,编译器在生成对于的类的时候,采取一种类似于懒加载的机制,只有在需要一个真正的被创建出来的类实例的时候,才会进行特定的类定义的生成。

1
2
ArrayTP<double, 30>* pt;		//只是一个指针,不会导致对象的创建
pt=new ArrayTP<double, 30>; //使用了new,需要调用构造函数去创建,因此会生辰对于的类定义

额外需要注意的是,对于模版类来说,不只是其的类定义是一种懒加载的机制,其的方法也可以看做是一种懒加载的机制,只有在使用到对于的类方法时,才会进行对于的类方法的实例化构建。

显式实例化

​ 对应于隐式实例化的还有一种显式实例化。顾名思义,就是显式的指出编译器需要去进行这种模版类的构建,这种方法的使用也很简单,就是在给模版类提供类型参数时前面加上一个template关键字。

1
template class ArrayTP<double,100>;

​ 通过加上这个关键字,在编译器识别到这行语句时,会直接进行对应的类定义的构建。


这里我们在对实例化的时机进行一下剖析,熟悉的从一个简单的东西突然发散然后给我感到想这想那,虽然浪费了一些时间,不过对于一些东西的理解也加深了,寒假末期也得逐渐找回状态了。

实例化时机

​ 无论是显式还是隐式的实例化,它的实例化都是在编译阶段进行的,也就是说,即使是你在运行阶段完全没有涉及到部分模版类的使用,这些个模版类还是会在编译阶段被实例化。那么,隐式实例化相对于显式实例化的意义到底在哪呢?

​ 这里我们就需要对于隐式实例化的实例时机来分析了,就现在我所了解到的,隐式实例化是在编译器识别到代码进行了特定的模版类的使用时进行的实例化。简单来说,编译器会进行一个全部文件代码的阅读,在这其中,我们可以简单的省略所有的判断条件,只要出现了特定的模版类使用(包括但不限于一个模版类对象的创建,类方法的使用),那么这对应的类定义以及类方法定义都会被实例化。

​ 知道了上面这点后,我们对于隐式实例化与显式实例化之间的区别应该有所了解了。一个隐式实例化相对于显式实例化能够更加节省编译器的资源,能够减少对于的类原型以及方法构建所需要的时间以及空间。那么,显式构建的优势在哪呢?

​ 其实一直到现在,由于我还没有对于模版进行比较深入的使用与了解,对于提到的显式实例化的优势,我也是有点怀疑的,不过假设我已经知道了这一块的。或者说,假定这个隐式实例化会导致一定的多余定义。那么显式实例化能够起到的作用其实很明显,就是通知编译器去生成一个对于的完整的类,包括类声明以及对于的方法定义。在这种情况下只要使用到这个被显式实例化的模版类,编译器不会再尝试去生成一个相同的类,这样能避免一些编译层面可能存在的问题。

​ 我们简单的使用链接性来进行隐式实例化与显式实例化之间存在的区别,

​ 简单来说,隐式实例化生成的符号(类以及对应的方法)等都只是一个局部链接性。当我们想要在多个文件这种去使用这个模版类去进行相同的类型实例化时,将会导致每个文件都产生对应的相同符号并编入到符号表中,在最后的符号表中,会存在符号重复的情况。虽然说编译器一般会对这些重复的符号进行合并。但是,这种生成是一种花销,这种合并也是一种花销。为了减少这种花销,提高性能,因此,出现了显式实例化这种功能。

​ 而显式实例化产生的类是一个全局链接性的,无论是在那个文件中,只要引用了对应的使用了显式实例的文件,那么这个显示实例产生的类就是全局可见的,再进行使用时,编译器不会再去进行对应的声明。这样就避免了一个相同的模版类被多次实例化。

好了,上面耗费的时间有点多了,我们进入下一个具体化

模版具体化

​ 很多时候,一个通用的模版其实是不够的,这些模版可能需要存在一些特例,在这些特例中,可能存在着一些与通用模版所不一致的行为,这时就需要使用显式具体化来实现了。通过显式具体化,我们能够定制一系列的在特定模版参数下的模版类。

​ 模版具体化,或者说,模版特化允许开发者为特定类型或特定条件提供定制化的实现,从而在保持代码泛化的同时,针对特定情况进行优化或调整。模板特化主要分为两种形式:全特化(Full Specialization)和偏特化(Partial Specialization)。

1. 全特化(Full Specialization)

全特化是指为模板的所有参数提供具体的类型或值,从而完全指定模板的行为。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cpp复制编辑// 通用模板
template <typename T>
class MyTemplate {
public:
void func() {
// 通用实现
}
};

// int 类型的全特化
template <>
class MyTemplate<int> {
public:
void func() {
// 针对 int 类型的特化实现
}
};

在上述示例中,MyTemplate<int>MyTemplate 的全特化版本,专门为 int 类型提供了特定的实现。

2. 偏特化(Partial Specialization)

偏特化是指只为模板的部分参数提供具体的类型或值,保留其他参数为泛型。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename T, typename U>
class MyTemplate {
public:
void func() {
// 通用实现
}
};

// 第一个类型为 int 的偏特化
template <typename U>
class MyTemplate<int, U> {
public:
void func() {
// 第一个类型为 int 的特化实现
}
};

// 第二个类型为 int 的偏特化
template <typename T>
class MyTemplate<T, int> {
public:
void func() {
// 第二个类型为 int 的特化实现
}
};

在上述示例中,MyTemplate<int, U>MyTemplate<T, int> 分别是 MyTemplate 的偏特化版本,针对第一个或第二个类型为 int 的情况提供了特定的实现。

关于模版的特性还有很多,但是由于我的动力已经在这里消失了,所以我将不会继续这一块,之后哪里遇到了感兴趣的模版的东西再回来更新吧。

-------------本文结束 感谢阅读-------------