1.1 简介
C++提供了两种模板机制,函数模板和类模板。
- 使用范围:模板的声明或定义**只能在全局或类范围进行**,不能在局部范围(如函数)内进行。
- 使用目的:使用模板是为了能够让程序员**编写与类型无关的代码。
函数模板和模板函数区别**:函数模板是一个模板,其中用到通用类型参数,不能直接执行;模板函数是一个具体的函数,它是一个具体执行的函数,由编译系统在遇到具体函数调用时生成,可执行。
1.2 函数模板
1.2.1 函数模板格式
template <类型形参表或称模板参数列表> //类型函数声明
返回类型 函数名(形参表)
{
函数体;
}
注意:
- template — 声明创建模板;
- 模板参数表:定义在类或函数定义中用到的类型或值。
- 类型形参表:可以包含基本数据类型,也可以包含类类型。类型形参需要加class或typename关键字(两者等价)
- T — 通用的数据类型,名称可以替换,通常为大写字母
类型形参表的参数必需是唯一的,不能有重名的。
template声明语句和函数模板声明之间不允许有其他语句。
举例:
template<class T>
T square(T number)
{
return number * number;
}
int y, x = 4;
y = square(x);
该模板前缀以关键字 template 开始,接下来是一组尖括号,里面包含一个或多个在模板中使用的通用数据类型。通用数据类型以关键字 dass 开头,后面跟着代表数据类型的形参名称。
1.2.2 函数模板实例化为模板函数
函数模板不能直接执行需要实例化为模板函数后才能执行。
编译系统发现有一个函数调用“函数名(实参表)”或“函数名<类型实参表>(实参表)”时,c++将根据“实参表”中的类型生成一个重载函数,即模板函数。
函数模板:
template <class T> //T 的名字也为其他
T abs(T x)
{
if (x < 0) return -x;
return x;
}
//调用函数模板生成模板函数
cout << abs(-1) << endl;//输出结果为1
生成的模板函数如下:
int abs(int x)
{
if(x<0) return -x;
return x;
}
1.2.3 编译器自动推导模板参数类型
函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。甚至于,根据实际场景中函数模板被调用的情形,编译器还可以自行推导出部分模板参数的类型。
template <typename R = int, typename U>
R func(U val)
{
return val;
}
int main()
{
func(97); // R=int, U=int
func<char>(97); // R=char, U=int
func<double, int>(97); // R=double, U=int
return 0;
}
1.2.4 函数模板注意
(1)自动类型推导,必须推导出一致的数据类型T,才可以使用;
template<class T>
void myswap(T &a ,T &b)
{
T temp=a;
a=b;
b=temp;
}
//1.自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{
int a=10;
int b=10;
char c ='c';
myswap(a,b); //正确,可以推导出一致T
myswap(a,c); //错误,推到不出一致的T类型
}
(2)模板必须确定出T的数据类型,才可以使用
template <class T>
void func()
{
cout<<"func调用"<<endl;
}
void main()
{
//func(); 错误,模板不能独立使用,必须确定出T的类型
func<int>(); //利用显示指定类型的方式,给T一个类型,才可以使用该模板
}
1.3 类模板
1.3.1 简介
类模板允许用户为类定义一种模式,使得类中的某些数据成员、成员函数的参数或成员函数的返回值能取任意类型。类模板的成员函数被认为是函数模板。
1.3.2 类模板格式
template <类型形参表> //类型参数声明
class 类名
{
类模板的代码
}
template<类型形参表> //定义在类模板之外的函数必需以关键字template开始
返回类型 类名 类型名表::成员函数n(形参表)
{
成员函数定义体
}
1.3.3 实例化类模板
实例化类模板
类模板不能直接使用,必需先实例化为相应的模板类。定义类模板之后,创建模板类的格式如下:
类模板名 <类型实参表> 对象表;
类型实参表与类模板中的类型形参表相匹配。
举例:
#include <iostream>
#include <string>
using namespace std;
template <class T1,class T2>
class Pair
{
public:
T1 key; //关键字
T2 value; //值
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator < (const Pair<T1,T2> & p) const;
};
template<class T1,class T2>
bool Pair<T1,T2>::operator < (const Pair<T1,T2> & p) const
//Pair的成员函数 operator <
{ //"小"的意思就是关键字小
return key < p.key;
}
int main()
{
Pair<string,int> student("Tom",19); //实例化出一个类 Pair<string,int>
cout << student.key << " " << student.value;
return 0;
}
1.3.4 类模板举例
#include <iostream>
#include <map>
#include "string"
//#include <memory>
using namespace std;
template <typename T>
class Array //类模板
{
int size;
T *p;
public:
Array();//默认构造函数
Array(int n);//重载构造函数
T &operator[](int) const;//[]重载函数
};
//默认构造函数
template <typename T>
Array<T>::Array()
{
size = 10;
p = new T[size];
}
//重载构造函数
template <typename T>
Array<T>::Array(int n)
{
size = n;
p = new T[size];
}
//下标运算符重载函数
template<typename T>
T &Array<T>::operator[](int i)const
{
if (i >= 0 && i < size)
{
return p[i];
}
}
class Student
{
int no;
char name[10];
public:
Student() {}
Student(int n,const char *s)//注意要有const 否则”Tom“无法传参
{
no = n;
strcpy_s(name, s);//使用strcpy 为字符串数组赋值
}
Student &operator=(Student &s) //赋值重载构造函数
{
no = s.no;
strcpy_s(name, s.name);
return *this;
}
void display()
{
cout << "学号:" << no << ",姓名:" << name << endl;
}
};
int main()
{
Array<int> a(5);
for (int i=0;i<5;i++)
{
a[i]=i+1;//调用运算符重载函数
cout << a[i] << " ";
}
cout << endl;
cout << "学生列表" << endl;
Array<Student> b(3);
//int c = 1;
Student x(1, "Tom"), y(2, "Marry"), z(3, "John");
b[0] = x; b[1] = y; b[2] = z;
for (int i=0;i<3;i++)
{
b[i].display();
}
}
输出结果:
1 2 3 4 5
学生列表
学号: 1 ,姓名: Tom
学号: 2 ,姓名: Marry
学号: 3,姓名: John
1.3.5 类模板作为函数参数
//类模板
template <class T> //T 的名字也为其他
class A
{
T x;
public:
A(T a) { x = a; }
T abs()
{
if (x < 0) return -x;
else return x;
}
};
//函数模板中模板类作为形参
template <class T>
void fun(A<T> x)
{
cout << x.abs() << endl;
}
int main()
{
//建立对象
A<int> s1(-5);
A<double> s2(-5.8);
fun(s1);
fun(s2);
}
输出结果:5 5.8
1.3.6 函数模板作为类模板的成员
类模板中的成员函数还可以是一个函数模板。成员函数模板只有在被调用时才会被实例化。例如下面的程序
#include <iostream>
using namespace std;
template <class T>
class A
{
public:
template <class T2>
void Func(T2 t) { cout << t; } //成员函数模板
};
int main()
{
A<int> a;
a.Func('K'); //成员函数模板Func被实例化
a.Func("hello");
return 0;
}
1.3.7 类模板中也可以使用非类型参数,即值参数
template<class T,int size>
class A
{
.....
}
实例化:
A<int,3> s;
1.3.8 模板与静态函数
类模板中定义静态函数,则该模板类的所有对象共享一个静态数据成员。
1.3.9 类模板的友元函数
一个类模板中可以设计友元函数,友元函数的形参可以是类模板或类模板的引用。如果在类模板中设计与参数类型无关的友元函数,那么在类外面实现时也不能省略template类型参数声明,否则将其看成是一个普通全局函数。
template <class T> //T 的名字也为其他
class A
{
public:
T x;
A() { }
A(T i):x(i){ }
friend void f1();//与参数类型无关的友元函数
friend void f2(A<T> &);//与参数类型有关的友元函数
};
template<class T>
void f1(){ cout << "f1" << endl; }
template<class T>
void f2(A<T> &a) { cout << "f2:x " << a.x << endl; }
int main()
{
A<double> a(1.2);
f1<int>();//f1是模板类A<int>的友元函数
f1<double>();//f1是模板类A<double>的友元函数
f2<double>(a);//f2是模板类A<double>的友元函数
}
输出结果:
f1
f1
f2:x 1.2
1.3 可变参数模板
1.3.1 可变参数模板的定义
在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。
- 可变参数模板,对参数进行了高度泛化,能表示任意个数,任意类型的参数
-
是一个**接受可变数目参数的模板函数或模板类**。可变数目的参数被称为**参数包**。
语法:可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename
或class
后面带上省略号...
:
template <class... T>
void f(T... args);
- 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数;
举例:
#include <iostream>
#include <map>
//#include <memory>
using namespace std;
template <typename T,typename...Args>//foo是可变参数函数模板 有一个名为T的类型参数和一个名为Args的模板参数包
void foo(const T &t,const Args& ...rest) //foo函数列表包含一个const&类型的参数 指向T的类型,名为rest的函数参数包
{
cout << sizeof...(rest) << endl;//输出函数参数的数目
};
int main()
{
int i = 0; double d = 3.14; string s = "hello";
foo(i,s,42,d);//包中有三个参数
foo(s, 42, d);//包中有两个参数
foo(d, s);//包中有一个参数
foo("hi");//空包
}
输出结果:
3
2
1
0
1.3.2 可变参数模板的展开
把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类,然而,模版函数不支持偏特化,所以可变模版参数函数和可变模版参数类展开可变模版参数的方法还不尽相同,下面我们来分别看看他们展开可变模版参数的方法。
展开可变模板参数函数的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。
(1)递归函数方式展开参数包
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的,
#include <iostream>
using namespace std;
//递归终止函数
void print()
{
cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
cout << "parameter " << head << endl;
print(rest...);
}
int main(void)
{
print(1,2,3,4);
return 0;
}
上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。
递归调用的顺序如下:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();
上面的递归终止函数还可以写成这样:
template <class T>
void print(T t)
{
cout << t << endl;
}
修改递归终止函数后,上例中的调用过程是这样的:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
可变模板参数求和:
template<typename T>
T sum(T t)
{
return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest)
{
return first + sum<T>(rest...);
}
sum(1,2,3,4); //10
递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归。
(2)逗号表达式展开参数包
template <class T>
void printarg(T t)
{
cout << t << endl;
}
template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), 0)...};
}
expand(1,2,3,4);
不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成
((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了.
1.3.3 可变参数模板类的展开
可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。
//前向声明
template<typename... Args> struct Sum;
//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum { value = Sum<First>::value + Sum<Rest...>::value };
};
//递归终止
template<typename Last>
struct Sum<Last>
{
enum { value = sizeof (Last) };
};
一个基本的可变参数模板应用类由三部分组成,第一部分是:
(1)前向声明,声明这个sum类是一个可变参数模板类
template<typename... Args> struct sum
(2)第二部分是类的定义
定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包。
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum { value = Sum<First>::value + Sum<Rest...>::value };
};
(3)第三部分是特化的递归终止类
template<typename Last> struct sum<last>
{
enum { value = sizeof (First) };
}
参考文献:
【1】【C++】C++11可变参数模板(函数模板、类模板):https://yngzmiao.blog.csdn.net/article/details/105247065
【2】泛化之美--C++11可变模版参数的妙用:泛化之美--C++11可变模版参数的妙用 - qicosmos(江南) - 博客园