主页
文章
分类
系列
标签
简历
【模板与泛型编程02】类模板
发布于: 2022-6-20   更新于: 2022-6-20   收录于: Cpp
文章字数: 1226   阅读时间: 6 分钟   阅读量:

一、定义类模板

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
//一个简单模板类
template <typename T> 
class Blob 
{ 
public: 
	typedef T value_type; 
	typedef typename std::vector<T>::size_type size_type;
	//构造函数
	Blob(); 
	Blob(std::initializer_list<T> il); // Blob中的元素数目 
	size_type size () const { return data->size(); } 
	bool empty() const { return data->empty(); }
	//添加和删除元素
	void push_back(const T &t) {data->push_back(t);}
	//移动版本
	void push_back(T &&t){data->push_back(std::move(t)); } 
	void pop_back();
	//元亲访问
	T& back(); 
	T& operator [](size_type i);
private:
	std::shared_ptr<std::vector<T>> data; 
	// 若 data [ i]无效,则抛出msg 
	void check(size_type i, const std::string &msg) const;
};

template<typename T>
Blob<T>::check(size_type i, const std::string &msg) const
{
	 //......
}

对于类模板BlobBlob可以称为类名或类模板,而Blob<T>可以称为类型名(Blob后面带了尖括号,所以表示的是一个具体类型),其中的T称为模板参数,T本身代表的也是一个类型(容器中的元素类型),

2、模板参数

2.1 类型参数

同函数模板

2.2 默认参数

2.2.1 常规默认参数

如果某个模板参数有默认值,那么从这个有默认值的模板参数开始,后面的所有模板参数都得有默认值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename T=char , typename U=int>
struct TC
{
	//...
};

int main()
{
	TC<> mytc1; //TC<char,int>
	TC<double> mytc2; //TC<char,double>
}

2.2.2 后面的模板参数依赖前面的模板参数

1
template <typename T,typename U = T*> struct TC{};

2.3 非类型模板参数

1
2
3
4
5
6
template <typename T , typename U , size_t arrsize = 8>
struct TC
{
	T m_arr[arrsize];
	void functest();
};

和函数模板一样,类模板非类型模板参数同样有限制,但是有两点有别于函数模板

  1. 全局指针不能作为模板参数
  2. 字符串常量无法作为模板参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <const char *p>
struct TC
{
	TC(){}	
};

const char* g_s = "hello";

int main()
{
	TC<g_s> mytc1; //×
	TC<"hello"> mytc2; //×
}

3、类型别名

对于模板,类型名往往比较长(实际的项目中往往比这里举例的类型名长很多),所以可以用typedef或using给这些类型名起一个额外的别名以简化书写,下面是使用typedef的方法

1
2
typedef TC<int , float> IF_TC;
IF_TC mytc1;

C++11支持using

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

二、实例化类模板

类模板中,只有被调用的成员函数,编译器才会产生出这些函数的实例化代码。如果类模板中有静态成员函数,那么当这个静态成员函数被调用的时候,也会被实例化。

1、模板参数推断

  • 不同于函数模板,编译器不能推断模板参数类型(C++17中这特特性已经失效)。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息,提供显式模板实参列表,来实例化出特定的类。
  • 一个类模板中所有的实例都形成一个独立的类

2、特化

以下列代码为范例,说明类模板特化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename T, typename U> 
struct TC 
{ 
	TC() {cout << "TC泛化版本构造函数" << endl; } 
	void functest1() { cout << "functest1泛化版本" << endl; } 
	void functest2() ;
};

template <>
void TC<int,int>::functest2(){cout<<"类模板成员函数的类外定义"<<endl;}

int main()
{
	TC<int , float> mytc;
	mytc.functest1();
}

2.1 全特化

概念同函数模板全特化一样,无非就是将模板参数用具体类型替代

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

template <typename T, typename U> 
struct TC 
{ 
	TC() {cout << "TC泛化版本构造函数" << endl; } 
	void functest1() { cout << "functest1泛化版本" << endl; } 
	void functest2() ;
};

template <> 
struct TC<int , int> //T == int , U == int
{ 
	TC() {cout << "TC特化版本构造函数" << endl; } 
	void functest1();
 };

void TC<int ,int>::functest1() { cout << "functest1特化版本的类成员函数类外定义" << endl; } 

int main()
{
	TC<int , int> mytc;
	mytc.functest1();
}

2.2 普通成员函数的全特化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template <typename T, typename U> 
struct TC 
{ 
	TC() {cout << "TC泛化版本构造函数" << endl; } 
	void functest1() { cout << "functest1泛化版本" << endl; } 
	void functest2() ;
};

template<>
void TC<double,double>::functest2()
{
	cout<<"普通成员函数的全特化"<<endl;
}

int main()
{
	TC<double , double> mytc;
	mytc.functest2();
}

2.3 静态成员变量的全特化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T, typename U> 
struct TC 
{ 
	TC() {cout << "TC泛化版本构造函数" << endl; } 
	void functest1() { cout << "functest1泛化版本" << endl; } 
	void functest2() ;
	static int m_stc;
	static int m_sts;
};

//静态成员变量泛化版本定义
template<typename T,typename U> int TC<T,U>::m_stc =50;
//静态成员变量特化版本定义
template<> int TC<int,int>::m_sts = 100;

int main()
{
	TC<int,int> mytc;
	cout<<mytc.m_sts;//c++11,如果对象不是int,int类型实例化,编译器Warning
	cout<<mytc.m_stc;
}

如果进行了普通成员函数的全特化,或者是静态成员变量的全特化,那么就无法用这些全特化时指定的类型对整个类模板进行全特化了。

3、偏特化

3.1 模板参数数量上的偏特化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T, typename U> 
struct TC 
{ 
	TC() {cout << "TC泛化版本构造函数" << endl; } 
};

template<typename U>
struct TC<double,U>
{
	TC() {cout << "TC偏特化构造函数" << endl; } 
	void functest1();
};

template<typename U>
void TC<double ,U>::functest1()
{
	cout << "TC偏特化的成员函数类外定义" << endl;
}
int main()
{
	TC<double , double> mytc;
	mytc.functest1();
}

类模板偏特化版本中的类型模板参数不可以有默认值

3.2 模板参数范围上的偏特化

同函数模板一致

4、C++17中的类模板参数推断

4.1 隐式推断

隐式的推断指南,必须依托于构造函数存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
template <typename T>
struct A
{
	A(T val1, T val2)
	{
		cout<<"A::A(T val1 , T val2)"<<endl;
	}

	A(T val)
	{
		cout<<"A::A(T val)"<<endl;
	}
};

int main()
{
	A obj1(15,16); // A<int> 
	A obj2(17.7); // A<double>
	return 0;
}

4.2 自定义推断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <typename T>
struct B
{
	T m_b;
};

template<typename T> B<T>->B<T>;//自定义模板推断

int main()
{
	B obj{15};
	return 0;
}

自定义推断有严格的使用条件,类B只有普通数据成员(聚合类)

三、类模板的成员

1、成员函数模板

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

template <typename T1>
class A
{
public:
    template <typename T2>
    A(T2 v1, T2 v2);

    template <typename T3>
    void myft(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 v2)
{
    cout<<"A::A(T2,T2)执行了!"<<endl;
}
int main()
{
	A<float> a(1,2);
	a.myft(3);
	A<float> a2(1.1 , 2.2);
	A<float> a3(11.1f , 12.2f);
}
  • 类模板中的成员函数,只有源程序代码中出现调用这些成员函数的代码时,这些成员函数才会出现在一个实例化的类模板中
  • 类模板中的成员函数模板,只有源程序代码中出现调用这些成员函数模板的代码时,这些成员函数模板的具体实例才会出现在一个实例化的类模板中。
  • 目前的编译器并不支持虚成员函数模板,因为虚函数表vtbl的大小是固定的,每个表项里面就是一个虚函数地址。但是成员函数模板只有被调用的时候才能实例化(否则编译器也不知道要用什么模板参数实例化这个成员函数模板)
  • 当函数和函数模板都合适时,编译器会优先选择函数而非函数模板

1.2 拷贝构造函数模板与拷贝赋值运算符模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//续写上一小节类模板
template <typename U>
A(const A<U>& other)
{
	cout <<"A::A(const A<U>& other)拷贝构造函数模板执行了!"<<endl;
}

template <typename U>
A<T1>& operator=(const A<U>& other)
{
	cout <<"operator=(const A<U>& other)拷贝赋值运算符模板执行了"<<endl;
	return *this;
}

拷贝构造函数模板不是拷贝构造函数,拷贝赋值运算符模板不是拷贝赋值运算符(其实这句话也同样适合构造函数模板不是构造函数)。因为拷贝构造函数或拷贝赋值运算符要求拷贝的对象类型完全相同,而拷贝构造函数模板或拷贝赋值运算符模板就没有这种要求。

1
2
3
4
5
6
7
int main()
{
	A<float> a3(1,2);
	a3.m_ic = 16.2f;
	A<float> a4(a3);//当执行A<float> a4(a3);代码行时,实际是要执行类模板A中的拷贝构造函数,但是类模板A中并没有拷贝构造函数,所以,编译器内部实际是执行了按值拷贝的一个动作,使a4.m_ic=16.2f。请记住:拷贝构造函数模板永远不可能成为拷贝构造函数,需要执行拷贝构造函数的地方,绝不会因为拷贝构造函数模板的存在就用对拷贝构造函数模板的调用代替对拷贝构造函数的调用。
	A<int> a5(a3);//类型不同(都是用类模板A实例化出来的类)的两个对象,用一个拷贝构造另外一个时会调用模板拷贝构造函数。因为a5是A<int>类型,a3是A<float>类型,两者类型不同,所以,利用a3构造a5时,就会导致拷贝构造函数模板的执行。
}

1.3 特化

成员函数模板也能被特化,但是具体能特化到什么程度,可能不同编译器的标准不同

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
//在上文中的类中继续添加
template <typename T1>
class A
{
public:
	template <typename U>
	class OtherC
	{
		public:
			void myFunC()
			{
				cout<<"myFunc执行了"<<endl;
			}
	}
};

//把myFunC()成员函数写在类模板A的float全特化版本定义外面
A<float>::OtherC<U>::myFunC(){cout<<"嵌入模板类的成员函数的类外定义"};
//把myFunC()成员函数写在类模板A的float全特化版本定义外面
template <typename T1>
template <typename U>
A<T1>::OtherC<U>::myFunC(){cout<<"嵌入模板类的成员函数的类外定义"};

int main()
{
	A<float>::OtherC<float> myObjC;
	myObjC.myFunc();
}

3、变量模板与成员变量模板(C++14)

变量模板的英文为Variable Templates,是C++14标准引入的。一般这种变量模板需要定义在全局空间或命名空间中(一般也是放在.h头文件中)

1
2
3
4
5
6
7
8
template <typenam T> T g_myvar{};
int main()
{
	g_myvar<float> = 15.6f;
	g_myvar<int> = 13;
	cout<<g_myvar<float><<endl;//15.6
	cout<<g_myvar<int><<endl;//13
}

g_myvar后面跟了一个大括号{},其实这是一种对变量的初始化方式,一般叫零初始化。所谓零初始化,就是数值型变量初始化为0;指针型变量初始化为nullptr;布尔型变量初始化为false,诸如此类(如果不对这些变量进行初始化,那么如果这些变量是局部变量的话,它们的值就可能是任意值)

3.1 变量模板的特化

3.1.1 全特化

1
2
3
4
5
template<>char T g_myvar<double>{};//不需要正在特化类型(double)与这个变量模板类型(char)保持一致
int main()
{
	g_myvar<double> ='2';//g_myvar<double>当作一个char类型的变量使用了,它能够存储的数据范围当然也是与char类型变量一致的
}

3.1.2 偏特化

1
template<typename T> T g_myvar<T *>{120};

3.2 默认模板参数

1
2
3
4
5
6
7
8
template <typename T = int> T g_myvar;
int main()
{
	g_myvar<int> = 13;
	g_myvar<> = 26;
	cout << g_myvar<int><<endl; //13
	cout << g_myvar<> << endl; //26
}

3.3 非类型模板参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T,int value> T g_myvar3[value];
int main()
{
	for(int i = 0; i < 15; ++i) 
	{
		g_myvar3<int, 15>[i] = i;
		/*注意[]中的下标数字<=14,否则下标会越界,g_myvar3<int, 15>的写法一出现,就表示定义了int g_myvar3<int, 15>[15] 这个有15个元素的int类型数组*/ 

	}
}

4、别名模板与成员别名模板

别名模板的英文叫作Alias Templates,是C++ 11标准引入的,引入的目的是不但能简化书写,而且可以达到通过其他手段很难实现的效果,一般都是通过using实现别名模板

1
2
3
4
5
6
7
8
#include <map>
template <typename T> using str_map_t = std::map<std::string,T>;
int main()
{
	str_map_t <int> map1;
	map1.insert({"first",1});
	map2.insert({"second",2});
}

5、模板模板参数

模板模板参数的英文为Template Template Parameters,名字比较绕嘴,就是让模板参数本身成为模板的意思。

  • int是一个类型(简单类型/内部类型)
  • vector或list是C++标准库中的容器,具体称呼应该是类模板(类名),而诸如vector<int>list<double>就属于模板被实例化了的产物,称为类型(类类型)

假设一个需求,创建一个叫作myclass的类模板,这个类模板中,有一个成员变量myc,这个成员变量是一个容器(可能是一个vector或list,或者其他容器),希望在实例化myclass类模板时能够通过模板参数指定myc是什么类型的容器,以及指定这个容器中所装的元素类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename T , 
typename<class> class Container = std::vector>
/*
Container代表的是一个类模板(类名),把类模板(而不是类型)当作一个参数传递到myclass中,这种模板参数就不叫作“类型模板参数”,而是叫作“模板模板参数”
*/
class myclass
{
	public:
	Container<T> myc;
};

template<class> class Container,是myclass这个类模板的模板参数,而Container本身也是一个模板,所以Container就叫“模板模板参数”

6、共用体模板(联合模板)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename T , typename U> union myuni
{
	T carnum;
	U cartype;
	U cname[60];
};

int main()
{
	myuni<int , char> myu ;
	myu.carnum =156;
}