欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++ Primer Plus 学习笔记——模板

程序员文章站 2022-03-09 09:28:12
...

函数模板

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;
}

C++ Primer Plus 学习笔记——模板

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);
相关标签: C++学习笔记 c++