关闭

[置顶] C++泛型编程1——函数模板实例化,模板参数,重载及特化

标签: 泛型编程函数模板特化C++
1549人阅读 评论(0) 收藏 举报
分类:

  在C++中我们都知道引入了函数重载,那么在实际应用过程中:
  如果我们想要实现一个加法函数,我们可以写出很简单的代码:
  

int ADD(int a,int b)
{
  return a+b;
}

  上面的函数很简单,但是仔细想一下,这样就实现了加法操作嘛?当我们的实参类型为double,float时,又怎么处理加法呢?
  我们可以多定义几个函数
  

double ADD(double a,double b)
{
  return a+b;
}
float ADD(float a,float b)
{
  return a+b;
}

  上面的方式是可行的,但是在C++中的数据类型那么多,我们应该如何妥善处理这个问题呢?难道如此简单的加法函数就要写那么多代码?
  如何解决上面的问题呢?

定义公共基类

  这个方法是将所有的重载函数都定义在一个公共基类里面,当我们调用的时候,直接通过这个公共基类去调用即可。
  上面的方法仿佛解决了使用的问题,但是还是有很多缺点:
1、借助公共基类来编写通用代码,将失去类型检查的优点;
2、对于以后实现的许多类,都必须继承自某个特定的基类,代码维护更加困难。

使用宏替换

  那么下面的方法即使用宏替换
  

#define ADD(a,b) ((a)+(b))

  在C语言学习时我们就知道,宏定义虽然很方便,但是还是有很多坏处的。
- 宏是在预编译时展开的
- 宏只是简单的文本替换
- 宏不具有类型检测,语句是否正确等编译功能
- 宏在定义时容易出现宏参数二义性问题

泛型编程

  所以在C++中,引入了泛型编程的概念。
  泛型编程:编写与类型无关的逻辑代码,是代码复用的一种手段。模板是泛型编程的基础。

  其实面向对象编程OOP和把泛型编程都能处理在编写程序时不知道类型的情况。
  不同之处在于:
  OOP能处理类型在程序运行之前都未知的情况,而在泛型编程中,在编译时就能获知类型了。

模板

  模板是C++中泛型编程的基础,一个模板就是一个创建类或者函数的蓝图。

函数模板

  函数模板代表了一个函数家族,该函数与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
  函数模板是通用的函数描述,也就是说,他们使用通用类型来定义函数,其中的通用类型可用具体的类型替换。如int,double类型。
  通过将类型作为参数传递给模板,可使编译器生成该类型的函数,由于模板允许以通用类型的方式编写程序,因此也被称为通用编程。

函数模板的定义

  一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。
  模板函数的格式

template<typename Param1, typename Param2,...,class Paramn>
返回值类型  函数名(参数列表)
{
    ...
}

  模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分割的一个或多个模板参数的列表,用”<>”包围起来
  typename是用来定义模板参数关键字,也可以使用class。
  

template <class T>
T ADD (T a,T b)
{
  return a+b;
}

  上面的代码就定义了一个函数模板。
  在模板定义中,模板参数列表不能为空

  模板参数列表的作用很像函数参数列表。
  模板参数表示类或函数定义中用到的类型或值。当使用模板是,我们指定模板实参,将其绑定到模板参数上。
  在上面的代码中,我们用名字T表示一个类型,而T表示的实际类型则在编译时根据ADD的使用情况而定。
  模板函数也可以定义为inline函数

template<typename T>
inline T Add(const T _left, const T _right)
{
     return (_left + _right);
}

注意
  inline关键字必须放在模板形参表之后,返回值之前,不能放在template之前。

实例化函数模板

  在理解实例化之前,我们必须搞清楚一件事情。
  在代码中包含函数模板本身并不会生成函数定义,它只是用于生成函数定义的方案,编译器使用模板为特定类型生成函数定义时,得到的是模板实例。

  当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参,即当我们调用ADD时,编译器使用实参的类型来确定绑定到模板参数T的类型。
  编译器用模板产生指定的类或者函数的特定类型版本,产生模板特定类型的过程称为函数模板实例化。

  编译器用推断出的模板参数来为我们实例化一个特定版本的函数。
  当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建一个模板的新实例

  当我们实例化两个模板函数时:
  

template<typename T>
inline T Add(const T _left, const T _right)
{
     return (_left + _right);
}

void Test()
{
  int a,b;
  double c,d;
  int e = ADD(a,b);
  double f = ADD(c,d);
}

  注意

模板被编译了两次:
  1.实例化之前,检查模板代码本身,查看是否出现语法错误,如:遗漏分号
  2.在实例化期间,检查模板代码,查看是否所有的调用都有效,如:实例化类型不支持某些函数调用

  在上面的例子中,编译器会实例化出两个不同版本的ADD。
  对于第一个调用,编译器编写并编译一个ADD版本,其中T被替换为int。
  

int ADD(int a,int b)
{
  return a+b;
}

  对于第二个调用编译器编写并编译另一个ADD版本,其中T被替换为double。

隐式实例化

  模板并非函数定义,但使用的模板实例是函数定义的,这叫做隐式实例化。
  比如上面的例子就是隐式实例化的,因为编译器之所以知道需要进行定义,是由于程序调用ADD函数时提供了int参数。

显式实例化

  显式实例化意味着可以直接命令编译器创建特定的实例,如ADD()。
  其句法是:
  声明所需的种类——用<>符号指示类型,并在声明前加上关键字template。
  

template int ADD(int a,int b);

  编译器看到上面的声明后,将使用ADD模板生成一个int类型的实例。也就是说,该声明的意思是使用ADD()模板生成int类型函数的定义
  

模板参数

模板类型参数

  在上面的例子中,ADD函数有一个模板类型参数,一般来说,我们可以将类型参数看做类型说明符,就像内置类型或类类型说明符一样使用。
  特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。

非类型模板参数

  除了定义类型参数,还可以在模板中定义非类型参数。
  一个非类型参数表示一个值而非一个类型。

  我们通过一个特定的类型名而非关键字class/typename来指定非类型参数。
  当一个模板被实例化时,非类型参数被一个编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
  例如,我们可以编写一个比较函数,用于两个字符串的比较。
  

template <int N,int M>
int compare(char a[N], char b[M])
{
  return strcmp(a,b);
}

  当我们调用这个函数时,编译器会使用字面常量大小来代替N和M,从而实例化模板。
  例如我们这样调用compare函数:
  

int i = compare("hi","hello");

  编译器会实例化出以下版本:
  

int compare (char a[3],char a[6])
{
  ...
}

  一个非类型参数可以是一个整型或是一个指向对象或函数类型的指针或引用。
  
  绑定到非类型整型参数的实参必须是一个常量表达式。
  绑定到指针或引用的非类型参数的实参必须具有静态的生存期。
  
模板形参说明

  1、模板形参表使用<>括起来
  2、和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同
  3、模板形参表不能为空
  4、模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后
  5、模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型 使用方法完全相同,可用于指定函数形参类型、返回值、局部变量和强制类型转换
  6、模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。

模板函数的重载

  需要多个对不同类型使用同一种算法的函数时,可使用模板,不过,并非所有的类型都使用相同的算法。
  为满足上面的需求,可以向重载常规函数定义那样重载模板定义。
  和常规重载一样,被重载的模板的函数的特征标必须不同。
  即:参数不同或者返回值不同。
  

int Max(const int& left, const int & right)
{
    //1
    return left>right? left:right;
}

template<typename T>
T Max(const T& left, const T& right)
{       
    //2
    return left>right? left:right;
}

template<typename T>
T Max(const T& a, const T& b, const T& c)
{
    //3
    return Max(Max(a, b), c);
};

int main()
{
    Max(10, 20, 30);
    //3
    Max<>(10, 20);
    //2
    Max(10, 20);
    //1
    Max(10, 20.12);
    //1
    Max<int>(10.0, 20.0);
    //2
    Max(10.0, 20.0);
    //1
    return 0;
}

  在上面的例子中,main函数中每一个Max函数调用哪一个函数我都标记出来了,这里的原因有:
  
  1、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
  2、对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调动非模板 函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
  3、显式指定一个空的模板实参列表,该语法告诉编译器只有模板才能来匹配这个调用,而且所有的模板参数都应该根据实参演绎出来。
  4、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

  尤其注意上面的第四条,这就是为什么Max(10, 20.12);调用的是第一个原来的函数而不是模板函数的原因。

模板函数的特化

  有时候并不总是能够写出对所有可能被实例化的类型都最合适的模板,在某些情况下,通用模板定义对于某个类型可能是完全错误的,或者不能编译,或者做一些错误的事情。
  比如说下面的例子:
  这里写图片描述

可以下面这样来定义

这里写图片描述
模板函数特化形式如下:

1、关键字template后面接一对空的尖括号<>
2、再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
3、函数形参表
4、函数体

template<>
返回值 函数名<Type>(参数列表)
{
      // 函数体
}

特化的声明必须与特定的模板相匹配,否则
这里写图片描述
  如果少了模板形参表,那么只是定义了一个普通函数,该函数含有返回类型和可与模板实例化相匹配的形参表。
  
  注意:
  在模板特化版本的调用中,实参类型必须与特化版本函数的形参类型完全匹配,
  如果不匹配,编译器将为实参模板定义中实例化一个实例。
  这里写图片描述

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:78578次
    • 积分:1446
    • 等级:
    • 排名:千里之外
    • 原创:64篇
    • 转载:4篇
    • 译文:0篇
    • 评论:11条
    最新评论