• 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;
    }
    
  • 相关阅读:
    蓝桥杯双周赛算法心得——摆玩具(分段的差不计入结果)
    深度学习中的一些概念
    一文了解VR全景,VR全景有哪些优势?
    面试官不按套路,竟然问我Java线程池是怎么统计线程空闲时间?
    C++二要素认证,游戏实名认证接口、金融实名认证
    《C++ Primer》练习9.51:设计类解析不同的输入
    Word使用小技巧
    港联证券:市场有望从2024年起进入大众化折叠屏手机时代
    【解决问题】在SpringBoot中通过配置Swagger权限解决Swagger未授权访问漏洞
    计算机毕业设计(附源码)python职工社保信息管理系统
  • 原文地址:https://blog.csdn.net/weixin_43903639/article/details/126961449