C++ 类模板小结

Last updated on 3 months ago

模板具体化

类模板与函数模板很相似,都有隐式实例化,显示实例化、显示具体化,这些统称为具体化( implicit instantiation) 。类模板的声明用泛型的方式来描述类,而具体化则是以具体的类型来生成对应的类声明

隐式实例化

在用类声明对象时,我们需要指定其类型,之后编译器会通过类模板来为该类型生成一个类定义(也就是类的具体化,类的模板参数变为已知),即:

1
stack<int>st;//编译器会生成一个int类型的类定义,然后用该类定义来实例化对象

这里需要说明的是:类的具体化是指「用指定的类型通过类模板声明来生成对应类型的类定义」。

我们通常会写出一个带有模板参数的类声明(这个类当中的所有成员方法都写出了模板定义,但我们依旧称整个类为模板类声明),当我们给模板参数指定一个类型的时候,编译器会利用类模板声明自动生成该类型所对应的类模板定义(这个时候类模板中的所有模板参数替换为指定的类型)

这个地方,我们是需要创建一个对象之后才会发生隐式实例化,如果没有对象需要创建则不会发生隐式实例化,即:

1
stack<int>pt*;//不会创建对象,自然也不会有隐式实例化,编译器不会为int生成一个类定义

那么为什么是「隐式」呢?

这里的隐式说的应该是类的具体化是隐式的。具体来说,由于需要创建对象,那么在创建对象之前就需要有相应的类定义,这个时候类的「具体化」就会被隐式地执行

下面地显式实例化也是同理。虽然没有创建对象,但由于显式制定了类型,也就相当于类的「具体化」会被显式执行

那么为什么要叫「实例化」呢,直接叫「隐式具体化」不就行了?

我们所说的实例化指的是用类定义类创建一个对象,「具体化」仅仅是生成对应的类定义,并不涉及到对象的创建。

然后,我们需要在这里说明一下什么叫「类声明」和「类定义」。我的理解是:类声明就是只写出类中方法的声明而没有给出定义,类定义是在前者的基础上给出了类方法的定义

那么「类模板声明」和「类模板定义」就是:前者为类定义,不过类当中的类型为模板参数,后者为类定义并且类当中的类型为指定类型

显式实例化

显示实例化必须放在类模板定义所处的名称空间中。这种情况下,虽然没有创建对象但编译器利用类模板声明也会生成对应类型的类定义

我们只需要用 template 来指定所需类型来声明类即可

1
template class stack<int>;

这么做之后,编译器会直接创建一个类型为 int 的类定义,就算没有指定对象。

显式具体化

前面说过,具体化就是用类模板声明来创建对应类型的定义。有的时候,这种类模板定义并不能满足对于一些特定类型的需求。因此我们需要为特定的类型单独生成其类模板定义,这便是显式具体化。

1
2
3
4
template<> class className<specialized-type-name>
{
...
};

当需要为 speciailized-type-name 创建对应的类模板定义时,编译器不会通过类模板声明来创建而是直接使用显式具体化得到的类定义(看到了吗,具体化就是生成对应类型的类定义)。

部分具体化

如果有多个模板参数,那么我们可以给部分模板参数指定类型,这便是「部分具体化」

1
2
3
4
5
6
template<class T1, class T2>
class P {...};

template<class T1>//这个部分写的是没有被具体化的模板参数
class P<T1, int> //这个部分不用写 class
{...};

当然,有多个模板可供选择的话编译器会选择具体化程度高的

我们也可以这样:

1
2
3
4
5
template<class T> class P{...};
template<class T*> class P{...};

P<char> t1;//匹配第一个
P<char*> t2;//匹配第二个

我们还可以更骚:

1
2
3
template<class T1, class T2, class T3> class P{...};//下面两个都是这个的特化
template<class T1. class T2> class P<T1, T2, T2>{...};
template<class T1> class P<T1, T1*, T1*>{...};

成员模板

一个类模板当中是可以嵌套另外一个类模板的,这一部分我们主要说明一下如何在类外定义这些东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T>//类声明
class TP
{
private:
template<class V>//这个地方不能写 T , 下面同理
class HD;
HD<T>a;
HD<T>b;

public:
TP(T e1, T e2, T e3, T e4)
: a(e1, e3), b(e2, e4)
{ }

void show() const;
template<typename Y>
Y cal();
};

这个例子基本上囊括全了,在类模板当中既有类模板声明也有函数模板声明。不管你怎么写,他们的在类外的定义总归是一样的

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
template<class T>
template<class V>//同样,不能写 T
class TP<T>::HD //类的话前面就是 class ,函数的话前面就是返回值
{
public:
HD(V e1, V e2 = e1)
:a(e1), b(e2)
{ }

V re_a() const { return a; }
V re_b() const { return b; }

private:
V a, b;
};

template<class T>
template<typename Y>
Y TP<T>::cal()
{
return a.re_a() + a.re_b() + b.re_a() + b.re_b();
}

template<class T>
void TP<T>::show()const
{
cout << a.re_a() << " " << a.re_b() << endl;
cout << b.re_a() << " " << b.re_b() << endl;
}

关于写了两层 template ,我需要说明一点的是:由于类内部的类 HD 只能由类名 TP 解析而得(因为模板是嵌套的,绝对不能用 template T, template V 这种,这就不是嵌套了),而关于类名 TP 必须要给出其模板参数类型才行,而类 HD 的定义本身也需要一层 template ,这便才有了两层。

我需要额外说明一点:这里的 HD 的模板参数跟 TP 的模板参数不同,当然也可以写成相同,这样的话只需要在类外写一层 template 就行。

将模板作为参数

我们上面提到的类模板的声明都是 template<class T> 。这里面的 T 可以表示任何类型,注意,是「类型」。那么什么叫「类型」?

个人理解是「能够用于初始化对象的」叫做类型。

就比如可以用 int 初始化对象,因此 int 就是类型;可以用 stack<int> 初始化变量,那么 stack<int> 就是类型。而我们在模板中所用的参数 T ,实际上是用这个类型来声明模板类。

也就是说,类模板中的所有成员变量的类型都是 T

那么这里就有问题了。如果我目前所需要写的一个类模板当中的大部分功能可以直接通过 STL 容器实现,即我可以通过对 STL 容器采取复合的操作来实现我的需求。而这个「复合」,指的就是我自己写的那个类当中的成员变量需要有类模板

而类模板并不是类型,无法于模板参数 T 相匹配。为了解决这个问题,我们引入了将模板作为参数

我们直接看例子吧:

我们知道,stack 的模板声明为 template<class _Ty, class _Container = deque<_Ty>> 。如果我们在类模板中希望使用 stack 类,那么我们的类模板可以这么定义:

1
2
3
4
5
6
7
template<template<class _Ty, class _Container = deque<_Ty>>class Type>
class TE
{
private:
Type<int>s1;//这里可以是其他的类型
Type<int>s2;
};

我们着重分析一下第一行。一般的都是 template<class T> (我们这里统一用 class )。

这里的 T非模板类型模板参数的意思,这里的 Type模板类型模板参数的意思。

后者需要在前者的前面加上 Type 是哪种类型的模板(这个模板的模板参数是什么)。

(这个地方我感觉我的语言说不清楚,我。。。觉得我理解了,但我说不出来。。。。)

我们后面在使用的时候是这么写的:

1
TE<stack>T;

编译器会用 stack 替代 Type ,因此 Type 必须为模板类型并且该模板类型必须与 stack 的模板类型一致。

模板类和友元

模板类的声明中可以加入友元。模板友元分三类。

  • 非模板友元
  • 约束模板友元
    • 友元类型取决于类被实例化时的类型
  • 非约束模板友元
    • 友元的所有具体化都是类的每一个具体化的友元

非模板友元

友元,实际上是给类外对象访问类内成员的一种方法(虽然这种方法会破坏封装)

我们在友元函数参数的部分通常会写这个类的类型。同理,对于类模板而言,我们需要为该类模板显式具体化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class Test
{
public:
friend void fun(Test<T> rhs);//使用类模板中的成员
friend void show();//不使用类模板中的成员
};

void fun(Test<int> rhs)
{
cout << "Chisato" << endl;
}

void show()
{
cout << "Chisato" << endl;
}

这个其实没有什么好说的,跟普通的类写友元函数一样。我们重点看后面两个

约束模板友元

你会发现,上面那种写法的话会很麻烦。为什么?

你想啊, Test 是一个类模板,可以适用于任何类型,而 fun 函数只能用于 Test 对于 int 类型的显式具体化,而对于其他类型的显式具体化则没有办法。

说人话就是,你每用 Test 来生成某个类型的具体化时,都需要对 fun 进行一次重载。

好家伙,这不累死人。作为一个优雅(懒惰)的程序员我觉得不允许这种事情的发生。这里便引入了约束模板友元。

我们想到,既然函数模板可以适用于任何类型,类模板也可以适用于任何类型。如果我将函数模板作为类的友元不就完美了?

事实上,约束模板友元就是这么做的。

我们直接看代码吧,这东西。。。。

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
template<typename T> void fun(T& rhs);//一定要在类模板的前面先声明函数模板

template<typename T> void show();

template<class T>
class Test
{
public:
friend void fun<Test<T>>(Test<T>& rhs);
//也可以这么写:friend void fun<>(Test<T>& rhs);
friend void show<T>();

Test(T rhs = 0)
: n(rhs)
{ }
private:
T n;
};

template<typename T>
void fun(T& rhs)
{
cout << rhs.n << endl;
}

template<typename T>
void show()
{
cout << "Chisato" << endl;
}

首先,最上面那个模板声明只能写在同一行,分两行写会报警告(我也不知道为什么,哦卡西)

至于类模板当中为什么要写成 friend void fun<Test<T>>(Test<T>& rhs) ,我们带入一个具体类型可以理解了。

T 替换成 int ,由于 fun 本身是模板函数,因此需要显式指出其模板参数,那么对于 Test<int> 类型的对象, fun 的声明就应该写成 fun<Test<int>>(Test<int>& rhs)

实际上在模板里面也可以写成 fun<>(Test<T>& rhs) ,因为可以通过函数的参数推导得出前面那么尖括号里面的内容。

而如果调用的是 show ,这玩意没有参数,就需要显式指出其类型了。具体的调用如下:

1
2
3
4
5
6
7
8
9
10
int main()
{
Test<int> t(5);
fun(t);
Test<double>r(15);
fun(r);
show<int>();
show<double>();
return 0;
}

非约束友元函数

前面一种的情况是:int 类型的类的具体化会生成 int 类型的函数的具体化,反过来, int 类型的函数的只能访问 int 类型的类的具体化 。这被称为是约束的

当然还有非约束的,这种的情况是:对于一个友元函数,它可以访问任何类型的类当中的成员,即每个函数具体化都是每个类具体化的友元

我们只需要在类内声明友元函数就可以实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class Test
{
public:
template<typename C, typename U> friend void fun(C&, U&);

Test(T rhs = 0)
: n(rhs)
{ }
private:
T n;
};

template<typename C, typename U> void fun(C& rhs, U& chs)
{
cout << rhs.n << " " << chs.n << endl;
}

这种没什么好说的,知道是什么意思就行,还满简单的

其实就是一个函数模板作为友元存在,整个非约束友元,花里胡哨的(((

下面是具体的调用代码:

1
2
3
4
5
6
7
int main()
{
Test<int>t(10);
Test<double>r(1.5);
fun(t, r);
return 0;
}

类模板结束了, 我可以去看 Effective C++ 当中泛型的部分了,走起!!!!


C++ 类模板小结
https://nishikichisato.github.io/2022/07/30/C++ Primer Plus/类模板/
Author
Nishiki Chisato
Posted on
July 30, 2022
Updated on
November 25, 2023
Licensed under