Bootstrap

【C++】模板(进阶)

本篇我们来介绍更多关于C++模板的知识。模板初阶移步至:【C++】模板(初阶) 

1.非类型模板参数

1.1 非类型模板参数介绍

模板参数可以是类型形参,也可以是非类型形参。类型形参就是我们目前接触到的一些模板参数。

//类型模板参数
template<class T, class Container = vector<T>, class Compare = Less<T>>

非类型模板参数就是用一个常量作为模板的参数,在模板中可以将该参数当作常量使用。

//N为非类型模板参数
template<class T, size_t N = 10> //N给缺省值
template<size_t N> //N没给缺省值

比如说我们要弄一个数组长度是固定的栈。

template<size_t N>
class Stack
{
private:
	int _a[N]; //定长数组
	//...
};

在使用的时候就可以传想要的N的值。

int main()
{
	Stack<5> st1; //存5个数据
	Stack<10> st2; //存10个数据

	return 0;
}

1.1.1 和C语言宏对比

这个和C语言的宏区别还是很大的,C语言的宏只能存5个或者10个数据,不可以像上面这样st1存5个值,st2又能存10个数据。

#define N 5  //宏,此时st1和st2都是存5个数据
class Stack
{
private:
	int _a[N]; //定长数组
	//...
};

int main()
{
	Stack st1; 
	Stack st2; 
	return 0;
}

1.1.2 底层原理及注意事项

非类型模板参数和传类型模板参数,模板的底层原理都是一样的,都是生成了不同的类,如果是函数模板就是生成了不同的函数

如果非类型模板参数给了缺省值,可以不传参,但是要加上<>。

template<size_t N = 5> //给缺省值
class Stack
{
private:
	int _a[N]; //定长数组
	//...
};

int main()
{
	Stack<> st1; //不传参
	Stack<10> st2; //传参
	return 0;
}

不传参也不加<>的写法在C++20才支持,这里还是建议加上<>。 

注意:

1.非类型模板参数只能用于整形(bool也算整形),浮点数(C++20才支持)、类对象以及字符串是不允许作为非类型模板参数的。

2.. 非类型的模板参数必须在编译期就能确认结果。

 我们也可以有多个非类型模板参数。

template<size_t N = 5, bool fg = true> //给缺省值

1.2 array介绍

array是一个容器,底层就是一个静态的数组,它就用到了非类型模板参数

相关文档:array - C++ Reference  ,使用时包含头文件 #include <array>

第一个模板参数T类型模板参数,第二个模板参数N非类型模板参数。 

array支持迭代器也支持下标访问,array就没有头删尾删、头插尾插这样的接口了,因为它是定长的。

我们来用一下array,假如要定义一个类型为int,长度为10的数组。

array<int, 10> a1;

等同于 int a1[10]; 

array对于数组越界的检查是比较严格的,比如下面的例子。

int a2[10];
cout << a2[10] << endl;//读越界的位置

未报错。

但是 array的越界检查是比较严格的,会直接报错

补充一句:array的数据是存在栈上的,vector的数据是存在堆上的。 

2.模板的特化

我们在使用模板的时候,有些情况下对于一些特殊类型的结果可能不是我们想要的。这时我们就可以使用模板的特化。

2.1 函数模板的特化

比如说我们用函数模板实现两个数的小于比较。

template<class T>
bool Less(T left, T right)
{
	return left < right;
}

在通常情况下这个模板是没有问题的,像传一些int、double这样的内置类型。但是如果传自定义类型可能结果就不是我们想要的,比如说Date类,我们就可以用模板的特化。

函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字 template 后面接一对 空的尖括号<>
3. 函数名后 跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<>
bool Less<Date*>(Date* left, Date* right)
{
	return *left < *right;
}

当我们传参类型为Date*时,就会直接走特化的模板,传别的类型的参数时,还是走函数模板那一套。

2.2 类模板的特化

2.2.1 全特化

全特化就是将模板参数列表中所有的参数都确定化。假如这里有一个如下的类。

template<class T1, class T2>
class Date
{
public:
	Date()
	{
		cout << "Date<T1, T2>" << endl;
	}
};

对这个类全特化就是在类名后面加上<>,<>里面写上具体类型。

template<>
class Date<int, char>
{
public:
	Date()
	{
		cout << "Date<int, char>" << endl;
	}
};

如果我们传int和char类型的参数过去,就直接匹配调用特化了的类,其余情况都调用正常的类模板。

int main()
{
	Date<int, int> d1;
	Date<int, char> d2;

	return 0;
}

2.2.2 偏特化(半特化)

偏特化有两种表现方式,部分特化参数更进一步的限制

部分特化

这种特化就是只特化一部分,如下。

template<class T1>
class Date<T1, char>
{
public:
	Date()
	{
		cout << "Date<int, char>" << endl;
	}
};

只要是第二个参数传的是char类型,就会匹配到这个偏特化(半特化)的类,其余情况还是匹配正常的类模板。

int main()
{
	Date<int, int> d1;
	Date<char, char> d2;
	Date<double, char> d3;
	return 0;
}

当全特化和偏特化(半特化)同时存在时,会优先选择全特化

template<class T1, class T2> //正常类模板
class Date 
{
public:
	Date()
	{
		cout << "Date<T1, T2>" << endl;
	}
};

template<>  //全特化
class Date<int, char>
{
public:
	Date()
	{
		cout << "Date<int, char>" << endl;
	}
};

template<class T1>  //偏特化
class Date<T1, char> 
{
public:
	Date()
	{
		cout << "Date<T1, char>" << endl;
	}
};

int main()
{
	Date<int, char> d2; //优先选择全特化

	return 0;
}

 参数更进一步的限制

这种特化针对模板参数更进一步的条件限制所设计出来的一个特化版本。比如说下面这个。

template<class T1, class T2>
class Date<T1*, T2*>   //指针
{
public:
	Date()
	{
		cout << "Date<T1*, T2*>" << endl;
	}
};

这个特化的意思就是,只要参数传的是指针不管什么类型的指针,都走这个模板。比如说下面的例子。

int main()
{
	Date<int, char> d1;
	Date<int*, char*> d2; //传的指针
	Date<double*, char*> d3; //传的指针
	Date<int*, double*> d4; //传的指针
	Date<char*, int*> d5; //传的指针
	return 0;
}

除了特化成指针,还能特化成引用

template<class T1, class T2>
class Date<T1&, T2&>    //引用
{
public:
	Date()
	{
		cout << "Date<T1&, T2&>" << endl;
	}
};
Date<int&, char&> d6; //传的引用
Date<double&, char&> d7; //传的引用
Date<int&, double&> d8; //传的引用
Date<char&, int&> d9; //传的引用

两者混合也可以,一个指针一个引用。

template<class T1, class T2>
class Date<T1&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1&, T2*>" << endl;
	}
};

易错点

我们先看下面这段代码。

template<class T1, class T2>
class Date<T1*, T2*>
{
public:
	Date()
	{
		cout << "Date<T1*, T2*>" << endl;
		T1 t;
		cout << typeid(t).name() << endl;
	}
};
int main()
{
    Date<int*, int*> d1;
}

代码中的t是什么类型?是 int* 还是 int?

是int。

如果传过去的是int**,t的类型就是int*。

Date<int**, int*> d1;

在特化的时候,除了指针这里会比较容易混淆,引用也是一样,比如下面这个例子。

template<class T1, class T2>
class Date<T1&, T2&>
{
public:
	Date()
	{
		cout << "Date<T1&, T2&>" << endl;
		T1 t;
		cout << typeid(t).name() << endl;
	}
};
void test6()
{
	Date<int&, int&> d1;
}

此时t的类型就是int,而不是int类型的引用。

所以,我们想定义一个指针或者引用的时候应该要像下面这样。

template<class T1, class T2>
class Date<T1&, T2*>
{
public:
	Date()
	{
		cout << "Date<T1&, T2*>" << endl;
		int a = 0;
		T1& t1 = a;  //定义引用
		T2* t2 = &a; //定义指针
		//...
	}
};

因为T1和T2在上面的情况下都是int类型,而不是我们想要的引用和指针。

3.模板分离编译及优缺点

3.1 模板的分离编译

我之前的很多篇博客用到了声明和定义分离的方法,主要是一些像模拟实现这样的代码较多的程序,随着我们更深入的学习,一个程序的代码量是会越来越多的,声明和定义分离可以让我们的代码更有条理。

分离编译:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

程序要运行,一般要经历这几个步骤:预处理 -> 编译 -> 汇编 -> 链接

 而模板,是不支持声明和定义分离的,会发生链接错误。

所以建议模板的声明和定义放在同一个文件中,不要分离。 

3.2 模板的优缺点

【优点】
        1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
        2. 增强了代码的灵活性
【缺陷】
        1. 模板会导致代码膨胀问题,也会导致编译时间变长
        2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误

本次分享就到这里,我们下篇再见~

;