有关模板的一些笔记

整理下如何写一些简单的模板的方法。

首先要明确一点,模板的功能十分强大,是 C++ 泛型编程的基础,C++ 引入模板的原因也就是为了实现泛型,精简不必要的代码。而且现代 C++ 的很多设计理念和基础设施基本都是建立在模板上面的,先不说完整掌握模板的使用,至少得能看懂一些吧。(实际上是原来学习过的知识都忘了...😂)

OK,废话不多说。

模板的种类

一般来讲,C++ 中的模板分为:函数模板(function template)类模板(class 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
#include <iostream>

template <typename T>
T myAdd(T t1, T t2) {
return t1 + t2;
}

int main() {

int a = 1, b = 2;
double f = 1.1, d = 2.2;
std::string s1 = "xx", s2 = "zz";

std::cout << myAdd(a, b) << std::endl;
std::cout << myAdd<double>(f, d) << std::endl;
std::cout << myAdd<std::string>(s1, s2) << std::endl;

return 0;
}
/*
3
3.3
xxzz
*/

在上面的代码中,我们定义了一个通用的函数模板,功能是将传入的两个变量相加,并返回相加的结果。其中T是模板的模板参数,也叫模板类型参数<typename T>叫做模板参数列表,第 15、16 行尖括号中的类型则称为模板实参。注意,一个模板参数名只能在一个特定模板参数列表中出现一次。编译器会根据模板实参,为我们实例化一个特定版本的myAdd函数来被我们调用。同时,在 14 行中,我们没有指定模板实参,此时编译器根据函数实参来推断模板实参,然后生成对应版本的函数。
另外,我们还可以为模板添加非类型模板参数,比如:

1
2
template<typename T, int X>
//...

也可以为模板类型参数指定默认类型:

1
2
template<typename T = int>
//...

另外,如果我们自定义一个类也希望使用这个函数模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

class A {
};

template <typename T>
T myAdd(T t1, T t2) {
return t1 + t2;
}

int main() {
A a, aa;
myAdd<A>(a, aa);

return 0;
}

此时会发现报错了,原因是因为在A这个自定义类中,我们没有提供+运算符,重载这个运算符后就可以了,比如:

1
2
3
4
5
6
7
8
class A {
public:
A operator+(const A& a) {
A aa;
// aa + a...
return aa;
}
};

在模板参数列表中,使用typenameclass都是可以的,没有任何区别。

类模板

有了前面的基础知识,我们可以很容易的实现一个类模板。假设我们要实现一个模板栈,我们希望这个栈可以存储各种数据类型,包括自定义类,并且还要具备栈的基本功能,那么我们可以这样实现:

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
#include <iostream>
#include <stdexcept>

template<typename T, int SIZE = 10>
class Stack {
public:
Stack(): idx(-1), data(new T[SIZE]) { }
~Stack(){
delete[] data;
data = nullptr;
}
bool push(T t) {
if(idx < SIZE) {
data[++idx] = t;
return true;
}
return false;
}
bool pop() {
if(idx > -1) {
--idx;
return true;
}
return false;
}
T top() {
if(idx == -1)
throw std::runtime_error("stack is empty!");
return data[idx];
}
bool empty() {
return idx == -1;
}
private:
int idx;
T* data;
};

int main() {
Stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while(!st.empty()) {
std::cout << st.top() << std::endl;
st.pop();
}

Stack<std::string> st2;
st2.push(std::string("xxx"));
st2.push(std::string("ddd"));
st2.push(std::string("sss"));
while(!st2.empty()) {
std::cout << st2.top() << std::endl;
st2.pop();
}
return 0;
}

在上述代码中,我们简单实现了一个可以存任意类型、容量为 10 的栈。我们为这个模板类提供了一个模板参数和一个带默认值的非类型模板参数(作为容量大小)。同时,类模板的所有函数都是写在模板类内的,这样做好处是模板类类内定义的成员函数会被隐式声明为内联(inline)函数
如果我们要将模板类成员函数写在类外,以上述代码为例:

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
#include <iostream>
#include <stdexcept>

template<typename T, int SIZE = 10>
class Stack {
public:
Stack(): idx(-1), data(new T[SIZE]) { }
~Stack(){
delete[] data;
data = nullptr;
}
bool push(T t) {
if(idx < SIZE) {
data[++idx] = t;
return true;
}
return false;
}
bool pop() {
if(idx > -1) {
--idx;
return true;
}
return false;
}
T top();
bool empty() {
return idx == -1;
}
private:
int idx;
T* data;
};

template<typename T, int SIZE>
T Stack<T, SIZE>::top() {
if(idx == -1)
throw std::runtime_error("stack is empty!");
return data[idx];
}

int main() {
Stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
while(!st.empty()) {
std::cout << st.top() << std::endl;
st.pop();
}

Stack<std::string> st2;
st2.push(std::string("xxx"));
st2.push(std::string("ddd"));
st2.push(std::string("sss"));
while(!st2.empty()) {
std::cout << st2.top() << std::endl;
st2.pop();
}
return 0;
}

注意在模板类类外定义成员函数时,必须使用与类声明完全一致的模板参数列表和类名限定​​。

与函数模板不同的是,编译器不能为类模板推断模板参数类型,必须现实提供模板实参。

总结

本文主要总结了函数模板和类模板的基本写法,没有什么偏难怪的地方,都是模板相关的常规基本知识。实际上,除了单独的函数模板和类模板外,我们也可以给普通类定义模板成员函数,或者给类模板定义类模板成员函数。总而言之,模板的功能十分强大,使用也非常灵活,需要结合更多的实践来掌握。


Buy me a coffee ? :)
0%