C++技能系列 ( 3 ) - 详解C++泛型模版和特化模版的使用

2023-10-27

系列文章目录

C++技能系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程

期待你的关注哦!!!有更多博文系列等着看哦,会经常更新!!!
因为你的关注激励着我的创作!!!
在这里插入图片描述
快乐在于态度,成功在于细节,命运在于习惯。
Happiness lies in the attitude, success lies in details, fate is a habit.

导图大纲

在这里插入图片描述

详解模版的使用

一、函数模版

1.1、函数模版 - 定义与调用

范例如下:

//模版的定义
template<typename T>
T func1(T a, T b)
{
	T sum = a + b;
	return sum;
}
//模版的调用
int value1 = func1(2, 4);
float value2 = func1(1.2f + 5.6f);

如上代码int value = func1(2, 4);实参类型是int,所以编译器能推断出来模版的形参T是一个int类型。此时,编译器会实例化一个特定版本的函数

int func1(int a, int b)
{
	int sum = a + b;
	return sum;
}

同理float value2 = func1(1.2f + 5.6f);也是一样的。

但是,⚠️ float value3 = func(3, 4.5f); 会导致编译出错,不知道模版参数类型应该推断为int类型还是float类型。

1.2、函数模版 - 非类型模版参数

template<typename T> 不仅可以代表一个类型参数,还可以代表非类型参数(表示一个值)。

模版被实例化之后,这种非类型参数的值或者由用户提供,或者由编译器推断,都可以。但是这些值必须都得是常量表达式

//定义函数模版
template<typename T, int a, int b>
int func3(T c)
{
	int sum = int(c) + a + b;
	return sum;
}
//函数模版也可以写成内联的
inline int func4(T c)
{
	int sum = int(c) + a + b;
	return sum;
}

int result = func3<1, 3, 24>();
std::cout << result << std::endl; //28

错误的调用写法,如下:

int a = 12;
int result = func3<1, a, 14>();//错误

⚠️ 非类型模版参数必须是常量表达式,值必须是在编译的时候就能确定,因为实例化模版是在编译的时候做的事。

小结:

函数模版的定义并不会导致编译器生成相关代码,只有调用这个函数模版时,编译器才会实例化一个特定版本的函数并生成函数相关代码。
编译器生成代码的时候,需要能够找到函数模版的函数体部分,所以函数模版的定义通常都在.h头文件中。

二、类模版

2.1、类模版 - 定义

类模版(也称模版类)定义的一般形式如下:

template<typename 形参名 1, typename 形参名 2, ..., typename 形参名 n>
class 类名
{
	//... ...
};

创建一个自己的一个myvector.h的文件,范例如下:

//自己的容器类模版
template<typename T>
class myvector
{
public:
	//迭代器
	typedef T* myiterator;   
public:
	//构造函数
	myvector();     
	//赋值运算符重载,在类模板内部使用模版名myvector
	//并不需要提供模版参数。当然提供也行,可以写成myvector<T>
	myvector& operator = (const myvector&);
public:
	//迭代器接口
	myiterator mybegin(); //迭代器起始位置
	myiterator myend(); //迭代器结束位置
}

使用如下:

myvector<int> vec1;
myvector<double> vec2;
myvector<string> vec3;

⚠️ myvector是类模板名,不是一个类型名(或者说是一个残缺的类型名),类模版是用来实例化类型的,所以myvector<int> 才是真正的类型名。

2.2、类模版 - 成员函数

类模版成员函数可以写在myvector.h的vector类模版定义中,这种写在类模板定义中的成员函数就被隐式声明为内联函数。

如果将实现写在类模板定义的外面,怎么写?

//构造函数
template<typename T>
void myvector<T>::myvector(){}
//成员函数
template<typename T>
void myvector<T>::myfunc(){}
//赋值运算符重载定义
myvector<T>& operator = (const myvector<T>&);

//类模版定义之外实现这个赋值运算符重载
template<typename T>
myvector<T>& myvector<T>::operator = (const myvector<T>&);

⚠️ 一个实例化的模版,它的成员函数只有在使用的时候才会被实例化(编写的代码中出现了调用该成员函数的代码)。

2.2.1 普通类的成员函数模版

不管一个普通类,还是一个类模版,它的成员函数本身可以是一个函数模板,这种成员函数称为“成员函数模版”,但是这种成员函数不可以是虚函数,如果写一个虚函数模版,编译器会报错。

class A {
public:
	template<typename T>
	void myft(T tmpt){
		cout << tmpt << endl;
	}
};

在main()函数调用:

A a;
a.myft(3);  //3

2.2.2 类模板的的成员函数模版

类模版,也是可以为它定义成员函数模版的,这种情况就是类模版和其他成员函数模版都有各自独立的模版参数。

看如下范例,注意这里的构造函数也引入了自己的函数模版参数,该模版参数和整个类的模版参数没有任何关系:

template<typename C>
class A
{
public:
	template<typename T2>
	A(T2, v1, T2 v2)//构造函数也引入自己的模版参数T2,和整个类的模版参数C没有任何关系
	{
		.....
	}
	template<typename T>
	void myft(T tmpt)
	{
		cout << tmpt << endl;
	}
	C m_ic;
};

在main主函数中,增加如下代码:

A<float> a1(1, 2); //类模版的模版参数必须用“< >”指定,函数模版的模版参数可以推断
A<float> a2(1.1, 2.2);
a1.myft(3);  //3

从上面的范例可以看到,类模板本身有自己模版参数C,而成员函数模版A、myft也有自己的模版参数T2、T,两者之间互不干扰。

如果要把成员函数模版的实例代码写到类模版定义之外去,怎么写?
范例如下:

首先,在类模板内部写下构造函数模版的声明:

template<typename T2>
A(T2 v1, T2 v2)

然后在类模版定义的下面,书写构造函数模版的实现:

template<typename C> //先跟类模版的模版参数列表,要在排上面(如果排在下面会报错)
template<typename T2> //再跟构造函数模版自己的模版参数列表
A<C>::A(T2 v1, T2 v2)
{
	cout << v1 << v2 << endl;
}

主函数main的代码不变

A<float> a1(1, 2); //实例化了一个A<float>类,并用int型来实例化构造函数
A<float> a2(1.1f, 2.2f);//A<float>已经被上面代码实例化过了,这里用float来实例化构造函数

⚠️(1)类模版中的成员函数,只有源程序代码中出现调用这些成员函数的代码时,这些成员函数才会出现在一个实例化了的类模板中。

⚠️(2)类模版中的成员函数模版,只有源程序代码中出现调用这些成员函数模版的代码时,这些成员函数模版的具体实例才会出现在一个实例化了的类模板中。

2.3、类模版 - 非类型模版参数的使用

模版参数并不局限于类型,普通的值也能作为模版参数,也就是非类型模版参数。

//非类型模版参数有size存在,而且还给了一个默认值
tmplate<typename T, int size = 10>
class myarray{
private:
	T arr[size];	
	void myfunc();
};

tmplate<typename T, int size>
void myarray<T, size>::myfunc()
{
	std::cout << size << std::endl;
}

调用如下:

myarray<int> tmparr;
tmparr.myfunc(); //10

myarray<int, 30> tmparr2;
tmparr2.myfunc(); //30

⚠️ (1)浮点型一般不能作为非类型模版参数

tmplate<typename T, double size>
class myarray{...}

⚠️ (2)类类型也不能作为非类型模版参数

class a{
	... ...
};
tmplate<typename T, a size>
class myarray{...}

2.4、类模版 - 模版的显示实例化与声明

模版只有在使用时才会被实例化;现在我们分析下如下范例。

ca.h头文件内容如下:

#ifndef __CAH__
#define __CAH__

template<typename C>
class A
{
public:
	template<typename T2>
	A(T2 v1, T2 v2);

	template<typename T2>
	void myft(T tmpt)
	{
		std::cout << tmpt << std::endl;
	}
	C m_ic;
};

template<typename C>
template<typename T2>
A<C>::A(T2 v1, T2 v2)
{
	std::cout << v1 << v2 << std::endl;
}
#endif

test.cpp源文件内容如下(注意其中有一个新函数mfunc):

#include <iostream>
#include <vector>
#include "ca.h"

using namespace std;
void mfunc()
{
	A<float> a(1, 2);
}

myproject.cpp源文件内容如下:

#include <iostream>
#include <vector>
#include "ca.h"
using namespace std;

int main()
{
	A<float> a(1, 2);
	A<float> a2(1.1, 2.2);
	a.myft(3); //3
	return 0;
}

这些.cpp对于编译器而言,都是独立编译的(每个.cpp编译后可能生成一个.obj文件,多个.cpp编译后自然生成多个.obj文件。)

所以,当这两个.cpp代码中的“A<float> a(1, 2);”这行代码在编译的时候,都会实例化出一个模版A的实例(既A<float>类 ),可想而知,多个.cpp都会实例化出一个相同的类模板。项目很大,.cpp源文件很多,那么这个额外的开销比较大,增加额外的编译时间是没有必要的。

可以通过“显示实例化”来避免这种生成多个相同类的模版实例的开销。

可以在test.cpp文件头写入如下代码:

模版实例化定义

template A<float>; //这叫“模版实例化定义”,只有一个.cpp文件里这样写,编译器为其生成代码

只需要在其他.cpp的头上声明这个实例化出来的类就行。

模版实例化声明

extern template A<float>; //其他所有.cpp文件都这样写

函数模版也是一样的,代码如下:

在test.cpp中上面的位置这样写:

template void func(int& v1, int& v2); //函数模版实例化定义,编译器会为其生成实例化代码

在myproject.cpp中的上面的位置这样写:

extern template void func(int& v1, int& v2); //函数模版实例化声明

三、模版 - 巧妙使用参数

3.1、函数指针可作为其他函数的参数

定义一个函数:

int mf(int tmp1, int tmp2){
	//... ...
	return 1;
}

定义一个函数指针类型:

//可以在一个头文件中定义一个函数指针类型和函数本本身的参数,返回值类型都一致,这里定义cpp文件开头就可以
typedef int(*FunType)(int, int);

接下来函数指针就可以作为某个函数参数的参数进行传递了,现在定义一个函数:

void testFunc(int i, int j, FunType funcpiont)//最后一个参数为函数指针类型
{
	//可以通过函数指针点用函数
	int result = funcpoint(i, j); //这个就是通过函数指针调用的函数
	cout << result << endl;
}

在main主函数中:

//调用testfunc,其中第三个参数为另一个函数的函数名,函数名被作为函数首地址可以传递到函数testfunc的第三个参数里,
//而testfunc的第三个参数正好是函数指针(函数指针代表函数的首地址)
testfunc(2, 4, mf);

继续观察,在testfunc函数里,因为拿到函数mf的函数指针,所以通过这个指针调用函数mf。接下来说一下函数模版范例,其中的模版参数类型就是一个函数指针类型。

3.2、类的可调用对象作为函数的参数

我们可以把上面的testfunc函数改写成函数模版,如下:

template<typename T, typename F>
void testfunc(const T& i, const T& j, F funcpoint)
{
	cout << funcpoint(i, j) << endl;
}

在main主函数调用testfunc(3, 4, mf);打印结果为1。

系统通过第一个参数3和第二个参数4,推断出testfunc的模版参数T是int类型,推断出模版参数F是函数指针类型,所以funcpoint就是函数指针,从而使用funcpoint来进行函数调用。

可调用对象作为函数模版参数,接下来我们先说一下可调用对象概念。

如果一个类,重载了“()”运算符,那么如果生成了该类的一个对象,就可以用“对象名(参数... ...)的方式来使用该对象,看起来就像函数调用一样,那么用这个类生成的对象就是一种可调用的对象。

class tc
{
publictc(){
		cout << "构造函数执行" << endl;
	}
	//重载圆括号
	int operator(int v1, int v2) const {
		return v1 + v2;
	}
};

在主函数中可加入如下代码:

tc tcobj;
testfunc(3, 4 tcobj); //这里调用拷贝构造函数

函数testfunc的第三个参数传递进去了一个tcobj的对象,系统推断模版参数F的类型应该为tc(类类型),因此testfunc函数模版这里会调用tc类的拷贝构造函数生成一个叫做funcpoint的tc类型的对象。

然后,在testfunc这个函数模版中,代码“cout << funcpoint(i, j) << endl;”实际执行的就是可调用对象(把类当做函数一样调用),也就是tc类中的重载的“()”运算符。所以打印结果为7。

在main()主函数中,现在换一种写法:

testfunc(3, 4, tc());

上面的代码行调用了tc类的构造函数,生成一个tc类的对象(临时对象),直接传递到函数模版testfunc的funcpoint形参里面去了。可以看到,并还没有执行tc类的拷贝构造函数,只执行了一次tc类的构造函数。这说明系统推断F类型。这说明系统F类型应该为tc(类类型),然后直接把代码tc()生成的临时对象构造到funcpoint对象(形参)中去了,这样就节省了一次拷贝构造函数的调用,自然也节省了一次析构函数的调用。这里不在讲述临时对象概念,因为不是重点。

同一函数模版,传入的参数不同,推断出不同的类型,这里:

  • 推断出的是函数指针;
  • 推断出的是可调用的对象。

⚠️ tc类必须是一个可调用的对象,也就是tc类本身必须重载“()”运算符,并且这个运算符里面的参数和返回值类型必须要与函数模版里面进行函数或可调用对象调用时所需要的参数类型以及返回值类型匹配。

3.3、默认模版参数

3.3.1、类模版 - 默认模版参数

类模版默认模版参数范例如下:

template<typename T = string, int size = 5>
class myarray{...};

⚠️调用的时候,如果完全用默认值,则可以直接使用一个空的尖括号(空的尖括号不能省)

myarray<> abc;

一般来讲,在程序中遇到这种类后面带"< >"的形式,都表示这是一个类模版并且使用的默认模版参数。

如果想提供一个模版参数,而另外一个模版参数使用默认值,可以这样写代码:

myarray<int> def;

3.3.1、函数模版 - 默认模版参数

基于上面讲的testfunc现在有如下需求,希望如下的调用方式:testfunc(3, 4); 就可以调用函数模版testfunc。

这时就需要给函数模版提供默认参数,让上面的调用者能够实现调用tc类里面的重载“()”的能力,看应该如何改造,如下:

可调用对象给默认参数,其实tc 可以写成tc():

template<typename T, typename F = tc>
void testfunc(const T& i, const T& j, F funcpoint = F())
{
	cout << funcpoint(i, j) << endl;
}

函数指针给默认参数:

template<typename T, typename F = mf>
void testfunc(const T& i, const T& j, F funcpoint = F())
{
	cout << funcpoint(i, j) << endl;
}

⚠️ (1)必须同时为模版参数和函数模版参数指定默认值,一个也不能少,否则语法不通过且语义也不完整。
⚠️ (2)可调用对象的前提保证就是必须重载“()”运算符。
⚠️ (3)一旦给函数提供了正常的参数,那么默认参数就不起作用了。
⚠️ (4)模版参数F是一个函数指针类型(FuncType),函数参数funcpoint = mf中的mf是函数名,代表函数的首地址。

四、模版 - 模版的全特化与偏特化(局部特化)

什么是特化呢?

例如,写一个类模板或者函数模版,传递进去一个类型模版参数。这个传递进去的类型可以自己制定,但是存在这样一种情况,给进去一个A类型,这个模版能够正常实例化,但是给进去一个B类型,这个模版就无法正常的实例化,如编译报错等。或者换句话说,B类型是一种比较独特的类型,我们要针对这种类型给这个模版做单独的设计和代码编写,原来的这种模版代码(通用模版代码或者叫泛化模版代码)不适合这种比较独特的类型。所以引入了模版特化的概念。

4.1、类模板 - 类模板的全特化

范例如下:

template<typename T, typename U>
struct TC
{
	TC(){
		cout << "TC 泛化版本构造函数" << endl;
	}
	void functest(){
		cout << "TC 泛化版本" << endl;
	}
}

template<> //全特化所有的类型模版参数都用具体类型代表,所以“< >”里就是空了
struct TC<int, int>
{
	TC(){
		cout << "TC<int, int> 特化版本构造函数" << endl;
	}
	//在这里可以对该特化版本做单独的处理
	void functest(){
		cout << "TC 特化版本" << endl;
	}
}

template<> 
struct TC<double, int> 
{
	TC(){
		cout << "TC<double, int> 特化版本构造函数" << endl;
	}
	//在这里可以对该特化版本做单独的处理
	void functest(){
		cout << "TC<double, int> 特化版本" << endl;
	}
}

//特化版本的成员函数
template<>
void TC<double, double>::functest()
{
	cout << "TC<double, double>的functest()特化版本" << endl;
}

如果使用TC类模版并指定了“int, int”或者“double, int”类型,编译器就会执行这些特化版本的代码(特化版本代码具有优先被选择权)。

在main主函数中,写入如下代码:

TC<char, int> tcchar; // TC泛化版本构造函数
tcchar.functest();  // TC 泛化版本

TC<int, int> tcint; // TC<int, int> 特化版本构造函数
tcint.functest(); // TC 特化版本         

TC<double, int> tcdouble;  // TC<double, int> 特化版本构造函数
tcdouble.functest();  // TC<double, int> 特化版本

TC<double, double> tdbldbl; // TC泛化版本构造函数
tdbldbl.functest(); // TC<double, double>的functest()特化版本

⚠️ 对于特化版本的成员函数,tdbldbl对象执行的构造函数是泛化版本,但是调用functest时调用的依旧是“double, double”的functest的特化版本。

4.2、类模板 - 类模板的偏特化

4.2.1、模板参数数量上的偏特化

部分参数指定了类型。

范例如下:

template<typename T, typename Utypename W>
struct TCP
{
	TCP(){
		cout << "TCP 泛化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCP 泛化版本" << endl;
	}
}

//模板参数数量上的偏特化
template<typename U>
struct TCP<int, U, double>
{
	TCP(){
		cout << "TCP<int, U, double> 偏特化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCP<int, U, double> 偏特化版本" << endl;
	}
}

在main主函数中,写入如下代码:

TCP<double, int, double> tcpdi;   //TCP 泛化版本构造函数
tcpdi.functest();         //TCP 泛化版本

TCP<int, int, double> tcpdi;   //TCP<int, U, double> 偏特化版本构造函数
tcpdi.functest(); //TCP<int, U, double> 偏特化版本

4.2.2、模板参数范围上的偏特化

就是任意类型的范围变小了。

范例如下:

template<typename T>
struct TCF
{
	TCF(){
		cout << "TCF 泛化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCF 泛化版本" << endl;
	}
}

//模板参数范围上的偏特化 (const T 特化版本)
template<typename T>
struct TCF<const T>  //const 特化版本
{
	TCF(){
		cout << "TCF<const T> 特化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCF<const T> 特化版本" << endl;
	}
}
//模板参数范围上的偏特化 (T * 指针特化版本)
template<typename T>
struct TCF<T*>  //T * 特化版本
{
	TCF(){
		cout << "TCF<T*>  特化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCF<T*>  特化版本" << endl;
	}
}

//模板参数范围上的偏特化 (T & 左值引用特化版本)
template<typename T>
struct TCF<T&>  //T & 特化版本
{
	TCF(){
		cout << "TCF<T&>   特化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCF<T&>   特化版本" << endl;
	}
}
//模板参数范围上的偏特化 (T && 右值引用特化版本)
template<typename T>
struct TCF<T&&>  //T & 特化版本
{
	TCF(){
		cout << "TCF<T&&>   特化版本构造函数" << endl;
	}
	void functest(){
		cout << "TCF<T&&>   特化版本" << endl;
	}
}

在main函数中,写入代码如下:

TCF<double> td;   //TCP 泛化版本构造函数
tcpdi.functest();         //TCP 泛化版本

TCF<double *> tcfd;   //TCF<T*>  特化版本构造函数
tcfd.functest();         //TCF<T*>  特化版本

TCF<const int> tcfi;   //TCF<const T> 特化版本构造函数
tcfi.functest();         //TCF<const T> 特化版本

TCF<int&> tcfyi;   // TCF<T&>   特化版本构造函数
tcfyi.functest();         //TCF<T&>   特化版本

TCF<int&&> tcfyii;   //TCF<T&&>   特化版本构造函数
tcfyii.functest();         //TCF<T&&>   特化版本

4.3、函数模板 - 函数模板的全特化

template<typename T, typename U>
void tfunc(T& tmprv, U& tmprv2)
{
	cout << "tfunc 泛化版本" << endl;
	cout << tmprv << endl;
	cout << tmprv2 << endl;
}

template<> //全特化“< >”里是空的
void tfunc(int& tmprv, double& tmprv2) //替换原来的T,U,这格式要与泛化版本一一对应,不然编译就会报错,例如第二个参数写成double tmprv2就会报错
{
	cout << "tfunc<int, double> 特化版本" << endl;
	cout << tmprv << endl;
	cout << tmprv2 << endl;
}

在mian主函数中,写入如下代码:

const char *p = "I love china";
int i = 12;
tfunc(p, i); //这里调用泛化版本

int k = 12;
double db = 15.8f;
tfunc(k, db);  //这里调用特化版本

全特化实际等价于实例化一个函数模版,并不等价于一个函数重载。
看看如下两行代码:

//全特化长这样,等价于实例化一个函数模板
void tfunc<int, double>(int& tmprv, double& tmprv2){...} 
//重载函数长这样
void tfunc(int& tmprv, double& tmprv2){...}  

此时,执行"tfunc(k, db);", 就不会调用模版的全特化版本,而是去调用重载函数。

所以,顺序是先选择普通函数,再选择特化版本,最后选在泛化版本。

4.4、函数模板 - 函数模板能偏特化吗?

答案:不能,函数模板能偏特化。

4.5、模版特化版本放置位置建议

模版的定义与实现一般都放在.h文件中,模版的特化版本一般都放在泛化版本的后面即可。

五、模版 - 可变参数模版

5.1、类模板 - 可变参数模版

5.1.1、可变参函数模版 - 定义

可变参数模版允许模版中的定义中含有0到多个(任意个)模版参数,这种模版在语法上也和传统的模版不太一样,多了个". . . "符号,这个符号代表省略号的意思。

范例如下:

template<typename... T>
void myfunc1(T... args)
{
	//sizeof...属于固定语法,用在可变参数模版内部,
	//用来表示收到的模版参数个数,只能针对这种...的可变参数
	cout << sizeof...(args) << endl;
	//本行与上行的效果是一样的
	cout << sizeof...(T) <<endl;
}

在main主函数中加入如下代码:

myfunc1(); // 0
myfunc1(10, 20); // 2 
myfunc1(10, 25, "abc", 68); // 4,注意参数类型不同

有几点说明一下:

(1)一般把上面的args称为一包或者一堆参数,而且每个参数的类型可以各不相同。所以理解T这个名字的时候,不能把它理解成一个类型,而且要理解成0到多个不同的类型。

(2)这包参数中可以容纳0到多个模版参数,而且这些模版参数的类型可以为任意的类型。

(3)T:称为可变参数类型,一包类型(里面包含的是0到多个不同的类型)。

(4)args:称为可变形参,代表一包形参。

看如下范例:

template<typename T, typename... U>
void myfunct2(const T& firstarg, const U& ...otherargs)
{
	//编译错误,说明sizeof...只能用在一包类型或者一包形参上
	//cout << sizeof...(firsttarg) << endl;
	cout << sizeof...(otherargs) << endl;
}

在mian主函数加入如下代码:

//语法错误,必须要有一个firstarg
//myfunct2(); 

//firstarg对应第一个参数,因为没有其他参数,
//所以 sizeof...(otherargs) = 0
myfunct2(10);

//firstarg对应第一个参数,剩余两个参数,
//所以 sizeof...(otherargs) = 2
myfunct2(10, "abc", 12.7); 

5.1.2、可变参函数模版 - 参数包的展开

一般都是递归函数的方式展开参数包,这种方式展开参数包,要求在代码编写中有一个参数包展开函数和一个同名的递归终止函数,通过这两个函数把参数包展开。

为了在参数包展开时引入更多的复杂的代码,一般会把可变参函数模版写成这样的范例的形式:void myfunct2(const T& firstarg, const U& ...otherargs)

这种形式的可变参数模版,具备如下特点

(1)带一个单独的参数;
(2)后面跟一个“一包参数”。

因为最适合参数包展开,建议书写可变参数模版时一个单独参数跟着一包参数。

范例如下(看代码备注):

//(1) 一个同名的递归终止函数(是一个函数,不是函数模版)。
// 一般带0个参数的同名函数,就是递归终止函数 
// 因为参数是被一个一个剥离,剥离后,参数个数就为0个,所以此时会调用这个版本的函数
void myfunct2()  //是一个普通函数,不是函数模版
{
	cout << “参数包展开时执行了递归终止函数myfunct2()" << endl;
}

//(2) 参数包展开函数
template<typename T, typename... U>
void myfunct2(const& firstarg, const U& ...otherargs)
{
	cout << "收到的参数值为:" << firstarg << endl;
	myfunct2(otherargs...); //递归调用,注意塞进来的是一包形参,这里...不能省略
}

在main主函数中

myfunct2(10, "abc", 12.7);

执行起来,看结果

收到的参数值为:10
收到的参数值为:abc
收到的参数值为:12.7
参数包展开时执行了递归终止函数myfunct2()

解释一下结果:

(1)第一次调用myfunct2,firstarg拿到了10,剩余两个参数otherargs拿到了,输出10;

(2)第二次调用myfunct2,otherargs里面的2个参数一个被拆分给了firstarg,剩余一个被otherargs拿到了,输出abc;

(3)以此类推,每次调用myfunct2,otherargs里的参数就会减少一个,最终当这一包参数为空的时候,firstarg和otherargs都为空,就会调用void myfunct2(),就能看到终止函数执行了。

5.1.3、可变参类模版 - 通过递归继承方式展开参数包

可变参数类模板参数包展开方式和可变函数模版不一样。

看一下可变参数类模版的范例,先写一个可变参数了类模版的偏特化:

//主模版定义(泛化版本的类模板)
template<typename... Args>
class myclasst
{
public:
	myclasst()
	{
		printf("myclass::myclasst()泛化版本执行了,this = %p\n", this);
	}
	
};

template<>
class myclasst<>
{
public:
	myclasst()
	{
		printf("myclasst<>::myclasst()特殊的特化版本执行了,this = %p\n", this);
	}
	
};

template<typename First, typename... Others>
class myclasst<First, Others...>: private myclass<Others...> //偏特化
{
public:
	myclasst() :m_i(0)
	{
		printf("myclasst::myclasst()偏特化版本执行了,this = %p, sizeof...(Others) = %d\n", this, sizeof...(Others));
	}
	
	myclasst(First part, Others... paro) :m_i(part), myclass<Others..>(paro...)
	{
		printf("myclass::myclass(part, ...paro)执行了, this = %p\n", this);
		cout << "m_i = " << m_i << endl;
	}

	First m_i;
};

在main函数运行

myclasst<int, float, double> myc(12, 13.5, 23);

结果如下:

myclasst<>::myclasst()特殊的特化版本执行了,this = 0122EEF54
myclass::myclass(part, ...paro)执行了, this = 0122EEF54
m_i = 23
myclass::myclass(part, ...paro)执行了, this = 0122EEF54
m_i = 13.5
myclass::myclass(part, ...paro)执行了, this = 0122EEF54
m_i = 12

5.1.4、可变参类模版 - 通过递归组合方式展开参数包

template<typename First, typename... Others>
class myclasst<First, Others...>
{
public:
	myclasst() :m_i(0)
	{
		printf("myclasst::myclasst()偏特化版本执行了,this = %p, sizeof...(Others) = %d\n", this, sizeof...(Others));
	}
	
	myclasst(First part, Others... paro) :m_i(part), m_o<paro..>(paro...)
	{
		printf("myclass::myclass(part, ...paro)执行了, this = %p\n", this);
		cout << "m_i = " << m_i << endl;
	}

	First m_i;
	myclasst<Others...>m_o;
};

在main函数运行

myclasst<int, float, double> myc(12, 13.5, 23);

结果如下:

myclasst<>::myclasst()特殊的特化版本执行了,this = 002AFD04
myclass::myclass(part, ...paro)执行了, this = 002AFCFC
m_i = 23
myclass::myclass(part, ...paro)执行了, this = 002AFCF4
m_i = 13.5
myclass::myclass(part, ...paro)执行了, this = 002AFCEC
m_i = 12

5.1.5、可变参类模版 - 通过递归tuple和调用方式展开参数包

//mycount用于统计,从0开始,mymaxcount表示参数的数量,可以用sizeof...取得
template<int mycount, int mymaxcount, typename... T>
class myclasst2
{
public:
	//下面的静态函数借助tuple(类型),借助get(函数)就能够把每个参数提取出来
	static void mysfunc(const typle<T...>& t) //静态函数,注意,参数是tuple
	{
		//可以把每个参数提取出来并输出
		cout << "value = " << get<mycount>(t) << endl;
		//计算每次+1,这里是递归调用,调用自己
		myclasst2<mycount + 1, mymaxcount, T...>::mysfunc(t);
	}
};
//必须有一个特化版本,必须结束递归调用
//偏特化版本,用于结束递归调用
template<int mymaxcount, typename... T>
class myclasst2<mymaxcount, mymaxcount, T...> //注意”< >“中的两个都是mymaxcount
{
public:
	static void mysfunc(const typle<T...>& t)
	{
		//这里其实不用干哈,因为基数为0、1、2使用泛化版本里的myfunc处理,到这里的时候是3,不用做处理
	}
};

主函数mian中的代码如下:

template<typename... T>
void myfunctuple(const tuple<T...>& t)
{
	myclasst2<0, sizeof..(T), T..>::mysfunc(t); //注意第一参数是0,表示计数从0开始
}


tuple<float, int, int> mytuple(12.5f, 100, 52);
myfunctuple(mytuple);

接下来,看下结果:

value = 12.5;
value = 100;
value = 52

5.2、类模板 - 模版模版参数

模版模版参数范例如下:


template<typename T, template<class> class Container>
class myclass
{
public:
	T m_i;
	Container<T> myc;
};

也可以改成 <typename T, template<typename> typename Container>

六、using定义模版别名

(1)typedef一般定义类型别名,using呢?如下:

typedef unsigned int uint_t;
typedef std::map<std::string, int> map_s_i;

//使用using
using uint = unsigned int

看看如下的类型模版:

template<typename wt>
struct map_s
{
	typedef std::map<std::string, wt> type; //定义了一个类型
}
//在main函数中使用
map_s<int>::type map1;
map1.instert({"first", 1});

使用using ,不用定义类模版了,如下两行就能解决问题:

template<typename T>
using str_map_t = std::map<std::string, T>

//在main函数中使用
str_map_t<int> map1;
map1.instert({"first", 1});

(2)typedef一般定义函数指针类型,using呢?如下:

//typedef
typedef int(* FunType)(int, int);
//using
using FunType = int(*)(int, int);

看看使用using如何定义类型相关模版?

template< typename T>
using myfunc_M = int( * )( T ,  T );

//在main函数中
myfunc_M<int> pointFunc; //函数指针

int RealFunc(int j, int j)
{
	return 3;
}
pointFunc = RealFunc;
cout << pointFunc(1, 6) << endl;

七、小结

也不知道总结什么,多学,多练吧!加油!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++技能系列 ( 3 ) - 详解C++泛型模版和特化模版的使用 的相关文章

随机推荐

  • 【Spark NLP】第 15 章:聊天机器人

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • sonarQube详细安装

    目录 安装前提 将zip包scp到服务器 解压sonar的zip包 修改配置 创建用户sonar 将sonar目录授权给sonar用户 执行命令 查看日志 遇到的问题 今天学习了一下sonar 想看看代码质量检查工具的使用 安装前提 需要j
  • python笔记9--socket初步使用

    python笔记9 socket初步使用 1 介绍 2 源码案例 2 1 tcp c s 案例 2 2 udp c s 案例 3 说明 最近需要写个c s小应用 因此看了下socket编程 把学习的笔记贴在此处以便于后续查阅 1 介绍 本文
  • Mysql服务的安装

    本文适用于新手 小白 而且专业术语不到位 本文内容可能无法解决教程外其他问题 望多包涵 多图警告 此教程适用于windows系统 教程流程 安装时 1 下载安装包 这个是下载链接 MySQLhttps www mysql com 打开界面后
  • ubuntu连接mysql命令_远程服务器 ubuntu 安装 mysql 及连接使用

    远程服务器 ubuntu 安装 mysql 及连接使用 MySQL是最流行的开源关系数据库管理系统 它速度快 容易使用 容易扩展 并且流行的LAMP和LEMP的一部分 这篇指南讲解了如何在 Ubuntu 20 04上安装和保护 MySQL
  • SQL 取数值小数后两位,但不四舍五入。

    例 1 67789 结果要显示为 1 67 select round 1 67789 2 1 1 67 语法 ROUND numeric expression length function 参数 numeric expression 精确
  • k8s滚动更新

    1 编写一个yaml文件 vi deployment nginx yaml apiVersion apps v1 kind Deployment metadata labels app nginx name nginx namespace
  • 22.MongoDB删除操作效率及相关问题验证

    最近遇到一个了一个MongoDB数据删除的问题 需要一次性删除上线即1 5年前 1年前的数据且之后每天清空一年过期的数据 在数据量比较大的情况下何种方式的删除效率最高是一个值得研究的问题 本文通过实际测试找出其中规律 本文采用腾讯云mong
  • PCL实现点云选取并计算选取点法向量及可视化

    1 背景及效果展示 因项目需求 基于PCL1 8 1 VS2015 实现点云特征点选取并计算选取的特点法向量 并对特征点选取过程可视化 法向量计算结果可视化 特此记录该小功能实现 随机选取几个特征点 计算选取特征点法线并可视化 2 实现步骤
  • 使用burp suite软件后开启代理后不能上网

    这篇一定要记录一下 不然忘记了太恶心了 转载网址 https blog csdn net weixin 45571987 article details 110411138
  • Shell万能工具箱脚本

    文章目录 说明 说明 使用步骤 万能工具箱 脚本结构 万能工具箱 执行效果 说明 说明 持续更新 整合业务中常用的脚本并分类触发 所有功能均基于运维企业实战Shell脚本合集 使用步骤 1 shell tools sh存放到 root sc
  • pidstat 命令详解

    pidstat 概述 pidstat是sysstat工具的一个命令 用于监控全部或指定进程的cpu 内存 线程 设备IO等系统资源的占用情况 pidstat首次运行时显示自系统启动开始的各项统计信息 之后运行pidstat将显示自上次运行该
  • python 绘制分组对比柱状图

    首先放效果图 coding utf 8 import numpy as np import tensorflow as tf from matplotlib path import Path from matplotlib patches
  • 算法notes

    算法notes1 一 位运算 本文重点讲解前移位 前三个 位运算规则 十进制 gt 二进制 符号位 正数为0 负数为1 1 无符号右移 符号位不变 低位溢出 高位用符号位 第一位都是0 无论正负 填充 没有无符号左移 2 左移 lt lt
  • MyEclipse中生成Hibernate实体类及映射文件的方法

    下午 想还有一个工程项目要做 是采用三大框架SSH完成的 以下是简单的Hibernate实体类及映射文件的方法 在MyEclipse工作区右上角选择进入MyEclipse Database Explorer透视图 在DB Browser视图
  • Axios三层封装

    Axios三层封装 在实际项目中axios都是要经过封装再使用的 企业级项目一般都是三层封装 1 工具函数层 对axios工具进行增强 如 设置公共的请求服务器 设置请求拦截器 设置响应拦截器 创建一个文件夹utils 用来放axios 创
  • 【c++】——STL容器之vector的使用和模拟实现

    目录 1 vector的概述 2 vector常用接口 2 1 构造函数 2 2 迭代器的使用 2 3 修改的接口 push back pop back insert erase find reverse 2 4 关于容量接口 resize
  • 2020-06-09

    应该或者说必须努力下去 这只是为了生存得有意义一点 现在也许可以出错 那就去试试看 至少去做过了一些事情 有一天发现自己真的应该去试试的时候 有可能又有其他原因会让你不再敢去了 或是自身的原因 或是家庭的原因 或是环境的原因 有些年纪是可以
  • 关于串口收发数据出现全零或者收发数据位不同或者数据位一样,数据不匹配的问题

    近日用串口终端通过ttl转ra232来收发嵌入式开发板的数据 打开串口终端的收发数据全为零 以为是自己开发板上数据线出现问题 经过测试 开发板完全正常 转接电路也正常 但是不管是接收还是发送数据依然出现是全零的现象 对此做如下测试 默认设置
  • C++技能系列 ( 3 ) - 详解C++泛型模版和特化模版的使用

    系列文章目录 C 技能系列 C 高性能优化编程系列 深入理解软件架构设计系列 高级C 并发线程编程 期待你的关注哦 有更多博文系列等着看哦 会经常更新 因为你的关注激励着我的创作 快乐在于态度 成功在于细节 命运在于习惯 Happiness