C++中模板与泛型编程

时间:2023-03-09 19:52:45
C++中模板与泛型编程

目录

    • 定义一个通用模板
    • 模板特化和偏特化
    • 模板实例化与匹配
    • 可变参数模板

  泛型编程是指独立与任何类型的方式编写代码。泛型编程和面向对象编程,都依赖与某种形式的多态。面向对象编程的多态性在运行时应用于存在继承关系的类,一段代码可以可以忽略基类和派生类之间的差异。在泛型编程中,编写的代码可以用作多种类型的对象。面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。


1 模板定义

1.1 函数模板

  • 模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。模板形参表不能为空。
  • 模板函数的类型形参跟在关键字 class 或 typename 之后定义.在函数模板形参表中,关键字 typename 和 class 具有相同含义,可以互换使用,两个关键字都可以在同一模板形参表中使用
  • 函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前
  • 函数模板调用方式。在发生函数模板的调用时,不显示给出模板参数而经过参数推演,称之为函数模板的隐式模板实参调用(隐式调用)在发生函数模板的调用时,显示给出模板参数而不需要经过参数推演,称之为函数模板的显示模板实参调用(显示调用)。显示模板实参调用在参数推演不成功的情况下是有必要的。
  • 函数模板与函数重载。函数模板实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,凡是函数体相同的函数都可以用这个模板来代替,不必定以多个函数。重载函数的参数个数、参数类型或参数顺序3者中必须至少有一种不同,函数返回值类型可以相同也可以不同,函数体可以相同。
 template<typename T>
inline bool isEqual(const T& t1, const T& t2) {
return t1 == t2;
}

1.2 类模板

  • 类模板也是模板,因此必须以关键字 template 开头,后接模板形参表
  • 除了模板形参表外,类模板的定义看起来与任意其他类问相似。类模板可以定义数据成员、函数成员和类型成员,也可以使用访问标号控制对成员的访问,还可以定义构造函数和析构函数等等。
  • 与调用函数模板形成对比,使用类模板时,必须为模板形参显式指定实参,类模板的形参不存在实参推演的。
 const size_t MAXSIZE = ;
template<class T>
class Stack{
private:
T elements[MAXSIZE];
public:
//others
};

1.3 模板参数

  • 类型模板形参:类型形参由关见字class或typename后接说明符构成,如template<class T> void getMaxVal(const T& a,const T& b){};其中T就是一个类型形参,类型形参的名字由用户自已确定。
  • 非类型模板形参:模板的非类型形参也就是内置类型形参,如template<class T, int X> greaterThanX(const T& a);其中int X就是非类型的模板形参。非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。非类型的模板参数是有限制的,一般是一个整型,它们可以是常整数(包括枚举类型)或者指向外部链接对象的指针。浮点数和类对象是不允许作为非类型模板参数的。
  • 模板的默认参数。可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认参数。类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值。类模板的类型形参默认值形式为:template<class T1, class T2=int> class A{};为第二个模板类型形参T2提供int型的默认值,在类模板的外部定义类中的成员时template 后的形参表应省略默认的形参类型。比如template<class  T1, class T2=int> class A{public: void h();}; 定义方法为template<class T1,class T2> void A<T1,T2>::h(){}
 template<typename T,int X = >
inline bool isEqualToX(const T& a) {
return a == X;
} template<class T,int MAXSIZE=>
class Stack {
private:
T elements[MAXSIZE];
public:
//others
};

2.模板特化与偏特化

  有时为了需要,针对特定的类型,需要对模板进行特化,也就是特殊处理。 例如,stack类模板针对bool类型,因为实际上bool类型只需要一个二进制位,就可以对其进行存储,使用一个字或者一个字节都是浪费存储空间的.。特化必须在同一命名空间下进行,可以特化类模板也可以特化函数模板,但类模板可以偏特化和全特化,而函数模板只能全特化。模板的偏特化是指需要根据模板的某些但不是全部的参数进行特化。严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。模板实例化时会优先匹配”模板参数”最相符的那个特化版本。template < >告诉编译器这是一个特化的模板。

 template<class T,int MAXSIZE=>
class Stack {
private:
T elements[MAXSIZE];
public:
//others
}; //template specializations aim at bool
template<>
class Stack<bool>{ };
 1 template<typename T>
inline bool isEqual(const T t1, const T t2) {
return t1 == t2;
} //针对int型的指针做特化
template<>
inline bool isEqual(const int* p1,const int* p2){
return *p1 == *p2;
}

类模板的偏特化,例如c++标准库中的类vector的定义,这个偏特化的例子中,一个参数被绑定到bool类型,而另一个参数仍未绑定需要由用户指定。

 template <class T, class Allocator>
class vector { // … // };
template <class Allocator>
class vector<bool, Allocator> { //…//};

函数模板的偏特化,严格的来说,函数模板并不支持偏特化,但由于可以对函数进行重载,所以可以达到类似于类模板偏特化的效果。根据重载规则,对(a)进行重载。如果将(a)称为基模板,那么(b)称为对基模板(a)的重载,而非对(a)的偏特化。C++的标准委员会仍在对下一个版本中是否允许函数模板的偏特化进行讨论。
template <class T> void f(T); (a)
template < class T> void f(T*); (b)


3 模板实例化与匹配规则

3.1 隐式实例化。在使用模板函数和模板类时,不存在指定类型的模板函数和模板类的实体时,由编译器根据指定类型参数隐式生成模板函数或者模板类的实体称之为模板的隐式实例化。函数模板隐式实例化指的是在发生函数调用的时候,如果没有发现相匹配的函数存在,编译器就会寻找同名函数模板,如果可以成功进行参数类型推演,就对函数模板进行实例化。类模板隐式实例化指的是在使用模板类时才将模板实例化。

3.2 显示实例化。显示实例化也称为外部实例化。在不发生函数调用的时候将函数模板实例化,或者在不适用类模板的时候将类模板实例化称之为模板显示实例化。对于函数模板而言,不管是否发生函数调用,都可以通过显示实例化声明将函数模板实例化,定义函数模板为:template函数返回类型 函数模板名<实际类型列表>(函数参数列表),显示实例化为template void func<int>(const int&);类模板的显示实例化,对于类模板而言,不管是否生成一个模板类的对象,都可以直接通过显示实例化声明将类模板实例化,定义类模板格式为:template class 类模板名<实际类型列表>,显示实例化为template class theclass<int>;

3.3 匹配规则

(1) 类模板的匹配规则。最优化的优于次特化的,即模板参数最精确匹配的具有最高的优先权,每个类型都可以用作普通型(a)的参数,但只有指针类型才能用作(b)的参数,而只有void*才能作为(c)的参数。

template <class T> class vector{//…//}; // (a) 普通型
template <class T> class vector<T*>{//…//}; // (b) 对指针类型特化
template <> class vector <void*>{//…//}; // (c) 对void*进行特化

(2) 函数模板的匹配规则。非模板函数具有最高的优先权。如果不存在匹配的非模板函数的话,那么最匹配的和最特化的函数具有高优先权

template <class T> void f(T); // (d)
template <class T> void f(int, T, double); // (e)
template <class T> void f(T*); // (f)
template <> void f<int> (int) ; // (g)
void f(double); // (h)
bool b;
int i;
double d;
f(b); // 以 T = bool 调用 (d)
f(i,42,d) // 以 T = int 调用(e)
f(&i) ; // 以 T = int* 调用(f)
f(d); // 调用(g)


4.可变参数模板

参考:http://www.cnblogs.com/qicosmos/p/4325949.html

可变参数模板是C++11新增的特性之一,它对参数高度泛化,他能表示0到任意个数、任意类型的参数。可变模板参数之前会带有省略号,把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点。可变模版参数和普通的模版参数语义是一致的,所以可以应用于函数和类,即可变模版参数函数和可变模版参数类,然而,模版函数不支持偏特化,所以可变模版参数函数和可变模版参数类展开可变模版参数的方法还不尽相同。

4.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(,,,);
return ;
}
 template <class T>
void printarg(T t)
{
cout << t << endl;
} template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), )...};
} expand(,,,);

4.2 可变模板参数类与参数展开

  可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂。可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,它的定义如下,这个可变参数模板类可以携带任意类型任意个数的模板参数。

 template< class... Types >
class tuple;
std::tuple<int> tp1 = std::make_tuple();
std::tuple<int, double> tp2 = std::make_tuple(, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(, 2.5, “”);
std::tuple<> tp;//可变参数模板的模板参数个数可以为0个,所以下面的定义也是也是合法的:

参考

  1. 模板特化和偏特化
  2. 泛化之美--C++11可变模版参数的妙用