Ⅰ . 非类型模板参数
01 什么是非类型模板参数?
STL 中的 array 就有一个非类型模板参数
注意看,我们普通定义的 T 是类型,而 N 这里并不是类型,而是一个常量
类型模板参数定义的是虚拟类型,注重的是你要传什么,而非类型模板参数定义的是变量
"非类型模板参数"
👇
template<class T, size_t N> class array;
👆
"类型模板参数"
02 非类型模板参数的使用场景
假设我们要定义一个静态栈:
#define N 100
template<class T>
class Stack
{
private:
int _arr[N];
int _top;
};
那么我们如果想定义两个容量不同的栈,可以做到嘛?
这里无论 #define 改成 100 还是 500 都无法解决这里的问题
这里只能使用非类型模板参数来解决
代码实现:
template<class T, size_t N>
class Stack {
private:
int _arr[N];
int _top;
};
int main()
{
Stack<int, 100> st1;
Stack<double, 500> st2;
return 0;
}
这里我们在模板这定义一个常量 N,于是我们就可以在实例化时去指定其实例化对象的大小了
这个 N 就是非类型模板参数
03 非类型模板参数不能修改
非类型模板参数是常量,是不能被修改的
template<class T, size_t N>
class Stack {
public:
void f() { // 修改常量试试看
N = 10;
}
private:
int _arr[N];
int _top;
};
int main()
{
Stack<int, 100> st1;
st1.f();
return 0;
}
运行结果如下:
test1711.cpp:10:15: error: lvalue required as left operand of assignment
N = 10;
04 非类型模板参数类型规定
有些类型是不能作为非类型模板参数的,比如浮点数、类对象和字符串
非类型模板参数基本上都是整型
05 STL 中的 array
我们现在再来看 array:
array 是 C++ 11 新增的,那么它有什么特别的地方嘛?
很可惜,基本没有
#include <iostream>
#include <array>
#include <vector>
using namespace std;
int main()
{
vector<int> v1(100, 0);
array<int, 100> a1;
cout << "size of v1: " << sizeof(v1) << endl;
cout << "size of a1: " << sizeof(a1) << endl;
return 0;
}
运行结果如下:
vector 是开在堆上,而 array 是开在堆上
尴尬的是 array 能做的操作 vector 几乎都能做,array 也只是封装过的原生数组罢了
array<int, 100> a1; // 封装过的原生数组
int a2[100]; // 原生数组
比起原生数组,array 的最大优势也只是越界的检擦,读和写都可以检查到是否越界
总结:array 相较于原生数组,有越界检查,实际中还是建议直接用 vector
Ⅱ . 模板的特化
01 给特殊类型准备特殊模板
通常情况下,使用模板可以实现一些与类型无关的代码
但对于一些特殊类型,我们需要一些特殊的处理
代码演示:
#include <iostream>
#include"Date.h"
using namespace std;
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2024, 8, 28);
Date d2(2024, 8, 29);
cout << Less(d1, d2) << endl;
Date* d3 = new Date(2024, 1, 1);
Date* d4 = new Date(2024, 1, 2);
cout << Less(d3, d4) << endl;
return 0;
}
运行结果如下:每次运行的结果都不一样
问题出在没传指针,传 *d3 和 *d4 就能解决
但如果不让你传指针怎么办呢?
这里就需要用到模板的特化,针对特定的类型做特殊化处理
02 模板特化的步骤
首先,需要有一个基础的函数模板
其次,关键字 template 后面接上一堆空的尖括号 <>
然后,函数名后跟上一对尖括号,尖括号中指定需要特化的内容
最后,函数形参表必须要和模板函数的基础参数类型完全相同
代码实现:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2024, 8, 28);
Date d2(2024, 8, 29);
cout << Less(d1, d2) << endl;
Date* d3 = new Date(2024, 1, 1);
Date* d4 = new Date(2024, 1, 2);
cout << Less(d3, d4) << endl;
return 0;
}
运行结果如下:
对于普通类型,它还是会调用正常的模板,对于 Date* 编译器就会发现这里有一个专门为它准备的特化版本,编译器会优先选择该特化版本,这就是模板的特化
那如果我们直接加一个普通函数,会调用哪个呢?
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
// 普通
bool Less(Date* left, Date* right)
{
return *left < *right;
}
函数重载,会直接调用普通函数的版本,因为是现成的,不需要实例化
03 类模板的特化
刚才的函数模板不一定要特化,可以写一个具体的函数
但类模板没法实现一个具体的实际类型,就必须要特化
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "Date<T1 ,T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Date<int, int> d1;
Date<int, double> d2;
return 0;
}
这种情况就需要类模板的特化
代码实现:
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "Date<T1 ,T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 类模板的特化
template<>
class Date<int, double>
{
public:
Date()
{
cout << "Date<int, double>" << endl;
}
};
int main()
{
Date<int, int> d1;
Date<int, double> d2;
return 0;
}
运行结果如下:
04 全特化和半特化
全特化:将模板参数列表中的所有参数全都确定化
...
// 全特化
template<>
class Data<int, double>
{
public:
Data() {
cout << "Data<int, double>" << endl;
}
};
半特化(偏特化):将部分参数列表中的一部分参数特化
...
// 半特化(偏特化)
template<class T1>
class Data<T1, char>
{
public:
Data()
{
cout << "Data<T1, char>" << endl;
}
};
int main()
{
// 只要第二个值是 char 都会匹配到半特化
Data<int, char> d3;
Data<char, char> d4;
return 0;
}
半特化还可以用来对参数进行进一步限制
template<class T1, class T2>
class Date<T1*, T2*>
{
public:
Date()
{
cout << "Date<T1*, T2*>" << endl;
}
};
int main()
{
Date<int*, char*> d3;
Date<char*, string*> d4;
Date<char**, void*> d5;
return 0;
}
运行结果如下:
template<class T1, class T2>
class Date<T1&, T2&>
{
public:
Date()
{
cout << "Date<T1&, T2&>" << endl;
}
};
int main()
{
Date<int&, char&> d6;
return 0;
}
运行结果如下:
Ⅲ . 模板的优缺点
优点:
① 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
② 增强了代码的灵活性。
缺点:
① 模板会导致 "代码膨胀" 问题,也会导致编译时间变长。
② 出现模板编译错误时,错误信息非常凌乱,不易定位错误。
具体分析:
优点:
泛型编程:模板允许你编写通用代码,可以适用于多种数据类型,而不仅仅是特定类型。这使得代码更具通用性和重用性。
类型安全:C++模板系统提供了强类型检查,这有助于捕捉在编译时发生的类型错误,而不是在运行时。
性能:模板生成的代码通常比使用宏或运行时多态的方式更高效。编译器可以生成针对具体类型的优化代码,从而提高性能。
容器和算法库:STL(Standard Template Library)使用了C++模板,提供了丰富的容器和算法,使开发者能够更轻松地使用和操作数据结构。
可扩展性:模板允许你创建自定义数据类型和函数,从而增强C++的可扩展性。
缺点:
编译时间:使用模板可能导致较长的编译时间,因为编译器需要为每个具体的模板实例生成代码。对于大型项目,这可能会导致显著的编译时间增加。
复杂性:模板语法相对复杂,可能对初学者不够友好。编写和维护模板代码需要一定的经验。
错误消息:当涉及到模板的错误发生时,编译器生成的错误消息通常比较晦涩难懂,这增加了调试的难度。
二进制兼容性:在C++中,由于模板的实现方式,对于不同编译器版本、不同编译选项或不同平台之间的二进制兼容性可能存在问题。
代码膨胀:每个不同的模板实例都会生成新的代码,这可能会导致代码膨胀,增加可执行文件的大小。
代码膨胀
代码膨胀(Code bloat)是指代码有着不必要的长度、缓慢或者其他浪费资源的情况。代码膨胀可能是由于编写代码的语言、编译时所用的编译器,或者编写的程序员所致。因此,虽然代码膨胀通常指源代码存在不必要的部分(由程序员导致),但也可指生成的代码或者二进制文件文件有膨胀问题。
在编程中使用了大量的模板、宏或泛型编程技术,导致生成的代码变得冗长、复杂和庞大的现象。这种情况通常出现在C++等编程语言中,其中模板和泛型编程被广泛应用。代码膨胀的主要原因是为了实现通用性和灵活性,程序员使用了大量的模板和泛型类型,以满足各种不同的数据类型和需求。代码膨胀可能会导致以下问题:
编译时间变长:生成大量的冗长代码需要更多的时间来进行编译。编译器需要处理更多的代码,这可能会导致编译时间明显增加,尤其是在大型项目中。
可维护性下降:庞大的代码库更难以维护和理解,因为其中可能包含了大量看似相似但实际上略有不同的代码片段。这会增加错误的引入和修复的难度,降低代码的可维护性。
错误信息混乱:当代码膨胀时,编译器生成的错误信息可能会变得非常混乱,不容易理解。这使得在出现编译错误时很难迅速定位和解决问题,增加了调试的复杂性。
为了避免代码膨胀,程序员可以谨慎使用模板和泛型编程技术,只在必要的情况下使用它们,而不是过度使用。此外,编译器优化和代码重构也可以帮助减少代码膨胀的问题。