《C++ Primer》学习笔记(第十六章)——模板和泛型编程

模板和泛型编程

泛型编程的基础就是模板,一个模板就是创建类或函数的公式或者框架。

一、函数模板
当我们定义一个函数,希望该函数可以对不同类型的实参进行相同的操作。一种方法就是函数重载,为每一种类型定义该类型的函数,但是这种方法比较麻烦,而且扩展性低。另外一种方法就是定义一个通用的函数模板,当我们使用不同的实参时,编译器就会根据实参类型实例化一个函数模板,如:

template<typename T>
T add(T a,T b)
{
	return a + b;
}

int main()
{
	int a = add(10, 20);
	double d = add(3.14, 3.14);
	string s = add(string("IG"), string("RNG"));
	cout << a << " " << d << " " << s << endl;
    return 0;
}

如上述代码所示:编译器会根据实参的类型推断出模板的实参,然后实例化特定版本的函数,所以上述代码编译器会实例化以下函数:

int add(int a,int b);
double add(doublie a,double b);
string add(string a,string b)

模板的定义有关键字template加上模板参数列表组成,参数列表中以有多个模板参数,但是每一个模板参数前都必须加上关键字typename或者class。typename与class没有任何区别,typename只是引入比较晚而已,模板定义如下:

//template<typename T, C>  错误;
template<tymename T,class C>
T func(T t, C c);

模板参数也可以是一个非类型参数表示一个值,当表示一个值是时,需要给定特定的类型名而不是关键字class或者typename。而非类型模板参数的模板实参必须是一个常量表达式。如书上的栗子:

template<unsigned N, unsigned M>
int compare(const char 

二、类模板
1、c++11标准库中的容器类如vector ,stack,set等就是类模板。与函数模板不同的是,编译器不能为类模板推断模板参数类型,因此我们在声明一个vector容器的实例时,需要在<>中显式指明vector中存储的是什么类型。定义类模板与定义函数模板类型,类模板以template关键字开始后跟一个模板参数列表,如:

template<typename T>
class MyClass {  //模板类
public:
	MyClass(T t) :num(t) {};
	T show() {
		return num;
	}
private:
	T num;
};

int main()
{
	MyClass<int> c1 = MyClass<int>(10);  //需要显式模板实参
	MyClass<double>c2 = MyClass<double>(3.14);
	MyClass<string>c3 = MyClass<string>("IG");
	cout << c1.show() << endl;
	cout << c2.show() << endl;
	cout << c3.show() << endl;
    return 0;
}

在上述代码中,成员函数show定义在类内部,与普通成员函数一样,该函数被隐式声明为inline。如果我们将show成员函数定义在类外部,就需要函数模板声明,如:

template<typename T>
class MyClass{
public:
T show():
}


template<typename T>
T MyClass<T>::show(){ // };  //类模板的成员函数在类外的声名形式,注意此时类必须包含模板实参

如上述代码所示:作用域限定符前面的类也必须加上模板实参。

2、实例化一个模板类时,其成员函数并没有被实例化,只有当成员函数在使用时才被实例化。

3、类与友元各自是否是模板是相互无关的,若干一个类模板包含一个非模板友元,那么该友元可以访问所以模板类实例,如果友元本身是模板,那么所以友元模板实例也是该类的友元。

4、与其他static数据成员一样,模板类的每个数据成员不行有且仅有一个定义,但是类模板的每一个实例都有一个独有的static对象,对于给定类型的类模板对象共享同一个static,比如:

MyClass<int> m1,m2,m3;//MyClass<int>类独有一个static成员,m1,m2,m3共享该static成员
MyClass<strng>s1,s2,s3;//MyClass<string>类独有另一个static成员,s1,s2,s3共享该static成员

三、模板实参推断和引用
1、当函数的模板参数类型为一个普通的引用时,如:

template<typename T>
void func1(T& p);  //模板参数类型为普通引用

这种情况下,我们只能传递一个左值,实参可以是const也可以不是。但是形参是const类型时,T被推断为const类型,如:

int i,
const int j;
void func1(i);  //模板参数类型T被推断为int
void func1(j);  //模板参数类型T被推断为const int

2、当函数的模板参数类型为一个常量引用时,如:

template<typename T>
void func2(const T& p);  //模板参数为常量引用

这种情况下,我们可以传入如何实参类型,左值也可以右值也可以(因为常量引用可以绑定到右值上啊),但是值得注意的是:当传递一个非常量类型如int时,T被推断为int型,当传递常量类型如const int时,此时T依旧被推断为int而不是const int型。如:

int i;
const int j;
func2(i);  //T被推断为int型
func2(j);  //T被仍然被推断为int型。

用大白话总结上述1和2就是:但模板参数中没有const时,你给我const那我T就要const,当模板参数中有const时,不管你给不给我const,我T都不会要const

3、当函数的模板参数类型为一个右值引用时,如:

template<typename T>
void func3(T&&);//模板参数类型为右值引用

这种情况下,理论上我们只能传递一个右值(如字面量值),但是c++允许通过模板参数先模板参数类型为右值引用的函数传递一个左值,此时的T被推断为一个左值引用,啥子意思?举个栗子:

int i; //i为左值
func3(10);//正确,字面量为右值,此时T被推断为int型
func3(i) ;//正确,此时T被推断为左值引用int&,相对于实例化函数:void func3(int& &&);

如上述代码所示:我们仿佛定义了一个引用的引用,但是第二章不是说了吗,引用不是对象,因此不能将一个引用绑定到另一个引用上啊。实际上这种特殊情况发生是因为存在引用折叠规则,即一个引用的引用会被折叠为一个普通的左值引用类型,因此有:

int& &&会被折叠为int& ,因此上述代码中的模板参数类型T被折叠为int&.

引用折叠只能应用于间接创建的引用的引用,比如这里的讲到的模板参数和类型别名。

总结第3点就是:但向一个模板参数类型为右值引用的函数传入一个左值时,会发生下面两件事情:
①、首先模板参数类型T被推断为一个普通左值引用,如int&,参数模板参数类型为:int& &&;
②、引用折叠规则将int& &&又折叠成普通左值引用int&。

实际上std::move()函数得以实现,得感谢引用折叠规则的存在,因为其实现内部就使用了引用折叠

四、可变参数模板
1、一个可变参数模板就是一个接受可变数目参数的模板函数或者模板类。可变数目的参数称为参数包。根据位置不同分为两种参数包:位于模板参数类别中的表示零个或多个模板参数称为模板参数包;位于函数参数列表中表示零个会在多个函数参数的称为函数参数包,所以如果在函数参数列表中,一个参数 的类型为模板参数包,那么这个参数一定是一个函数参数包。

2、模板参数包和函数参数包的声名如下:

template<typename T, typename...Args>  //Args为模板参数包
void func(T t,Args... args);  //args为函数参数包

如上代码所示:模板参数包的声明就是使用关键字typename…或者class…

3、编译器可以从函数实参中推断出模板参数包中类型和相应数量,如:

int i = 1;
	int j = 2;
	double d = 3.14;
	string s = "IG";
	func(i, j, d,s);
	//编译器会实例化函数为:void func(int,int,double,string);

如上述代码所示,其中T类型由第一个实参推断出来,编译器由剩下是实参数目和类型推断出模板参数包的数量和类型。在这个例子中,模板参数包中的参数个数为3个,类型分别为int ,double, string。

4、我们可以使用sizeof…计算参数包中的元素个数,如:

template<typename T,typename... Args>
void func(T p,Args... args)
{
	cout << sizeof...(Args) << endl;  //模板参数包中元素个数
	cout << sizeof...(args) << endl;  //函数参数包中元素个数
}

5、可变参数函数通常是递归的,就函数调用该函数本身,这种情况下我们可能需要对参数包进行扩展,所谓的包扩展就是把它分解为构成的元素,也就是把元素一个个取出来。举一个书上的栗子:

//伪代码
template<typename T,typename...Args>
void print(T t,Args... args)
{
//
return print(args...);  //包扩展
}

如上述代码所示:实际上每进行一次包扩展,函数参数包中元素就少一个,因为每递归一次print,args中的第一个实参就拿给T了,所以下一层调用print时,a函数函数参数包元素就少一个。举个栗子:


void func() {}; //必须声明一个不含参数的func函数,不然会报错

template<typename T,typename... Args>
void func(T p,Args... args)
{
      cout << sizeof...(args) << endl;
	  func(args...);
}

int main()
{
	int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6;
	func(a,b,c,d,e,f);、、输出为:5,4,3,2,1,0
    return 0;
}

从上述代码可以看出,每递归调用一次func函数,函数参数包中元素个数就少一个。另外我们必须定义一个不含参数的func函数,因为递归到最后时,形参个数为0,此时不能调用模板类型的func函数,因为模板类型的func函数至少有一个实参,所以的声明一个没有形参的func函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值