【C++泛型学习笔记】类模板、变量模板和别名模板

学习参考书籍:王健伟《C++新经典:模板与泛型编程》

类模板

和函数模板一样,类模板可以理解为产生类的模具,通过给定的模板参数生成具体的类。vector容器就是一个类模板应用的例子,vector可以存放不同类型的数据类型元素,其就是通过引入类模板来减少不同类型元素存储时重复的代码,代码更加精简和通用。示例如下:

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
#include <iostream>
using namespace std;

template<typename T> //标识符为T的模板参数,表示myvector容器所保存的元素类型
class myvector
{
public:
typedef T* myiterator; // 迭代器
public:
myvector(); //构造函数
myvector(T tmpt) // 带参数的构造函数
{

}
myvector& operator=(const myvector&); // 重载赋值运算符,在类模板中使用模板名可以不用提供模板参数,如myvector<T>
public:
void myfunc()
{
cout << "mufunc() 被调用" << endl;
}
public:
// 迭代器接口
myiterator mybegin(); //迭代器起始位置
myiterator myend(); //迭代器结束位置
};

template<typename T>
myvector<T>::myvector() // 类外构造函数实现
{

}


int main()
{
myvector<int> tempvec; //T被替换成int,即指定模板参数T为int
myvector tempvec1(6); // 不用指定模板参数
tempvec.myfunc(); //调用类模板中的普通成员函数
}

对于类模板myvector,myvector称为类名类模板,myvector<T>称为类型名,其中T称为模板参数,T本身代表容器中的元素类型。在类模板内部类型名可以简写成类名,如myvector& operator=(const myvector&);,但在类模板外不可以,如myvector<T>::myvector()

对于类模板的模板参数推导,代码myvector tempvec1(6); 中,我们通过调用含参数的构造函数实例化模板类,编译器通过传入的实参类型可以自动推导出T的类型。而myvector<int> tempvec;则是通过类名<类型>指定T的类型(倘若没有带参数的构造函数,T仍需指定)。实现模板参数推导的功能是通过使用推断指南(deduction guide),其作用为推断类模板参数时提供推断指引。一般我们常见的(如上面代码中的例子)都为隐式推断指南,无需指定(前提是待传入参数的构造函数存在)编译器自动推断。当然,程序员也可以自定义推断指南(如不存在构造函数的情况)。形式如下:

1
2
template<typename T>
A(T,T)->A<T>;

对于类模板的特化类模板的全特化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<>
class myvector<int>
{
public:
myvector()
{
cout << "全特化版本" << endl;
}

void myfunc();
};

void myvector<int>::myfunc()
{
cout << "全特化版本的myfunc" << endl;
}

int main()
{
myvector<int> tempvec;
tempvec.myfunc();
}

需要区分的是,泛化版本的类模板和全特化版本的类模板只是同名,两者实例化后的对象是完全不同的两个类,即成员属性和函数无法共享。其次对在全特化版本类模板外定义的成员函数不能在开头加template<>,相当于全特化后变成了一个普通类。

普通成员函数和静态变量的全特化如下:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
using namespace std;

template<typename T> //标识符为T的模板参数,表示myvector容器所保存的元素类型
class myvector
{
public:
typedef T* myiterator; // 迭代器
static int m_stc; // 静态变量声明
public:
myvector(); //构造函数
myvector(T tmpt) // 带参数的构造函数
{

}
myvector& operator=(const myvector&); // 重载赋值运算符,在类模板中使用模板名可以不用提供模板参数,如myvector<T>
public:
void myfunc();

public:
// 迭代器接口
myiterator mybegin(); //迭代器起始位置
myiterator myend(); //迭代器结束位置
};

template<typename T>
myvector<T>::myvector() // 类外构造函数实现
{
cout << "泛化版本的构造函数被调用" << endl;
}

template<>
class myvector<int>
{
public:
myvector()
{
cout << "全特化版本" << endl;
}

void myfunc();
};

void myvector<int>::myfunc()
{
cout << "全特化版本的myfunc" << endl;
}

template<typename T>
void myvector<T>::myfunc()
{
cout << "泛化版本的普通成员函数myfunc()被调用" << endl;
}

template<>
void myvector<double>::myfunc()
{
cout << "泛化版本的普通成员函数myfunc()的全特化版本被调用" << endl;
}

template<typename T>
int myvector<T>::m_stc = 10;

template<>
int myvector<double>::m_stc = 100;


int main()
{
myvector<float> tempvec;
tempvec.myfunc();
cout << tempvec.m_stc << endl;
}

在上述代码的mian函数中,我们使用myvector<float> tempvec;指令模板参数类型为float实例化了类模板,因为myvector类模板存在全特化版本,所以编译器会先找到全特化版本,但是由于全特化版本中模板参数特化类型为int,class myvector<int>,所以只能使用泛化版本,因此运行的是泛化版本的构造函数。接着,运行代码tempvec.myfunc();,tempvec是泛化版本示例化后的类,所以myfunc函数也是泛化版本中的,不过在泛化版本中存在全特化的同名函数,所以会优先选择其。不过由于该成员函数的全特化版本模板参数类型为double,void myvector<double>::myfunc(),不是该实例化类中模板参数float,所以该全特化函数不会取代原来的同名成员函数。对于模板类中的静态变量m_stc同理。亦然,我们如果最开始指定模板类型为double,那么会依次执行泛化版本的构造函数,泛化版本的全特化myfunc成员函数,输出泛化版本的m_stc静态变量。想一想,如果实例化类的模板参数类型为int,那么cout << tempvec.m_stc << endl;必然会报错,因为类模板的全特化版本中没有这一静态变量。

注意:如果进行了普通成员函数或静态成员变量的全特化,那么就无法用这些全特化时指定的类型对整个类模板进行全特化了。因为在对成员函数或静态成员变量进行了全特化后导致实例化了对应类型的类模板,如果再次进行全特化,将不会重复进行相同类型的实例化,编译器报错。

对于类模板的偏特化有两种:一是模板参数数量上的偏特化;一是模板参数范围上的偏特化。具体实现和原理类似于函数模板的偏特化。

对于默认参数

  • 与函数模板的默认参数不同,类模板的默认参数规定:如果某一模板参数具有默认值,那么其后所有的模板参数都得有默认值。
  • 有默认值的模板参数在类模板实例化时可以不用提供,如myvector<> tmpvec
  • 后面的模板参数可以依赖前面的模板参数,如template<typename T, typename U=T*>
  • 还可以在模板声明中指定默认参数。

类型别名,可以通过typedef或者using关键字给类型名起一个别名。

1
2
typedef TC<int, float> IF_TC;
using IF_TCU = TC<int, float>;

和函数模板一样,类模板中同样也可以有非类型模板参数,但全局指针、浮点数和字符串常量不能作为非类型模板参数。

成员函数模板

示例如下:

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
#include <iostream>
using namespace std;

template<typename T1>
class A
{
public:
A(double v1, double v2) // 普通构造函数
{
cout << "A::A(double,double)执行了!" << endl;
}
A(T1 v1, T1 v2) // 使用类模板参数类型的构造函数
{
cout << "A::A(T1,T1)执行了!" << endl;
}
template<typename T2>
A(T2 v1, T2 v2); // 构造函数模板

template<typename T3>
void myfunc(T3 tmpt) // 普通成员函模板
{
cout << tmpt << endl;
}
T1 m_ic;
static constexpr int m_stcvalue = 200;
};

// 在类外实现类模板的构造函数模板
template<typename T1> // 先写类模板的模板参数列表
template<typename T2> // 再写构造函数模板自己的模板参数列表
A<T1>::A(T2 v1, T2 v3)
{
cout << "A::A(T2,T2)执行了!" << endl;
}


int main()
{
A<float> a(1, 2);
a.myfunc(3);
}
  • 类模板中的成员函数,只有在源代码中调用时,对应成员函数才会出现在实例化的类模板中。
  • 类模板中的成员函数模板,只有在源代码中调用时,对应成员函数模板的具体实例才会出现在实例化的类模板中。
  • 编译器目前不支持虚成员函数模板。

拷贝构造函数模板不等同且永远不可能成为拷贝构造函数,拷贝赋值运算符模板不等同且永远不可能成为拷贝赋值运算符。类型相同的对象拷贝构造调用的是拷贝构造函数,类型不同的对象拷贝构造调用的是拷贝构造函数模板,并不会因为找不到对应调用对象而且调用另一个。

对于成员函数模板也具有特化版本。

相关资料表示,C++标准不允许在类模板之外全特化一个未被全特化的类模板的成员函数模板。即在类模板外,如果要全特化一个成员函数模板,需要确保该成员函数模板所属的类模板为全特化版本。

类模板嵌套

类模板中套类模板和类中类相差不大。需要注意的是,将子类模板的成员函数写在父类模板的泛化版本之外,应当如下形式:

1
2
3
4
5
template<typename T1>	//父类模板参数列表
template<typename U> //子类模板参数列表
void A<T1>::B<U>::myfunc()
{
}

变量模板与成员变量模板

变量模板定义和使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

template<typename T>
T myvar{}; // 变量模板,{}为零初始化

int main()
{
myvar<int> = 13;
myvar<double> = 13.1;
cout << myvar<int> << " " << myvar<double> << endl;
}

不同的指定类型得到不同的变量。

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
template<typename T>
T myvar{}; // 泛化版本

template<>
char myvar<int>{}; // 全特化版本,myvar<int>可以当作char类型使用

template<typename T>
T myvar<T *>{120}; // 偏特化版本

template<typename T = int>
T myvar{}; // 默认模板参数

template<typename T, int val>
T myvar[val]; // 非类型模板参数

template<typename T>
class A
{
public:
template<typename W>
static W m_tpi; // 成员变量模板
};

// 成员变量模板在类模板外定义
template<typename T>
template<typename W>
W A<T>::m_tpi = 5;

别名模板与成员别名模板

别名模板的作用主要是简化书写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <map>

template<typename T>
using str_map_t = std::map<std::string, T>; // 别名模板

template<typename T>
class A
{
template<typename T>
using str_map_t = std::map<std::string, T>; // 成员别名模板
public:
str_map_t<int> map1;
};

int main()
{
str_map_t<int> map1;
map1.insert({'one', 1});
map1.insert({'two', 2});
A<float> obja;
obja.map1.insert({'one', 1});
}

模板模板参数

之前我们学习的模板参数有类型模板参数和非类型模板参数,这一部分将提出模板模板参数,即模板参数本身为模板,将类模板当作参数传递到另一个模板中。我们在学习类模板的时候知道vector、list等容器类其实也是类模板,所以若我们想要将这些容器类作为模板参数传入模板中,写法如下:

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
#include <iostream>
#include <vector>
#include <list>
using namespace std;

template<typename T, template<class> class Container = std::vector>
//template<typename T, template<typename W> typename Container = std::vector>
class myclass
{
public:
Container<T> myc;
public:
void func();
myclass()
{
for (int i = 0; i < 10; i++)
{
myc.push_back(i); // 本行代码正确性取决于模板参数类型
}
}
};

template<typename T, template<class> class Container>
void myclass<T, Container>::func()
{
cout << "mstifiy" << endl;
}

int main()
{
myclass<double, list> mylistobj; // double是容器中的元素类型,list是容器类型
mylistobj.func();
std::cout << mylistobj.myc.size() << endl;
}

其中重点注意模板参数列表的写法:

1
2
3
4
template<typename T, template<class> class Container = std::vector>
template<typename T, template<typename W> typename Container = std::vector> // class也能替换成typename修饰
template<typename T, template<typename> typename Container = std::vector> // W为容器模板的类型模板参数,不能用,故省略
template<typename T, template<typename> typename Container> // 没有默认模板参数

class和typename可以相互替代,都是合法的。