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、模板的使用方法

关键词 templatetypename 是必须的!(在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:如何创建一个允许指定数组大小的简单数组模板?

  1. 方法一:在类中使用动态数组和构造函数参数来提供元素数目;
  2. 方法二:使用模板参数来提供常规数组的大小。
// 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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值