C++ Primer Plus 学习笔记——模板
函数模板
1、什么是函数模板?
模版使用泛型来定义函数,泛型可以用具体的类型(如int、double等)替换。
(1)函数模板允许以任意类型的方式来定义函数
通过将类型作为参数传递给模板,可是编译器生成该类型的函数。——也称通用编程
(2)模板的好处
减少工作量。对于同一算法的不同数据类型实现,可以避免程序员多次手动编写、修改代码,进而避免了在修改代码时犯下的一系列错误。
(3)模板的应用
需要多个将同一种算法用于不同类型的函数,请使用模板。
(4)模板的局限性
编写的模板函数很可能无法处理某些类型。
(这里就有一个疑问,既然存在特殊化,干吗还要模板呢?直接用非模板的多好。)
// 举例
template <typename T>
void f(T a, T b){...if(a>b)...}
// 如果 T 是int,则if 判断语句是有意义的。那 T 换成数组、结构等呢?模板就失效了。
解决方法有2种:
- C++重载运算符,以便能够将其用于特定的结构或类。
- 为特定类型提供具体化的模板定义。
2、模板的使用方法
关键词 template 和 typename 是必须的!(在C++98 标准及其以前,使用关键词 class 来代替 typename)
必须使用尖括号!
在不同的文件(或转换单元)之间编程时,模板声明和定义的位置需要注意,有三种方案:
- 在实例化要素中让编译器看到模板定义;
- 用另外的文件来显式地实例化类型,这样连接器就能看到该类型。
- 使用export关键字。
/* 下面是一个模板的例子 */
/* 函数声明 */
template <typename T> // 这里的 T 可以表示任意类型。函数声明需要这句话!
void lizi(T &a, T &b, int k); // *** 模板函数中的参数,不一定必须都是模板参数类型!***
/* 函数定义 */
template <typename T> // 这里的 T 可以表示任意类型。函数定义也需要这句话!
void lizi(T &a, T &b, int k){ T temp; temp = a; a = b + k; b = temp; }
注意:
(1)函数模板并不创建函数!而是告诉编译器在调用该函数时,应该根据实际使用的类型来定义函数,此时才算创建了函数。So,最终的代码不包含任何模板,只包含了为程序生成的实际函数。
(2)考虑向低版本兼容的话,可以用 class 替换 typename。
(3)常见的是把模板放在头文件中,在使用时包含头文件。
3、函数重载的模板
被重载的模板的函数特征标(参数列表)必须不同!
/* 下面是一个函数重载的模板的例子 */
template <typename T>
void swap(T &a, T &b);
template <typename T>
void swap(T *a, T *b, int n);
4、具体化和实例化
(1)显式具体化的模板函数:
提供一个具体化的函数模板,包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
显式具体化声明在关键字 template 后包含 <>,而显式实例化没有。
template <>
void swap<int>(int &, int &); // 显式具体化
template <>
void swap(int &, int &); // 显式具体化
(2)显式实例化的模板函数:(这一部分知识点存疑,待定)
- 直接命令编译器创建特定的实例。其语法是,声明所需的种类——用<>符号指示类型,并在声明前加上关键字 template。
template void swap<int>(int, int); // 显式实例化
- 还可通过在程序中使用函数来创建显式实例化。
template <class T>
T Add(T a,T b){return a + b;}
int m=6;
double x = 10.2;
cout << Add<double>(x, m)<< endl; // 显式实例化
这里的模板 Add(T a,T b) 与函数调用 Add(x,m) 不匹配,因为该模板要求两个函数参数的类型相同。但通过使用 Add(x, m),可强制为double 类型实例化,并将参数m强制转换为double 类型,以便与函数 Add(double, double)的第二个参数匹配。
注意:
试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错!
禁止在main函数中进行实例化。
(3)隐式实例化的模板函数:
编译器通过模板生成的函数定义。
#include<iostream>
#include<string>
using namespace std;
struct job {
int jobname = 0;
int jobnum = 0;
};
template <typename T>
void myswap(T&, T&); // 普通模板
template <typename T>
void myswap(T& a, T& b) {
T c = a;
a = b;
b = c;
}
template <> void myswap<job>(job &, job &); // 显式具体化
template <> void myswap<job>(job& job1, job& job2) {
int i = job1.jobname;
int j = job1.jobnum;
job1.jobname = job2.jobname;
job1.jobnum = job2.jobnum;
job2.jobname = i;
job2.jobnum = j;
}
template <> void myswap<char>(char&, char&);
template <> void myswap<char>(char& c, char& d) {
char e = c;
c = d + 1;
d = e + 1;
}
int main()
{
short a = 21, b = 19;
cout << "a=" << a << ";" << "b=" << b << endl;
myswap(a, b); // 隐式实例化
cout << "a=" << a << ";" << "b=" << b << endl;
job job1 = { 11, 11 };
job job2 = { 22, 22 };
cout << "job1.name=" << job1.jobname << ";" << "job2.name=" << job2.jobname << endl;
myswap(job1, job2);
cout << "job1.name=" << job1.jobname << ";" << "job2.name=" << job2.jobname << endl;
char c = 'a';
char d = 'b';
cout << "c=" << c << ";" << "d=" << d << endl;
myswap(c, d);
cout << "c=" << c << ";" << "d=" << d << endl;
return 0;
}
5、 选择哪个函数版本呢?
(1)对于函数重载、函数模板、函数模板重载,C++的一个策略是重载解析:
1️⃣创建候选函数列表:包括同名函数和模板函数;
2️⃣在候选函数列表中创建可行函数列表:这些都是参数数目正确的函数,为此有一个隐式转换序列,其中当然也包括完全匹配的情况。
3️⃣确定是否有最佳的可行参数。最佳到最差的顺序:
- 完全匹配——要考虑到常规函数优于模板。
- 提升转换——(例如:char、short转换为int,float转换为double)
- 标准转换——(例如:int转换为char,long转换为double)
- 用户定义的转换,如类声明中定义的转换。
(2)完全匹配和最佳匹配
struct blot {int a; char b[10];};
blot ink = {25,"spots"};
recycle(ink);
// 在这种情况下,下面的原型都是完全匹配的:
void recycle(b1ot); // #1 blot-to-blot
void recycle(const blot); // #2 blot-to- (const blot)
void recycle(blot &); // #3 blot-to- (blot &)
void recycle(const blot &); // #4 blot-to- (const blot &)
- 如果有多个匹配的原型,无法确定最佳的可行函数,就会报错:ambiguous,二义性。
- 有例外:两个函数都完全匹配,仍可完成重载解析:**指向非常量数据的指针、引用,可以优先的与非常量指针、引用参数匹配。**在上面的例子里,如果只定义了函数#3和#4,则将选择#3,因为 ink 没有被声明为 const 。如果只定义了函数#1和#2,则将出现二义性。
- 一个完全匹配优于另一个的另一种情况是:
- 其中一个是模板函数,而另一个不是。在这种情况下, 非模板函数将优先于模板函数(包括显式具体化)。
- 如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如,这意味着显式具体化将优于使用模板隐式生成的具体化。较具体的意思是,编译器在推断使用哪种类型时执行的转换最少。
template <c1ass Type> void recycle (T t); // #1
template <class Type> void recycle (T *t); // #2
struct blot {int a; char b[10];};
blot ink = {25,"spots"};
recycle(&ink);
// 假设recycle(&ink)调用与#1的模板匹配,匹配时将T解释为 blot*
// 假设recycle(&ink)调用与#2的模板匹配,匹配时将T解释为 blot
// 在 #2 的模板中,T已经被具体化为指针,因此说它更具体。
(3)自己选择
自己编写合适的函数调用,引导编译器作出自己希望的选择。
将模板函数定义放在文件开头,从而无需提供模板原型。
template<class T> // or template <typename T>
T lesser(T a,T b){……} // #1
int lesser(int a, int b){……} // #2
int main(){
int m = 20; int n = -30; double x = 15.5; double y = 25.9;
lesser(m,n); // 调用#2
lesser(x,y); // 调用#1,T为double
lesser<>(m,n); // 调用#1,T为int
lesser<int>(x,y); // 调用#1,T为int,double型的x和y被强制转换
}
lesser<>(m,n);中的 <> 指出:编译器应选择模板函数,而不是非模板函数。
6、 模板函数的发展
C++98标准:
- 对于给定的函数名,可以有非模板函数、模板函数、显式具体化模板函数以及它们的重载版本。
- 显式具体化的原型和定义应以 template<> 打头,并通过名称来指出类型。
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
缺陷:
template<class T1, class T2>
void ft(Tl x,T2 y){
...
?type? xpy=x+y;
...
} // 在不知道 T1 和 T2 的类型时,很难确定 xpy 的类型。可能是T1,T2或者其他类型。
C++11新增的关键字:decltype
int x;
decltype(x) y; // 使得 y 的类型和 x 的类型一样
decltype(y+y) xpy; // 给decltype的参数可以是表达式,包括算术表达式、函数调用等
实际上,decltype 的原理要复杂的多:为了确定参数的类型,编译器必须遍历一个核对表:(简化)
对于:decltype(expression) var;
1️⃣如果 expression 是一个没有用括号括起的标识符(decltype本身的括号不算),则 var 的类型与 expression 类型相同,包括 const 等限定符。
double x = 5.5;
double y = 7.9;
double &rx = x;
const double * pd;
decltype(x) w; // w is type double
decltype(rx) u = y; // u is type double &
decltype(pd) v; // v is type const double *
2️⃣如果 expression 是一个函数调用,则 var 的类型与函数的返回类型相同。
注意:这个过程中,并不会真正的去调用函数,只是会查看函数的原型以获得返回类型。
long indeed(int) ;
decltype (indeed(3)) m; // m is type int
3️⃣如果 expression 是一个左值,则 var 为指向其类型的引用。
注意!此处已经是第三步了,我们是从第一步开始判断的,符合条件就结束了~所以啊,这里进入第三步的情况是:当expression 是用括号括起来的标识符:
double xx = 4.4;
decltype((xx)) r2 = xx; // r2 is double &
decltype(xx) w = xx; // w is double (此种情况在第一步就判断出来了)
// 括号并不会改变表达式本身的性质。
double (xx) = 4.4; // 等价于 double xx = 4.4;
4️⃣如果前面的条件都不满足,则 var 的类型与 expression 的类型相同:
int j = 3;
int &k = j
int &n = j;
decltype(j+6) i1; // i1 type int
decltype(100L) i2; // i2 type 1ong
decltype(k+n) i3; // 13 type int;
// 虽然 k 和 n 都是引用。但表达式 k+n 却不是引用,它是两个int的和,因此类型为 int。
C++11后置返回类型
auto 新增的一个功能。
decltype 的缺陷:无法预先知道将 x 和 y 相加得到的类型。
template<class Tl, class T2>
?type? gt(Tl x,T2 y){
...
return x + y;
}
// 新增语法
double h(int x, float y);
auto h(int x, float y) -> double; // 将返回类型移到参数声明的后面。
auto h(int x, float y) -> double{ // 这种语法也可用于函数定义
……
}
-
->double 被称为后置返回类型。
-
auto 是一个占位符,表示后置返回类型提供的类型。
通过结合使用这种语法和 decltype ,便可指定返回类型:
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y){
...
return x + y;
}
类模板
1、定义类模板
template<class Type> class ClassName{} // 旧版本,用 class
template<typename Type> class ClassName{} // 新版本,用 typename 避免与类的关键词class 混淆
Type 是一个通用的类型说明符,模板被调用(实例化)时,Type 将具体的类型值代替。
可以使用模板成员函数替换原有类的类方法,要求:每个函数头都将以相同的模板声明打头,且类限定符也需要加上 。当然,在类中定义,限定符和模板前缀都可以省略!
template<typename Type> class ClassName{
public:
bool push(const Type &);
// bool push(const Type & item){……}
}
// bool ClassName::push(const Type & item){……}
template<typename Type> bool ClassName<Type>::push(const Type & item){……}
模板不是函数,它不能单独编译,模板必须与特定的模板实例化请求一起使用。
由于C++11中不再支持 export 以前的功能,不能将模板成员函数放在独立的实现文件中,最简单的方法就是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
2、使用模板类
需要声明一个类型为模板类的对象,方法是使用显式地所需要的具体类型替换泛型名(类型参数,如此处的Type)。
格式:类名 <具体的类型参数1, 具体的类型参数2, …> 该类的实例化对象名;
【注】和模板函数的区别,必须是显式地。
3、深入探讨模板类
(1)使用指针栈(指针作为类型参数)
方法:让调用程序创建一个指针数组,其中每个指针都指向不同的字符串。栈的任务是管理指针,而不是创建指针。
(2)数组模板示例和非类型(也称:表达式)参数
Q:如何创建一个允许指定数组大小的简单数组模板?
- 方法一:在类中使用动态数组和构造函数参数来提供元素数目;
- 方法二:使用模板参数来提供常规数组的大小。
// Array 模板
#include<iostream>
#include<cstdlib>
template<class T, int n> class ArrayTP{
private:
T ar[n];
public:
ArrayTP(){};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
}
template<class T, int n> ArrayTP<T,n>::ArrayTP(const T & v){
for(int i=0; i<n; i++) ar[i] = v;
}
template<class T, int n> T & ArrayTP<T,n>::operator[](int i){
if(i<0 || i>=n){
std::cerr<<"错误,下标: "<< i << " 越界了"<<std::endl;
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template<class T, int n> T ArrayTP<T,n>::operator[](int i)const{
if(i<0 || i>=n){
std::cerr<<"错误,下标: "<< i << " 越界了"<<std::endl;
std::exit(EXIT_FAILURE);
}
return ar[i];
}
template<class T, int n> 中的 int n 这种参数(指定特殊的类型而不是用作泛型名)称为非类型或表达式参数。
-
表达式参数有一些限制:
- 可以是整型、枚举、引用或指针,如double就不可以,而double*可以。
- 不可以是表达式,如n++,&n等。
- 实例化模板时,用作表达式参数的值必须是常量表达式。
-
优点:为自动变量维护内存占栈,而不是使用new出来的堆上内存。
-
缺点:每种数组大小都将生成自己的模板。下面的声明将生成两个独立的类声明:
ArrayTP<double, 12>eggweights;
ArrayTP<double, 13>donuts;
4、模板的多功能性
(1)模板类可用作基类、组件类、其他模板的类型参数
template<typename T> class Array{
private:
T entry;
...
};
template<typename Type> class GrowArray: pulic Array<Type>{...}; // 基类,继承
template<typename Tp> class Stack{
Array<Tp> ar; // 组件类
...
};
...
Array<Stack<int>> asi; // 用作其他模板类
上面最后一句代码:C++98中,要求至少使用一个空白字符将两个 > 符号分开,以免与运算符 >> 混淆,C++11则不要求。
(2)可以递归使用模板
vector<vector<int>> a;
(3)使用多个类型参数
#include<iostream?
#include<string>
template<class T1, class T2> class Pair{
private:
T1 a;
T2 b;
public:
T1 & first();
T2 & Second();
T1 first() const{return a;}
T2 Second() const{return b;}
Pair(const T1 & aval, const T2 & bval):a(aval), b(bval){}
Pair() {}
};
template<class T1, class T2> T1 & Pair<T1,T2>::first(){return a;}
template<class T1, class T2> T2 & Pair<T1,T2>::Second(){return b;}
int main(){
using std::cout;using std::endl;using std::string;
Pair<string, int> rating[2] = { // 类名是 Pair<string, int>
Pair<string, int>("1", 1); // 而不是 Pair
Pair<string, int>("2", 2); // Pair(char*, double)是另一个完全不同的类名称
};
int joints = sizeof(ratings)/sizeof(Pair<string, int>);
...
return 0;
}
(4)默认类型模板参数
template<class T1, class T2 = int> class Topo{...};
// 如果省略 T2 的值,编译器将使用 int
Topo<double, double> m1; // T1 为 double, T2 为 double;
Topo<double> m2; // T1 为 double, T2 为 int;
5、模板的具体化
具体化:隐式实例化、显式实例化、显式具体化。
模板以泛型的方式描述类,具体化是使用具体的类型生成类声明。
(1)隐式实例化
声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。
ArrayTP<double, 30> *pt; // 隐式实例化.编译器在需要对象前,不会生成类的隐式实例化:
pt = new ArrayTP<double, 30>;/ // 现在需要了
(2)显式实例化
当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化。
声明必须位于模板定义所在的名称空间中。
template class ArrayTP<string, 100>; // 生成 ArrayTP<string, 100> 类
在这种情况下,虽然没有创建或提及类对象,编译器也将生产类声明(包括方法定义)。
(3)显式具体化
是特定类型(用于替换模板中的泛型)的定义。因为有时候需要为特殊类型修改模板,所以索性单独显式的具体化。具体化模板定义的格式:
template <> class ClassName<specialized-type-name>{...};
// 通用模板
template<typename T>class SortedArray{...};
// 显式具体化
template <> class SortedArray<const char*>{...};
(4)部分具体化
部分限制模板的通用性。可以给类型参数之一指定具体的类型:
template<class T1, class T2> class Pair{...}; // 通用模板
template<class T1> class Pair<T1, int>{...}; // 部分具体化
☆ 从这里可以看出:template 后面尖括号<>里的类型参数是没有被具体化的,具体化的写在后面。
- 如果有多个模板可供选择,具体化程度越高,越优先选择;
Pair<double, double> p1;
Pair<double, int> p2;
pair<int, int> p3;
- 可以通过为指针提供特殊版本来部分具体化现有的模板;
template<class T> class Feeb{...}; // 通用模板
template<class T*> class Feeb{...}; // 指针具体化版本
Feeb<char> fb1; // 使用通用模板,其中 T = char
Feeb<char *> fb2; // 使用指针具体化版本,其中 T = char
- 部分具体化能够设置各种限制:
template <class T1, class T2, class T3> class Trio{...}; // 1
template <class T1, class T2> class Trio<T1, T2, T2>{...}; // 2
template <class T1> class Trio<T1, T1*, T1*>{...}; // 3
Trio<int, short, char*> t1; // 使用 1 通用模板
Trio<int, short, short> t2; // 使用 2 Trio<T1, T2, T2>
Trio<char, char*, char*> t3; // 使用 3 Trio<char, char*, char*>
6、成员模板
模板可用作结构、类或模板类的成员。
template <typename T> class beta{
private:
template <typename V> class hold{ // 模板类
private:
V val;
public:
hold(V v=0):val(v){};
void show() const{cout<<val<<endl;}
V value() const{return val;}
};
hold<T> q; // 模板对象
hold<int> n;
public:
beta(T t, int i):q(t), n(i){}
template <typename U> U blab(U u, T t){ // 模板成员函数
return (n.value() + q.value()*u/t;)
}
void Show() const{...};
}
7、将模板用作参数
模板新增特性,用于实现STL。
template< template<typename T> class Thing> class Crab{};
8、模板类友元
模板类声明可以有友元。模板的友元分三类:非模板友元;约束模板友元;非约束模板友元。
(1)非模板友元
(2)约束模板友元
(3)非约束模板友元
9、模板别名(C++11)
可使用 typedef 为具体化后的模板指定别名:
typedef std::array<double, 12> arrd_12;
typedef std::array<int, 12> arri_12;
typedef std::array<std::string, 13> arrstr_13;
arrd_12 name1; // name1 是类型 std::array<double, 12>
arri_12 name2; // name2 是类型 std::array<int, 12>
arrstr_13 name3; // name3 是类型 std::array<std::string, 13>
C++11新增了:使用模板提供一系列别名:
template<typename T> using arrtype = std::array<T,12>; // array<T> 表示类型 array<T, 12>
arrtype<int> name1; // name1 是类型 std::array<int, 12>
arrtype<double> name2; // name2 是类型 std::array<double, 12>
arrtype<std::string> name3; // name3 是类型 std::array<std::string, 12>
C++11允许将 using = 用于非模板,用于不是模板的情况时,该语法与常规 typedef 等价:
typedef const char* pc1;
using pc2 = const char*; // pc1 和 pc2 都是 const char*
10、可变参数模板(C++11)
即:可接受可变数量的参数。
要创建可变参数模板,需要理解4个要点:1、模板参数包;2、函数参数包;3、展开参数包;4、递归。
1、模板参数包 和 函数参数包
元运算符:用省略号表示。
- 能够声明表示模板参数包的标识符,模板参数包基本上是一个列表(类型列表);
- 能够声明表示函数参数包的标识符,函数参数包基本上是一个列表(值列表)。
template<typename T> void show_list0(T t); // 通用模板函数
template<typename... Args> // Args 是一个模板参数包
void show_list1(Args... args) // args 是一个函数参数包
{...}
Args 和 T 的差别在于:T 只与一种类型匹配,而 Args 可以与任意数量(包括零个)的类型匹配。
// 调用 show_list1 时:
show_list1(2, 4, 6, "who we are", string("appreciate"));
上述????函数调用时,模板参数包 Args 包含类型 int、int、int、const char*、std::string,而函数参数包 args 包含值 2、4、6、“who we are”, string(“appreciate”)。
2、展开参数包
考虑如何访问函数参数包中的数据,显然无法使用 Args[x] 来访问第x+1个类型的数据。相反,可将省略号放在函数参数包名的右边,将参数包展开。
// 存在无穷递归缺陷的代码
template<typename... Args> void show_list1(Args... args){
show_list1(args...); // 这导致函数每次都使用相同的参数不断调用自己,无限递归后报异常。
}
3、在可变参数模板函数中使用 递归
核心理念:将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。
template<typename T, typename... Args> void show_list3(T value, Args... args)
#include<iostream>
#include<string>
void show_list3(){}
template<typename T, typename... Args> void show_list3(T value, Args... args){
std::cout<< value<<", ";
show_list3(args...);
}
int main(){
int n =14;
double x = 2.71828;
std::string mr = "asdfgh";
show_list3(n, x);
show_list3(x*x, '!', 7, mr); /* 第一个实参导致 T 为double,value 为x*x;其它三种类型(char、int、string)将放入Args包中,对应的三个值('!', 7, mr)将放入args包中。
然后,cout 输出 x*x 的值和逗号,开始递归:实际上第一次递归调用的是:show_list3('!', 7, mr);
因为,每次调用,列表将减少一项,直至为空时,调用:void show_list3(){},导致处理结束。*/
return 0;
}
上述代码可以改进:
- 改进之一:列表最后一项输出时,不需要在末尾再加上逗号了。方法:对列表只有一个元素的情况,添加一个处理的模板。
template<typename T> void show_list3(T value){ std::cout<<value<<std::endl; }
- 改进之二:函数使用的是值传递,效率低,改变可变参数模板,指定展开模式。使用常量引用。
template<typename T, typename... Args> void show_list3(const Args&... args);