【Cpp】第十章-模板进阶

模板进阶

  之前的博客已经介绍过模板的概念,这是Cpp在实现泛型编程中不可缺少的一环,在模板进阶的讨论中会着重于模板的更为高级的使用。

非类型模板参数

使用

  在模板中我们通常都是定义一个类型模板参数,在进行实例化的时候通过传入类型来实例化模板,但是模板中也可以定义非类型的模板参数。用一个封装的定长顺序表进行举例。

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
#include <iostream>
#include <assert.h>
using namespace std;
template<class T, size_t L>
class Array
{
public:
Array()
:_arr({0})
,_size(0)
{}
size_t Size() const
{
return _size;
}
T& operator[](size_t pos)
{
assert(pos < _size);
return _arr[pos];
}
void Push_Back(T data)
{
if(_size < L)
{
_arr[_size] = data;
_size++;
}
else
{
cout << "Array is full" << endl;
}
}
void Pop_Back()
{
if(!Empty())
{
_size--;
}
else
{
cout << "Array is empty" << endl;
}

}
bool Empty() const
{
return _size == 0;
}
private:
T _arr[L];
size_t _size;
};
int main()
{
Array<int, 20> arr;
arr.Push_Back(1);
arr.Push_Back(2);
arr.Push_Back(2);
arr.Push_Back(5);
arr.Push_Back(7);
arr.Push_Back(5);
arr.Pop_Back();
arr.Pop_Back();
arr.Pop_Back();
for(int i = 0; i < arr.Size(); i++)
{
cout << arr[i] << endl;
}
}

注意

  非类型模板参数在使用中有很严格的要求,它必须遵循以下规则,其实这块的要求在网上大部分博客中都说的十分模糊并且在我的实验下发现过于片面和局限,因此我给出我的总结和理解。
  1、非类型模板参数可以是整形,指针和引用。
  2、再给模板参数传参时要求其必须是一个常量表达式。
  3、如果模板参数是一个整形,那么在传参的时候可以传入字面值常量,也可以是全局/局部常量,可以是外部/内部链接(关于链接属性参考其他博客)。
  4、如果模板参数是一个指针或者引用,那么传参时要求,如果传入变量,变量必须是全局的,如果是常量常量必须是外部链接属性的,并且不能把动态对象的指针或引用传入。也就是说局部变量和内部链接属性的常量是不可以当作模板参数构造模板的,并且指针和引用还不能是动态对象的。
  以上是我个人在环境下多次实验得出的理解和总结,环境是gcc 6.3.0,如果有误区还请指出。

模板的特化

  模板可以封装不同的类型做相同的操作,然而有这么一种情况,我想要根据不同的类型做出不同于原来模板的操作,这就需要用到模板的特化这个语法。特化就是在原模版的基础上,根据特殊类型进行特殊化的实现方式。

函数模板的特化

  函数模板特化要求当然要有基本的函数模板,在特化时格式要求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>
using namespace std;
//为了验证函数模板与类模板
//先写一个函数模板
template<class T>
T Add(T a, T b)
{
return a + b;
}
//特化处理
template<>
int Add<int>(int a, int b)
{
cout << "specialization" << endl;
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
}


specialization
3

  这样的特化写法是最为规范的写法,就像是函数模板的显示实例化一样,当然我们也可以将<int>不要,只要能让模板知道你再特化哪一种情况即可,但是要与模板完全符合,不然是编不过的。
  还有一种更为简单的方式,及利用我在模板初阶中提到的模板匹配规则。模板匹配是在所有同名函数都不符合调用的情况下才会进行实例化,因此我们可以用类似重载的情况写同名函数来进行处理,在调用时会优先调用非模板函数。这种情况并不能算是模板的特化处理,但是可以用来处理一些模板无法匹配的情况。

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
#include <iostream>
using namespace std;
template<class T>
T Add(T& a, T& b)
{
return a + b;
}
template<>
int Add(int& a, int& b)
{
cout << "specialization" << endl;
return a + b;
}
int Add(int a, int b)
{
cout << "overload" << endl;
return a + b;
}
int main()
{
int a = 1;
int b = 2;
cout << Add(a, b) << endl;//调用重载的Add
cout << Add<int>(a, b) << endl;//调用特化的Add
}


overload
3
specialization
3

  在这个例子中可以看出根据模板匹配规则确实优先调用了我们重载的函数,我们只有显示实例化才会调用模板。

类模板的特化

  类模板特化与函数模板类似,但是由于类模板是无法通过其他方式识别模板参数的类型的,因此我们必须通过<>来显示实例化才能进行特化。

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
#include <iostream>
using namespace std;
//类模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//特化处理
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
int main()
{
Data<int, int> data1;
Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

特化的种类

全特化

  全特化就是将模板参数全部进行实例特化的情况。这里用类模板举例。

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
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
int main()
{
Data<int, int> data1;
Data<int, char> data2;
}


Data<T1, T2>
Data<int, char>

偏特化

  偏特化就是全特化以外的情况,并没有将所有的模板参数全部都实例化为某一特殊情况。偏特化又有两种情况。
  1、部分特化:部分特化是将模板参数中一部分模板参数进行实例化特化的情况。

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
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
//这里就是一个部分特化,只将第一个参数进行实例化的情况
template<class T>
class Data<int, T>
{
public:
Data()
{
cout << "Data<int, T>" << endl;
}
};
int main()
{
Data<int, int> data1;//这里会调用部分特化
Data<int, char> data2;//这里调用全特化
}



Data<int, T>
Data<int, char>

  通过这个例子还可以证实如果实例化同时符合全特化和部分特化的特化情况,则会优先调用全特化,之后才会考虑部分特化的情况。
  2、将参数进一步限制的特化
  这个类型的特化是将参数进行了进一步的限制,我们可以将参数限制为指针类型或者引用,在这种情况下才会调用这种类型的特化

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
#include <iostream>
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _data1;
T2 _data2;
};
//这样的把所有的模板参数都进行实例特化的就叫全特化
template<>
class Data<int, char>
{
public:
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _data1;
char _data2;
};
//这里就是一个部分特化,只将第一个参数进行实例化的情况
template<class T>
class Data<int, T>
{
public:
Data()
{
cout << "Data<int, T>" << endl;
}
};
//将两个参数类型限制为指针类型,如果两个参数都是指针类型则调用这个特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
};
int main()
{
Data<int, int> data1;//这里会调用部分特化
Data<int, char> data2;//这里调用全特化
Data<int*, char*> data3;//这里调用类型限制的特化
}

类型萃取

  类型萃取是通过特化的方式使模板可以识别不同的类型从而使用不同的处理方法。比如说我们要写一个拷贝函数,对于内置类型我们直接调用memcpy()函数拷贝内存,而对于自定义类型我们可能需要使用自定义的其他拷贝方法进行深拷贝,这时候就可以使用类型萃取的方式来区别内置类型和自定义类型达到不同处理方式的结果。
  这并不是一种新的语法,更像是一种设计模式,在STL中就有所体现。

模板的分离编译

  什么是分离编译模式?分离编译模式就是我们通常在写项目是方便项目管理时所使用的将函数和类的声明放进.h文件中而在.cpp文件中写类定义和类成员函数定义的模式。这种方法可行就是应为我们将声明写入.h而在需要使用的地方引入头文件,在链接过程中编译器会根据声明找到具体实现定义的地址完成链接。

模板不支持分离编译

  但是这里要说的是,无论是函数模板还是类模板在没有实例化之前都是没有具体代码的,那么也就没有具体定义的地址,我们根据声明也就无法链接到定义的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}

  以上这个例子中的代码时编不过的,原因就是模板不支持分离编译,无法连接。

如何解决?

  解决方法也很简单,最简单的就是将定义和声明都写到头文件中可以了,这样就省去了链接这个步骤,也就不存在错误了。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助