ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

模板初阶(C++)

模板初阶(C++)


  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页吃透C语言专栏数据结构Gitee仓库C++Linux算法

文章目录

  • 一,泛型编程
  • 二,函数模板
    • 1 函数模板概念
    • 2 函数模板格式
    • 3 函数模板的原理
      • 1 类型推演
    • 4 函数模板的实例化
      • 1 隐式实例化
      • 2 强制类型转换
      • 3 显示实例化
    • 5 多个模板参数
      • 1 返回值类型T无效的原因
    • 6 模板参数的匹配原则
  • 三,类模板
    • 1 定义格式
    • 2 成员函数声明和定义分离
    • 3 类模板实例化

一,泛型编程


  • 泛型编程:算法/数据结构与具体类型解耦,类型作为参数传入,只描述操作逻辑,类型在编译期确定。

  • 核心思想:不针对特定类型写代码,写出一套可以复用、适用于多种类型的通用逻辑,把「类型」当成参数。C++ 中主要靠模板 (template)实现

Swap交换函数,我们需要交换的数据是各种类型的,int,double,char,但是Swap交换函数的逻辑都是相同的,函数体中的代码除了数据类型,其他部分都是一样的,但是因为数据类型不同,我们需要写一个个函数重载,大量重复相同的代码。但是C++中泛型编程中,模板实现了多种类型通用一套逻辑,不针对特定类型写代码


  • 总结:仅仅因为数据类型不同,而需要写多个针对类型不同而导致重复代码和逻辑。而模板的方式可以制定一个多种类型通用逻辑,告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码

模板

函数模板

类模板


二,函数模板

1 函数模板概念

  • 函数模板:是一个模板,不是真实函数。编译器根据你传入的参数类型,实例化生成对应类型的真实函数

  • 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定
    类型版本

2 函数模板格式

template<typename T>TAdd(constT&left,constT&right){returnleft+right;}intmain(){std::cout<<Add(1,2)<<std::endl;std::cout<<Add(1.1,2.3)<<std::endl;return0;}


项目解析
template<>声明这是模板,template翻译就是模板
typename定义模板参数的关键字,也可以用class
T模板形参,叫啥都行,就是一个参数名,T代表type的意思,最后实参的类型会替换这个模板形参

  • 总结:模板格式就是template < typename T1,typename T2,typename T3 ...... >声明这是模板,设置好模板形参T,剩下的就是按照正常函数的写法去写,把类型都替换成模板形参T,不指定类型,用模板形参T,编译器会根据实参中具体的类型,去推演T,形成对应的函数

3 函数模板的原理

1 类型推演

  • 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

  • 类型推演:在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此

template<typename T>TAdd(constT&left,constT&right){returnleft+right;}Add(1,2);Add(1.1,2.2);
  • **Add(1,2)中,编译器通过实参 1推演出第一个形参是 int 类型,const T& left中的T被推演成int,编译器通过实参 2,推演出第二个形参是int 类型,const T& right,中的T被推演成int,而left和right都是T,确定了T就是int,所以返回值类型T,也被推演成int,所以函数变成了int Add(const int& left,const int& right),而Add(1.2,2.2)也是同理,编译器通过实参传入的类型,来推导出模板形参T的类型,生成对应的特定类型的函数。只有推演成功,才会去替换,替换的过程叫作隐式实例化,如果在推演的过程中,出现第一个形参是int类型,第二个形参是double*类型,而且只有一个模板形参T,如Add(1,1.1),T出现矛盾,无法确认T。

4 函数模板的实例化

  • 用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

  • 解析:函数模板的实例化也就是函数模板确定成指定类型的函数的过程,如T Add(T x,T y)-->int Add(int x,int y),这就是函数模板的实例化

1 隐式实例化

  • ** 隐式实例化:让编译器根据实参推演模板参数的实际类型**
template<class T>TAdd(constT&left,constT&right){returnleft+right;}intmain(){std::cout<<Add(1,2)<<std::endl;}
  • 隐式实例化就是由编译器自己通过模板和实参,来推演出参数的实际类型,最后确定特定类型函数的过程,隐式的关键点在于是由编译器自己推演得到函数的过程。如T Add(const T& left,const T& right)-->int Add(const int& left,const int& right)

前面就提到过,当只有一个模板形参T的时候,但是实参出现了两种类型,Add(1,1.2)的时候,模板形参T推演出两种类型,模板形参类型不统一,无法确认模板形参,这时候推演出错,无法通过编译,这时候,在不改变模板形参个数的条件下,解决的办法有两种,1:用户自己强制类型转换实参。2:使用显示实例化。目标都是让实参的类型统一。


2 强制类型转换

template<typename T>TAdd(constT&left,constT&right){returnleft+right;}intmain(){inta=10,b=20;doublec=10.1,d=20.2;std::cout<<Add(a,(int)c)<<std::endl;std::cout<<Add((double)a,c)<<std::endl;return0;}

  • 只有函数模板的时候,没有函数,实参是无法隐式类型转换的,所以在只有一个模板参数的条件下,为了统一实参的类型,我们可以将实参强制类型转换成统一类型,这样就可以使模板参数推演的类型统一,缺点是强制类型转换会导致精度的丢失

3 显示实例化

  • 在函数名后的<>中指定模板参数的实际类型,如Add<int>(a,b),这样就会跳过编译器通过实参类型推演模板形参类型的过程,直接将模板形参类型替换为<>内指定类型,实例化出指定类型的函数
template<typename T>TAdd(constT&left,constT&right){returnleft+right;}intmain(){inta=10,b=20;doublec=10.1,d=20.2;std::cout<<Add<int>(a,c)<<std::endl;std::cout<<Add<double>(a,c)<<std::endl;return0;}

  • 显示实例化了以后,编译器就不会进行推演了,直接将模板参数替换成<>中指定的类型,其实原理是实参隐式类型转化成<>中指定的类型

5 多个模板参数

强制类型转换实参和显示实例化都是在只有一个模板参数的前提下,所以去改变实参类型去满足模板单参条件,但是模板参数是没有限制的,我们下面来讲解多个模板参数的情况


template<class T1,class T2>autoAdd(constT1&left,constT2&right){returnleft+right;}intmain(){inta=10,b=20;doublec=10.1,d=20.2;std::cout<<Add(a,c)<<std::endl;std::cout<<Add<int>(c,d)<<std::endl;std::cout<<Add<int,double>(c,b)<<std::endl;return0;}

template<class T1,class T2>autoAdd(constT1&left,constT2&right)
  • 1️⃣Add(a,c),T1推演实参a(int),T2推演实参c(double),推演结束后进入模板隐式实例化,最后得到函数auto Add(const int& left,const T2& right),当函数Add(a,c)调用函数的时候,auto自动计算函数体中return left + right结果的类型。这时候,通过多个模板参数,配上auto作为返回值类型,就解决了实参类型不统一的问题,不需要因为单个模板参数而强制实参类型统一,使用多个模板参数推演实参得到对应的函数即可

  • 2️⃣Add<int>(c,d),只要是函数模板,就可以显示实例化。显式指定的实参从左到右依次绑定到模板形参列表,剩余未指定的模板形参则继续通过函数实参进行推演,所以绑定到了T1,T1不再推演,直接由<int>绑定,但是T2并没有显示指定的类型,所以T2依然推演d的类型,所以结果是30.2

  • 3️⃣Add<int,double>(c,b)显示实例化两个实参,int直接绑定T1,double直接绑定T2,不走推演

1 返回值类型T无效的原因

template<class T1,class T2>autoAdd(constT1&left,constT2&right){returnleft+right;}

template<class T1,class T2,class T3>T3Add(constT1&left,constT2&right){returnleft+right;}

  • C++ 函数模板的模板参数推导,只看函数调用传入的实参,模板的推演发生在实例化的前面,而实例化在编译函数体的前面,所以模板参数的推演只有通过函数实参进行推演,T3位于返回值类型的位置,在模板形参中不存在,所以无法推演得到T3的类型,更何况推演发生在实例化之前,发生在编译函数体之前,在推演的时候,根本就不会进入函数体,就算进入函数体,也不会去运算left + right,所以无论怎么样都不可能得到返回值类型,更不用说推演了,所以只要记住模板参数推演,只会通过函数调用的实参来进行推演,模板参数类型从左到右和实参类型从左到右依次绑定,这就是推演,然后把模板参数中推演得到的类型,把对应的参数全部替换成推演得到的类型,如T1全部替换成int,形成一个函数,这就是实例化的过程。

  • 而如果想让T3被推演,只能在模板形参部分写上T3,然后实参部分传对应类型的参数,这才能完成推演,完成推演之后,所有的T3都被替换成推演得到的类型,这时候返回值类型T3才能变成推演得到的类型。但是工程上不会这样写的,为了一个返回值刻意去加模板形参和函数实参

  • auto的写法,是在函数调用的时候,函数体运算的过程中,接收return的结果并推导对应的类型

  • 重点:❗❗❗所以彻底了解了模板参数类型仅仅和实参绑定,仅仅由推演实参得到,模板参数通过实参类型推演确定了以后,才会把所有的模板参数(返回值类型中,函数体中的模板参数)替换成推演得到的类型。所以在除了模板形参的地方写模板参数,最终都是要靠模板形参推演得到的类型实例化的时候替换成对应的类型,但是有一些模板中没有模板形参,模板参数是写在函数体或者返回值中的,无法通过函数实参推演得到模板参数类型,这时候就需要用到显示实例化指定模板参数的类型了,而出现多个模板形参的情况,就是按照函数实参从左往右推演匹配,显示实例化也是从左往右按顺序指定模板形参类型
template<class T>T*func(inta){T*p=(T*)operatornew(sizeof(T))new(p)T(a);returnp;}intmain(){func(1)//形参不是模板形参,无法推导func<int>(1)//显示实例化,直接指定模板参数类型,跳过推演}
  • ⬆️没有模板形参,无法通过推演函数实参类型而实例化函数,但是我们可以显示实例化,直接指定模板参数类型,跳过推演的过程

  • 总结:模板本身就是作用就是通过模板形参,推演函数实参,推演模板形参对应的类型,让后把所有的形参替换成推演得到的类型,实例化成函数的过程:所以理解了这个流程我们就可以注意下面这些要素:1:只能通过模板形参来推演函数实参,推演成功才能把所有对应的模板参数替换成对应类型。所以在一个模板形参的条件下就不能出现函数实参类型不同导致推演前后矛盾的情况,还有没有模板形参,但是模板参数写在别的位置,如返回值类型和函数体中,没有模板形参推演得到类型,其他的模板参数无法被替换,所以这时候就可以使用显示实例化,不需要模板形参来进行推演类型,直接指定模板参数的类型,这时候就不需要模板形参了

6 模板参数的匹配原则

1️⃣

intAdd(intleft,intright){returnleft+right;}

2️⃣

template<class T1,class T2>autoAdd(T1 left,T2 right){returnleft+right;}

3️⃣

template<class T>TAdd(constT&left,T right){returnleft+right;}

  • (1,2)当1️⃣2️⃣3️⃣同时存在的时候,函数适配,模板函数也适配,优选选择函数,不会去选择模板函数

  • (1.1,2.2)当1️⃣2️⃣3️⃣同时存在的时候,函数不适配,模板3️⃣,只有一个模板参数T,而模板2️⃣,有两个模板参数T1,T2,而实参为(double,double)是同类,所有优先使用3️⃣一个模板参数

  • (1,2.0)当1️⃣2️⃣3️⃣同时存在的时候,函数不适配,模板3️⃣只有一个模板形参也不适配,模板2️⃣,有两个模板参数,适配

  • 总结:1:当模板和函数同时存在的时候,优先和匹配的函数适配,只有没函数匹配的时候才去看模板 2:当在模板中二选一的时候,只会去适配能匹配的模板,如果匹配不理,模板形参都矛盾。

  • 补充:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换,因为函数模板本身就不是函数,只是一个模板,怎么可能进行类型转换,模板需要通过实参去推演,然后实例化才会生成函数,不可能自动类型转换。

三,类模板

1 定义格式

template<class T1,class T2,...,class Tn>class 类模板名{// 类内成员定义};
  • 类模板和函数模板一样,仅仅是一个模板而已,需要经过实例化才能变成类或者函数

template<typename T>class Stack{private:T*_arr;int_top;int_capacity;public:Stack(intcap=4){_arr=new T[cap];_top=0;_capacity=cap;}voidPush(constT&val){_arr[_top++]=val;}TTop(){return_arr[_top-1];}~Stack(){delete[]_arr;}};

  • 模板类中的数据替换成了模板参数T,当实例化的时候,把T替换成设定好的参数,就成了我们需要的类,如果是C语言,我们只能每次都在typedef中修改,而现在有了模板类,我们只需要在实例化的时候设定我们需要的类型替换模板参数就可以了。

2 成员函数声明和定义分离

//类模板成员函数声明和定义分离template<class T>class Stack{public:T _data;voidfunc(T val);};template<class T>voidStack<T>func(T val){_data=val;}
  • 如果类模板成员函数声明和定义分离的时候,在类外需要重新写模板声明template<class T>告诉编译器T是谁,而类外定义函数要声明类域,类域就是类型 + ::,但是对于模板类,类型是类模板名+<模板参数>如Stack<T>

3 类模板实例化

Stack<int>s1;Stack<double>s2;
  • 模板名+<模板参数>,就是类的类型
  • 点击表格内对应链接跳转对应内容⬇️⬇️⬇️
作者主页吃透C语言专栏数据结构Gitee仓库C++Linux算法

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进