cpp中的函数模板

之前我们讲了函数重载,对一个不同参数类型的函数作出了不同的行为,但是其实有一个更加高效的方式,那就是函数模板,也叫泛型编程。或者模板元编程。

下面是一个交换两个元素的例子

# include "iostream"

template<typename Type>
void swap(Type &a, Type &b) {
    Type c = a;
    a = b;
    b = c;
}

int main() {
    int i = 0;
    int j = 1;
    swap<int>(i, j);
    std::cout << i << std::endl;
    std::cout << j << std::endl;
}

我们只需要在调用时指定类型就可以

我们也可以指定两种类型,下面的程序没有什么实际意义,就是为了展示一下

# include "iostream"

template<typename Type1, typename Type2>
Type2 add(Type1 &a, Type2 &b) {
    Type2 c = a + b;
    return c;
}

int main() {
    int i = 0;
    int j = 1;
    int m = add<int,int>(i, j);
    std::cout << i << std::endl;
    std::cout << j << std::endl;
    std::cout << m << std::endl;
}

实例化和具体化

在C++中,我们没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。

比如上面的add函数,我们传入两个指针,最后返回两个相加的指针地址,这是没有意义的。我们需要将我们用到的数据单独处理,

具体化

模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显式具体化(Explicit Specialization)。

我们看下面的例子

#include 
#include 
using namespace std;
typedef struct{
    string name;
    float score;
} STU;

template<typename T> const T & Max(const T &a, const T &b){
    return a > b ? a : b;
}

template<> const STU & Max<STU>(const STU &a, const STU &b){
    return a.score > b.score ? a : b;
}

ostream & operator << (ostream & out, const STU &stu){
    out << stu.name << ' ' << stu.score;
    return out;
}

int main(int argc, char const *argv[]){
    int a = 10, b = 20;
    cout<<Max(a, b)<<endl;

    STU stu1 = {"Sam", 90}, stu2 = {"Amy", 100};
    cout<<Max(stu1, stu2);
    return 0;
}

请格外注意这一行

template<> const STU & Max<STU>(const STU &a, const STU &b)

我们使用了STU直接替换了T,也就是显示式的申明了参数类型。Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>另外,Max中的STU是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写方式如下所示:

template<> const STU& Max(const STU& a, const STU& b);

实例化

#define MAXNAME 128
struct job {
    char name[MAXNAME]:
    int salary;
};
 
template<class T>
void swap(T &a, T &b )
{
    T temp;
    temp = a;
    a = b;
    b = temp;
};

# 实例化
template void swap<int>(int &a, int & b);  
template<> void swap<job>(job &a, job &b) {
      int salary:
      salary = a.salary:
      a.salary = b.salary;
      b.salary = salary;
};

区别

为进一步了解模板,必须理解术语实例化和真体化。记住,在代码中包含函数模板本身并不会生成函数:!定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例.( instantiation)。例如,在第一个程序中,函数调用swap(i,j)导致编译器生成swap()的一个实例;:该实.例使用int类型。模板并非函数定义,但使用.int 的模板实例是函数定义::这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了 int参数。

模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。

模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。

另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。

具体化:即显式具体化,与实例化不同的是,它也是一个模板定义,但它是对特定类型的模板定义。

**实例化:**在程序中的函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。这即是函数模板的实例化。

有人会说,那具体化不就是实例化基础上多此一举吗?不是的,有时要针对特定数据类型做不同的处理,所以需要具体化。

在程序运行时匹配模板时,遵循的优先级是:具体化模板优先于常规模板,而非模板函数优先于具体化和常规模板。

实例化的缺陷

C++ 支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。

一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。

总起来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。

编译器选择使用哪个版本的函数

对于函数重载,函数模板和函数模板重载,C++需要有一个策略找出使用哪一个函数定义,尤其是有多个参数时,这个过程称之为重载解析,我们只需要大致了解一下这个过程

  • 创建候选函数列表,其中包含名称相同的函数和模板函数
  • 使用候选函数列表第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
  • 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错

模板函数的发展历史

在C++发展的早期,大多数人都没有想到模板函数和模板类会有这么强大而有用,它们甚至没有就这个主题发挥想象力。.但聪明而专注的程序员挑战模板技术的极限,阐述了各种可能性。根据熟悉模板的程序员提供的反馈,C++98标准做了相应的修改,并添加了标准模板库。.从此以后,模板程序员在不断探索各种可能性,并消除模板的局限性。C+11标准根据这些程序员的反馈做了相应的修改。下面介绍一些相关的问题及其解决方案。

类型

template <class T1, class T2>
void ft(T1 t1,T2 t2) {
	type xpy = t1 + t2;
}

xpy应该是什么类型呢,我们是不知道的,因为我们不知道用户会把T1和T2赋值为什么,所以在C++11标准中新增了关键字decltype。

int s = 11;
decltype(s) ss; 

ss的类型和s保持一致。

因此可以这样修复之间的模板函数

template <class T1, class T2>
void ft(T1 t1,T2 t2) {
	decltype(t1 + t2) xpy = t1 + t2;
}

C++11新增关键字decltype

decltype使用起来还是比较简单地,但是实现起来确实比较复杂,为了确定类型,编译器必须遍历一个核对表,假设现在有下面的申明

decltype(expression) var;

第一步,如果expression是一个没有用括号括起来的标识符,则var类型与该标识符的类型相同,包括const等限定符。

double x = 5.5;
double &xx = 5.5;
const double *xxx = x;

decltype(x) a; // double
decltype(xx) b; // double &
decltype(xxx) c; // const double *

第二步,如果expression是一个函数调用,则car的类型与函数的返回类型相同,

long fun(int);
decltype(fun(3)) d; // long

这里并不会调用函数,编译器只是看一下

第三步:如果expression是:个左值,则var为指向其类型的引用。这好像意味着前面的a应为引用类型,因为x是一个左值。但别忘了,这种情况已经在第一步处理过了。要进入第三步,expression不能是未用括号括起的标识符,那么,,expiession是什么时将进入第三步呢?种显而易见的情况是,expression是用括号括起的标识符:

double xx = 4.4;
decltype((xx)) e = xx; //e is double &

顺便说一下,括号并不会改变表达式的值和左值性,下面两条语句等价

xx = 3.3;
(xx) = 3.3;

第四步:如果前面的条件都不满足,则 var的类型与expression 的类型相同

int i = 3;
int &k = j;
int &n = j;
decltype(j+6) a; // int
decltype(100L) b; // long
decltype(k+n) c; // int

虽然k与n都是引用,但是k+n并不是引用,他是两个int的和,所以类型为int

C++11后置返回类型

有一个问题是decltype所无法解决的,看下面这个函数

template<class T1,class T2>
type gt(T1 t1,T2 t2){
	return x+y;
}

同样,无法预先知道将x和y.相加得到的类型。好像可以将返回类型设置为deceltype(x+y),但不幸的是,此时还未声明参数x和y,它们不在作用域内,(编译器看不到它们,也无法使用它们)。必须在声明参数后使用decltype,为此,C++新增了三种声明和定义函数的语法。主面使用内置类型来说明这种语法的王作原理。对于下面的原型:

double h(int x,float y);

使用新增的语法可以写成这样

auto h(int x,float y) -> double;

将返回类型移到参数申明后面,->double称为后置返回类型,其中auto是一个占位符,表示后置返回类型提供的类型,这样我们的模板就可以改为

template<class T1,class T2>
auto gt(T1 t1,T2 t2) -> decltype(x+y){
    return x+y;
}

你可能感兴趣的:(c++,c++,算法,图论)