Ⅲ. 函数模板实例化
0x00 引入:这些不同类型的Swap函数是怎么来的
int a = 0, b = 1; Swap(a, b);
编译器在调用 Swap(a, b) 的时候,发现 a b 是整型的,编译器就开始找,
虽然没有找到整型对应的 Swap,但是这里有一份模板 ——
template<typename T> // 大家好我是模板,飘过~ void Swap(T& rx, T& ry) { T tmp = rx; rx = ry; ry = tmp; }
这里要的是整型,编译器就通过这个模板,推出一个 T 是 int 类型的函数。
这时编译器就把这个模板里的 T 都替换成 int,生成出一份 T 是 int 的函数。
char e = 'e', f = 'f'; Swap(e, f);
一样的,如果要调用 Swap(e, f) ,e f 是字符型,编译器就会去实例化出一个 char 的。
你调的函数还是那些函数,只是你写一份模板出来,让编译器去用模板生成那些函数。
前面注意事项那里我们说过,函数模板本身不是函数。
它是是编译器使用方式产生特定具体类型函数的模具,在编译器编译阶段,
对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。
比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,
将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。
0x01 转到反汇编观察
🐞 我们刚才调试的时候在监视窗口已经看到了,它们的值成功交换了。
现在我们再调试一次,这次转到反汇编,去验证一下编译器通过模板生成函数这件事:
0x01 模板实例化的定义
模板将我们本来应该要重复做的活,交给了编译器去做。
编译器不是人,它不会累,让编译器拿着模板实例化就完事了。
用手搓衣服舒服,还是用洗衣机洗舒服?
自己手写舒服,还是编译器自己去生成舒服?
📚 用不同类型的参数使用模板参数时,成为函数模板的实例化。
模板参数实例化分为:隐式实例化 和 显式实例化 ,下面我们来分别讲解一下这两种实例化。
0x02 模板的隐式实例化
📚 定义:让编译器根据实参,推演模板函数的实际类型。
我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。
💬 现在我们再举一个 Add 函数模板做参考:
#include <iostream> using namespace std; template<class T> T Add(const T& x, const T& y) { return x + y; } int main(void) { int a1 = 10, a2 = 20; double d1 = 10.1, d2 = 20.2; cout << Add(a1, a2) << endl; cout << Add(d1, d2) << endl; return 0; }
❓ 现在思考一个问题,如果出现 a1 + d2 这种情况呢?实例化能成功吗?
Add(a1, d2);
这必然是失败的, 因为会出现冲突。一个要把它实例化成 int ,一个要把它实例成 double,
💡 解决方式
① 传参之前先进行强制类型转换,非常霸道的解决方式:
template<class T> T Add(const T& x, const T& y) { return x + y; } int main(void) { int a1 = 10, a2 = 20; double d1 = 10.1, d2 = 20.2; cout << Add(a1, a2) << endl; cout << Add(d1, d2) << endl; cout << Add((double)a1, d2) << endl; return 0; }
② 写两个参数,那么返回的参数类型就会起决定性作用:
#include <iostream> using namespace std; template<class T1, class T2> T1 Add(const T1& x, const T2& y) { // 那么T1就是int,T2就是double return x + y; // 范围小的会像范围大的提升,int会像double "妥协" } // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转 int main(void) { int a1 = 10, a2 = 20; double d1 = 10.1, d2 = 20.2; cout << Add(a1, d2) << endl; // int,double 👆 return 0; }
当然,这种问题严格意义上来说是不会用多个参数来解决的,
这里只是想从语法上演示一下,我们还有更好地解决方式,我们继续往下看。
③ 我们还可以使用 "显式实例化" 来解决:
Add<int>(a1, d2); // 指定实例化成int Add<double>(a1, d2) // 指定实例化成double
我们下面先来详细介绍一下显式实例化,然后再回来看看它是如何解决的。
0x03 模板的显式实例化
📚 定义:在函数名后的 < > 里指定模板参数的实际类型。
简单来说,显式实例化就是在中间加一个尖括号 < > 去指定你要实例化的类型。
(在函数名和参数列表中间加尖括号)
函数名 <类型> (参数列表);
💬 代码:解决刚才的问题
template<class T> T Add(const T& x, const T& y) { return x + y; } int main(void) { int a1 = 10, a2 = 20; double d1 = 10.1, d2 = 20.2; cout << Add(a1, a2) << endl; cout << Add(d1, d2) << endl; cout << Add<int>(a1, d2) << endl; // 指定T用int类型 cout << Add<double>(a1, d2) << endl; // 指定T用double类型 return 0; }
🚩 运行结果:
🔑 解读:
像第一个 Add(a1, a2) ,a2 是 double,它就要转换成 int 。
第二个 Add(a1, a2),a1 是 int,它就要转换成 double。
这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。
像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。
如果无法成功转换,编译器将会报错。
🔺 总结:
函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
你也可以选择去显式实例化,去指定具体的类型。
0x04 模板参数的匹配原则
我们还是用刚才的 Add 函数模板来举例,现在我需要对整型的 a1 和 a2 进行加法操作:
template<class T> T Add(const T& x, const T& y) { return x + y; } int main(void) { int a1 = 10, a2 = 20; cout << Add(a1, a2) << endl; return 0; }
我们是通过这个 Add 函数模板,生成 int 类型的加法函数的。
💬 如果我们有一个现成的、专门用来处理 int 类型加法的函数:
// 专门处理int的加法函数 int Add(int x, int y) { return x + y; } // 通用加法函数 template<class T> T Add(const T& x, const T& y) { return x + y; } int main(void) { int a1 = 10, a2 = 20; cout << Add(a1, a2) << endl; return 0; }
❓ 思考:如果你是编译器,当 Add(a1, a2) 时你会选择用哪一个?
是用函数模板印一个 int 类型的 Add 函数,还是用这现成的 Add 函数呢?
我们继续往下看……
📚 匹配原则:
① 一个非模板函数可以和一个同名的模板函数同时存在,
而且该函数模板还可以被实例化为这个非模板函数:
// 专门处理int的加法函数 int Add(int x, int y) { cout << "我是专门处理int的Add函数: "; return x + y; } // 通用加法函数 template<class T> T Add(const T& x, const T& y) { cout << "我是模板参数生成的: "; return x + y; } int main(void) { int a1 = 10, a2 = 20; cout << Add(a1, a2) << endl; // 默认用现成的,专门处理int的Add函数 cout << Add<int>(a1, a2) << endl; // 指定让编译器用模板,印一个int类型的Add函数 return 0; }
② 对于非模板函数和同名函数模板,如果其他条件都相同,
在调用时会优先调用非模板函数,而不会从该模板生成一个实例。
如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
// 专门处理int的加法函数
int Add(int x, int y) {
cout << "我是专门处理int的Add函数: ";
return x + y;
}
// 通用加法函数
template
T1 Add(const T1& x, const T2& y) {
cout << "我是模板参数生成的: ";
return x + y;
}
int main(void)
{
cout << Add(1, 2) << endl; // 用现成的
//(与非函数模板类型完全匹配,不需要函数模板实例化)
cout << Add(1, 2.0) << endl; // 可以,但不是很合适,自己印更好
//(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)
return 0;
}
// 专门处理int的加法函数 int Add(int x, int y) { cout << "我是专门处理int的Add函数: "; return x + y; } // 通用加法函数 template<class T1, class T2> T1 Add(const T1& x, const T2& y) { cout << "我是模板参数生成的: "; return x + y; } int main(void) { cout << Add(1, 2) << endl; // 用现成的 //(与非函数模板类型完全匹配,不需要函数模板实例化) cout << Add(1, 2.0) << endl; // 可以,但不是很合适,自己印更好 //(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数) return 0; }
Ⅳ. 类模板
0x00 引入:和本篇开头本质上是一样的问题
💬 就比如 Stack,如果我们定它是 int,那么它就是存整型的栈:
class Stack { public: Stack(int capacity = 4) : _top(0) , _capacity(capacity) { _arr = new int[capacity]; } ~Stack() { delete[] _arr; _arr = nullptr; _capacity = _top = 0; } private: int* _arr; int _top; int _capacity; };
❓ 如果我想改成存 double 类型的栈呢?
当时我们在讲解数据结构的时候,是用 typedef 来解决的。
typedef int STDataType; class Stack { public: Stack(STDataType capacity = 4) : _top(0) , _capacity(capacity) { _arr = new int[capacity]; } ~Stack() { delete[] _arr; _arr = nullptr; _capacity = _top = 0; } private: STDataType* _arr; int _top; int _capacity; };
如果需要改变栈的数据类型,直接改 typedef 那里就可以了。
这依然是治标不治本,虽然看起来就像是支持泛型一样,
它最大的问题是不能同时存储两个类型,你就算是改也没法解决:
int main(void) { Stack st1; // 存int数据 Stack st2; // 存double数据 return 0; }
你只能做两个栈,如果需要更多的数据类型……
那就麻烦了,你需要不停地CV做出各种数据类型版本的栈:
class StackInt {...}; class StackDouble {...}; ……
这和文章开头提到的问题(Swap)本质上是一个问题,就是不支持泛型。
它们类里面的代码几乎是完全一样的,只是类型的不同。
函数我们可以使用模板,类也是可以的,我们下面就来讲解一下类模板。
0x01 类模板的定义格式
📚 定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < > :
template<class T1, class T2, ..., class Tn> class 类模板名 { 类内成员定义 }
💬 代码:解决刚才的问题
template<class T> class Stack { public: Stack(T capacity = 4) : _top(0) , _capacity(capacity) { _arr = new T[capacity]; } ~Stack() { delete[] _arr; _arr = nullptr; _capacity = _top = 0; } private: T* _arr; int _top; int _capacity; }; int main(void) { Stack st1; // 存储int Stack st2; // 存储double return 0; }
但是我们发现,类模板他好像不支持自动推出类型,
它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用。
函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。
你定义一个类,它能推吗?没这个能力你知道吧!
所以这里只支持显示实例化,我们继续往下看。
0x02 类模板实例化
基于上面的原因,我们想要对类模板实例化,我们可以使用显示实例化。
类模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。
类名 <类型> 变量名;
💬 代码演示:解决刚才的问题
template<class T> class Stack { public: Stack(T capacity = 4) : _top(0) , _capacity(capacity) { _arr = new T[capacity]; } ~Stack() { delete[] _arr; _arr = nullptr; _capacity = _top = 0; } private: T* _arr; int _top; int _capacity; }; int main(void) { Stack<int> st1; // 指定存储int Stack<double> st2; // 指定存储double return 0; }
📌 注意事项:
① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。
template<class T> class Stack {...};
类模板名字不是真正的类,而实例化的结果才是真正的类。
② Stack 是类名,Stack 才是类型:
Stack<int> s1; Stack<double> s2;
0x03 类外定义类模板参数
❓ 思考问题:下面的 Push 为什么会报错?
template<class T> class Stack { public: Stack(T capacity = 4) : _top(0) , _capacity(capacity) { _arr = new T[capacity]; } // 这里我们让析构函数放在类外定义 void Push(const T& x); ~Stack(); private: T* _arr; int _top; int _capacity; }; /* 类外 */ void Stack::Push(const T& x) { ❌ ... }
🔑 解答:
① Stack 是类名,Stack 才是类型。这里要拿 Stack 去指定类域才对。
② 类模板中的函数在类外定义,没加 "模板参数列表" ,编译器不认识这个 T 。类模板中函数放在类外进行定义时,需要加模板参数列表。
这段代码第一个问题是没有拿 Stack 去指定类域,
最大问题其实是编译器压根就不认识这个T!
即使你用拿类型 Stack 指定类域,编译器也一样认不出来:
我们拿析构函数 ~Stack 来演示一下:
💬 代码演示:我们现在来看一下如何添加模板参数列表!
template<class T> class Stack { public: Stack(T capacity = 4) : _top(0) , _capacity(capacity) { _arr = new T[capacity]; } // 这里我们让析构函数放在类外定义 ~Stack(); private: T* _arr; int _top; int _capacity; }; // 类模板中函数放在类外进行定义时,需要加模板参数列表 template <class T> Stack<T>::~Stack() { // Stack是类名,不是类型! Stack<T> 才是类型, delete[] _arr; _arr = nullptr; _capacity = _top = 0; }
这样编译器就能认识了。
本章完!