【c++】模板进阶,模板的分离编译重点讲解

【c++】模板进阶,模板的分离编译重点讲解
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!

目录


前言

【c++】STL容器-list和vector的反向迭代器的模拟实现——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】模板进阶
在阅读本文前,请读者友友优先先阅读【c++】模板初阶 详情请点击<——
本文是基于【c++】模板初阶的基础上进行的一系列讲解

一、class和typename作为模板参数的关键字的区别

从风格来讲,typename更准确作为模板参数的关键字从基本使用上来讲class和typename基本没有区别,但是在一些场景下typename可以用于声明类型
  1. 以下面场景为例,你想要写一个打印函数用于打印vector<int>类型的对象中的数据,那么在打印函数内使用迭代器即可完成打印,函数中直接调用即可,这很正确
voidprint(const vector<int>& v){ vector<int>::const_iterator it = v.begin();while(it != v.end()){ cout <<*it <<' ';++it;} cout << endl;}intmain(){ vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3);print(v);return0;}
运行结果如下
  1. 但是现在我不想要打印vector<int>类型的对象的数据了,而是想要打印另一个vector<double>类型的对象的数据了,那么原打印函数由于函数的参数类型不匹配就不能够继续打印
  2. 那么我要是还想要使用模板去打印list对象的数据呢?是不是也没有办法去打印
  3. 此时我想要使用模板来写一个可以打印所有支持迭代器的容器的打印函数,通常来讲第一次写这种类型的打印函数都会出错,下面小编以错误写法为大家进行演示讲解
  4. 那么此时有了这个打印函数之后,那么小编接下来尝试进行调用打印
template<typenameContainer>voidprint(const Container& con){ Container::const_iterator it = con.begin();while(it != con.end()){ cout <<*it <<' ';++it;} cout << endl;}intmain(){ vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3);print(v); vector<double> v1; v1.push_back(1.1); v1.push_back(2.2); v1.push_back(3.3);print(v1); list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3);return0;}
运行结果如下

运行失败,编译器要求在Container::const_iterator it = con.begin();这一句前面加上typename作为前缀,奇怪,这是为什么呢?明明感觉没有错误呀其实这里是有错误的,域作用限定符作用域类可以去访问内嵌类型(typedef的类型和内部类)和静态成员变量(对象)我们知道当类中同时有内嵌类型和成员变量中有静态成员变量的时候,此时使用域作用限定符去访问,这里编译器是无法进行区分的,因为const_iterator并非关键字,在类中的静态成员变量也可以命名为const_iterator呀,假如这里的const_iterator是静态成员变量,也就是对象,那么这里就变成了Container::const_iterator it,使用对象去命名对象,那么在编译阶段是不是要给你报错,语法有错,这里如果是类型,那么至少编译不会报错,语法没有问题由于编译器无法区分这个地方是类型还是对象,此时如果没有其它说明,编译器就掀桌子了,你这里不编写清除我就直接给你报错了那么此时当我们明确这里使用的是类型,那么我们的typename就登场了,typename是类型的意思,在Container::const_iterator it = con.begin();这一句前面加上typename,当编译器在进行编译的时候告诉编译器,我这里是类型,不是对象,语法无误,不要报错了,等到这个函数模板实例化之后再进行检查,当函数模板实例化之后编译器进行检查的时候,类域中确实有typedef的const_iterator的这么一个类型,这样在编译阶段就可以过了
  1. 那么小编在Container::const_iterator it = con.begin();这一句前面加上typename之后重新运行
template<typenameContainer>voidprint(const Container& con){typenameContainer::const_iterator it = con.begin();while(it != con.end()){ cout <<*it <<' ';++it;} cout << endl;}intmain(){ vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3);print(v); vector<double> v1; v1.push_back(1.1); v1.push_back(2.2); v1.push_back(3.3);print(v1); list<int> lt; lt.push_back(1); lt.push_back(2); lt.push_back(3);print(lt);return0;}
运行结果如下

二、非类型模板参数

介绍

模板参数分为类型形参和非类型形参类型形参:出现在模板参数列表中,跟在class或typename之后的参数类型名称非类型形参:使用一个int或char类型的整形常量作为类(函数)模板的一个参数,这个参数可以在类(函数)模板中作为一个常量来使用
非类型模板参数主要用于在类模板中定义数组大小,变量不可以定义数组大小,int或char类型的整形常量可以定义数组大小,注意这个非类型模板参数必须是int或char类型的整形常量,其它类型均不行,并且这个int或char类型的整形常量(非类型模板参数)不可以进行修改
由于其是作为常数进行使用,所以这个非类型模板参数必须在编译阶段就要确定结果

使用

  1. 那么现在有一个需求,使用模板定义一个静态栈,实现数组大小的可传入控制,那么这时候就可以使用非类型模板参数,并且给这个非类型模板参数一个缺省值10
//静态栈template<typenameT, size_t n =10>classstack{public:private: T _a[n];int _capacity;int _size;};intmain(){ stack<int> st1; stack<int,100> st2;return0;}
运行结果如下,不同大小的静态栈就被定义出来了

array容器的使用

在这里插入图片描述
  1. 观察在array类模板的中同样也使用了非类型模板参数去定义固定大小的数据,这也是非类型模板参数的一个应用
  2. 其支持了迭代器,operator[]等,那么我们来包头文件#include<array>和展开命名空间std,进行使用一下array容器,并且array对其中存储的数据还不进行初始化

array容器所管理的数组是固定大小,所以也就不存在什么扩容插入之类的说法

在这里插入图片描述
#include<iostream>#include<array>usingnamespace std;intmain(){ array<int,5> a; a[0]=1;for(auto e : a){ cout << e <<' ';} cout << endl;return0;}
运行结果如下

array容器与c语言数组的对比

  1. 但是array容器相对于c语言的数组没有什么优势,c语言的数组同样支持它的大部分接口
intmain(){int a[5]; a[0]=1;for(auto e : a){ cout << e <<' ';} cout << endl;return0;}
运行结果如下
  1. 无非是array容器对于越界的检查更严格了,array容器对于越界检查非常严格,array可以检查出越界读和越界写
//注意:这是小编编写的错误示例,读者友友请不要这样写intmain(){ array<int,5> a1; a1[5]; a1[5]=10;return0;}
检查出了越界读和越界写
//注意:这是小编编写的错误示例,读者友友请不要这样写intmain(){int a[5]; a[5];return0;}
c语言中的数组对于越界读不检查,可以进行越界读
//注意:这是小编编写的错误示例,读者友友请不要这样写intmain(){int a[5]; a[5]=10;return0;}
c语言中的数组可以检查出少部分越界写

三、模板的特化

概念引入

  1. 使用模板可以实现一些与类型无关的代码, 但是对于一些特殊类型无法针对性的得出我们想要的结果,这时候就需要进行特殊处理。下面以小编编写的比较小于的函数函数为例进行讲解
template<typenameT>boolCompare(T a, T b){return a < b;}intmain(){int a =1, b =0;int* pa =&a,* pb =&b; cout <<Compare(a, b)<< endl; cout <<Compare(pa, pb)<< endl;return0;}
运行结果如下

分析,这里对于普通类型int的比较结果正确,但是对于int*的指针比较结果错误,这里小编本意是想要使用传指针特殊化比较指针指向的对象的大小,这里却比较成了指针,即对象的地址,那么这种结果无法达到小编的预期结果,应该如何处理呢?这时候应该使用模板的特化,即针对特殊类型,对函数(类)模板进行特殊化的实现
模板特化分为函数模板特化和类模板特化

四、函数模板特化

函数模板特化的注意事项:必须要先有一个基础的函数模板,因为特化的函数模板不能脱离基础的函数模板而独立存在必须要使用关键字template并且后面加空的尖括号<>表示这是函数模板的特化函数名后面跟尖括号<>,在尖括号中指定需要特化的类型特化的函数模板的参数列表中的形参的类型必须要跟模板函数的基础参数类型保持一致
模板函数的基础参数类型指的是内置类型和自定义类型和引用
template<typenameT>boolCompare(T a, T b){return a < b;}template<>bool Compare<int*>(int* a,int* b){return*a <*b;}intmain(){int a =1, b =0;int* pa =&a,* pb =&b; cout <<Compare(a, b)<< endl; cout <<Compare(pa, pb)<< endl;return0;}
运行结果如下
  1. 通常来讲,当遇到函数模板需要进行特化的时候,我们一般直接进行函数重载,函数重载后,编译器在进行调用的时候会优先调用与自身类型最匹配的或者现成已经实例化的函数
  2. 这种函数重载的方式易于编写,可读性强
boolCompare(int* a,int* b){return*a <*b;}

五、类模板特化

全特化

全特化是将模板参数列表中的全部参数都确定化
template<typenameT1,typenameT2>classA{public:A(){ cout <<"A<T1, T2>"<< endl;}private: T1 _a; T2 _b;};template<>classA<double,double>{public:A(){ cout <<"A<double, double>"<< endl;}private:double _a;double _b;};intmain(){ A<int,int> a; A<double,double> a1;return0;}

偏特化

偏特化:任何针对模板参数进行进一步的条件限制设计的特化版本。那么以下面的类模板为基础类模板进行设计
template<typenameT1,typenameT2>classA{public:A(){ cout <<"A<T1, T2>"<< endl;}private: T1 _a; T2 _b;};
偏特化的两种表现形式:部分特化参数更进一步的限制
部分特化
将模板参数列表中的部分参数进行特化
template<typenameT>classA<T,double>{public:A(){ cout <<"A<T, double>"<< endl;}private: T _a;double _b;};
参数更进一步限制
可以针对模板参数更进一步进行条件限制设计出来的特化版本同时我们进行设计的特化的类模板中的成员没有必要追求和原类模板的成员一样,应该具体根据实际的应用场景去进行设计成员
  1. 针对指针的进一步限制
template<typenameT1,typenameT2>classA<T1*, T2*>{public:A(){ cout <<"A<T1*, T2*>"<< endl;}private:};
  1. 针对引用的进一步限制
template<typenameT1,typenameT2>classA<T1&, T2&>{public:A(){ cout <<"A<T1&, T2&>"<< endl;}private:};
测试
intmain(){ A<int,int> a; A<int,double> a1; A<int*,int*> a2; A<int&,int&> a3;return0;}
测试结果如下

类模板特化的应用举例

下面的讲解会应用到仿函数,不了解的读者友友可以先点击后方蓝字进行阅读学习仿函数的介绍与使用 详情请点击<——
  1. 我们编写一个进行比较小于的仿函数,但是只能处理普通类型的比较
  2. 对于我们传的指针类型,我们本意是想要比较指针指向的对象的大小关系,这里进行比较的是对象的地址(指针),比较错误,不符合我们的预期
template<typenameT>classLess{public:booloperator()(T a, T b){return a < b;}};intmain(){ Less<int> com;int a =1, b =0; cout <<com(a, b)<< endl; Less<int*> com1;int* pa =&a;int* pb =&b; cout <<com1(pa, pb)<< endl;return0;}
运行结果如下
  1. 那么我们针对指针类型对类模板的参数进行更进一步的条件限制实现特化版本,进而达到我们的预期效果
  2. 这样当模板参数的类型为指针类型的时候,就会去调用我们限制的指针类型的特化版本,对指针指向的对象的大小进行比较
template<typenameT>classLess{public:booloperator()(T a, T b){return a < b;}};template<typenameT>classLess<T*>{public:booloperator()(T* a, T* b){return*a <*b;}};intmain(){ Less<int> com;int a =1, b =0; cout <<com(a, b)<< endl; Less<int*> com1;int* pa =&a;int* pb =&b; cout <<com1(pa, pb)<< endl;return0;}
运行结果如下

六、模板的分离编译

什么是分离编译

什么是分离编译:一个程序(项目)由多个源文件共同实现,而每个源文件单独编译形成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
  1. 那么小编使用栈的模拟实现的代码进行讲解,并且为了便于讲解,小编将栈的模拟实现代码进行一定的删减,在Stack.h下面是声明和定义不分离的源代码,下面小编将针对Stack.h进行改编讲解
namespace wzx {template<typenameT,typenameContainer= std::deque<T>>classstack{public:voidpush(const T& val){ _con.push_back(val);} T&top(){return _con.back();}private: Container _con;};}

通常来讲模板为什么不能声明和定义分离

//Stack.h#include<deque>namespace wzx {template<typenameT,typenameContainer= deque<T>>classstack{public:voidpush(const T& val); T&top(){return _con.back();}private: Container _con;};}//Stack.cppusingnamespace std;#include"Stack.h"namespace wzx {template<typenameT,typenameContainer>voidstack<T, Container>::push(const T& val){ _con.push_back(val);}}//test.cpp#include<iostream>usingnamespace std;#include"Stack.h"intmain(){ wzx::stack<int> st; st.push(1); st.top();return0;}
运行代码,会出现链接错误
  1. 这里的stack是类模板,在进行编译的时候,由于Stack.cpp和test.cpp是分离编译,所以各自会进行各自的预处理编译汇编,直到链接的时候才会交互
  2. 那么在test.cpp中预处理阶段会进行展开的头文件Stack.h,那么在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,那么test.cpp在进行编译的时候会将push和top函数名符号进行汇总起来,同时在编译阶段在test.cpp中有stack<int>,T会被确定为int,那么也就可以进行对应的实例化了,会将stack类模板对应有定义的函数进行实例化,函数实例化之后定义才能的到地址,在汇编阶段根据这两个符号去找对应的地址,这时候push,虽然有类型T,不可以进行实例化函数,由于没有定义没有进行实例化,所以push找不到地址,top有定义并且进行了实例化找到地址了,那么在test.o中的符号表中,push没有对应的地址,top有对应的地址,但是由于push有声明并不会直接进行报错因为链接时在其它的目标文件中可能会存在对应符号的地址进行汇总
  3. 那么在Stack.cpp中预处理阶段会进行展开头文件Stack.h,那么在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,在Stack.cpp中有push的定义(注意这里是类模板),那么Stack.cpp中也没有类似于test.cpp中的stack<int>进行实例化类模板,并且这里也没有进行显示实例化类模板,那么此时T的类型就不能进行确定,在编译阶段,会将push和pop符号汇总起来,并且由于T的类型无法确定,没有办法实例化类模板,那么stack类模板中对应的函数也就不会进行实例化,那么函数不进行实例化函数也就不会有对应的地址,在汇编阶段去根据对应的符号去寻找符号对应的地址的时候就没有办法找到符号对应的地址,那么在Stack.o的符号表中,push和top都没有对应的地址,但是由于push和top有声明并不会直接进行报错因为链接时在其它的目标文件中可能会存在对应符号的地址进行汇总
  4. 在链接过程,test.o中的符号表中,push没有对应的地址,top有对应的地址,在Stack.o的符号表中,push和top都没有对应的地址,此时进行符号表的合并和符号地址的重定位之后,push还是没有对应的地址,top有对应的地址了,那么此时push没有对应的地址,编译器就会进行报错,那么就会出现链接错误,无法找到符号(函数)对应的地址

怎么样模板才能进行分离编译(只能缓解分离编译,本质上模板不能进行分离编译)

  1. 模板声明和定义分离放在不同的源文件中,进行模板的显示实例化,这里是以类模板为例进行讲解的,那么这里就进行类模板的显示实例化进行讲解
类模板的显示实例化的注意事项:使用关键字template注意这个关键字后面不加分号class 类名加尖括号<>; 尖括号内放需要进行对应实例化的模板参数的类型,注意这个语句后面必须要加分号
//Stack.cpp#include"Stack.h"usingnamespace std;namespace wzx {template<typenameT,typenameContainer>voidstack<T, Container>::push(const T& val){ _con.push_back(val);}templateclassstack<int>;}//Stack.h//test.cpp//这两个文件中维持原代码不变
测试结果如下,push和top可以正常调用运行

模板不分离编译做法

  1. 声明和定义分离编译之后将声明和定义放在同一个源文件中
//Stack.h#pragmaonce#include<deque>namespace wzx {template<typenameT,typenameContainer= deque<T>>classstack{public:voidpush(const T& val); T&top(){return _con.back();}private: Container _con;};template<typenameT,typenameContainer>voidstack<T, Container>::push(const T& val){ _con.push_back(val);}}//test.cpp#include<iostream>usingnamespace std;#include"Stack.h"intmain(){ wzx::stack<int> st; st.push(1); st.top();return0;}
运行结果如下

那么下面小编来分析一下,为什么这种方式可以正常调用push和top?在预处理阶段,test.i中会展开Stack.h头文件,在Stack.h中有push的声明,没有push的定义,在Stack.h中有top的声明和定义,在stack类模板外的Stack.h中声明了类域编写有push的定义,那么在Stack.h中就包含有push和top的声明和定义,进行展开后,在test.cpp中就会有push和top的声明和定义在编译阶段,test.s中会汇总符号,即收集了函数名push和top,并且在此阶段由于代码中编写有stack<int>,那么T就会被确定为int类型,那么就会进行stack类模板的实例化,那么在stack类模板中的函数push和top由于T的类型被确定为int,那么stack模板类的类型就被确定为stack<int>就可以进行类模板的实例化,并且push和top都有对应的定义,就可以进行函数实例化,函数实例化之后push和top函数就有了地址,注意在编译阶段并不会将地址进行收集起来,而是在汇编阶段才会根据符号去寻找对应的地址在汇编阶段,test.o中根据编译阶段汇总的符号,即根据汇编收集的函数名push和top去寻找对应的地址,那么在编译阶段push和top都进行了实例化,都有了地址,那么就可以正常找到地址,进而根据符号和地址去生成符号表,那么此时test.o中的符号表中就有push和top的地址在链接阶段,a.out中进行符号表的合并和重定位,此时test.o的符号表有push和top的地址,链接后生成的符号表中push和top也都有对应的地址,此时编译器进行检查就不会报错,也就不会出现链接错误了

七、模板的总结

优点:模板复用了代码,节省了资源,可以进行更快的迭代开发,c++标准库(STL)因此产生编写者不用再去重复写相同的工作,并且可以根据特定的需求进行模板特化,代码的灵活性增强

缺点:模板会导致代码膨胀(假设编写者本该编写5份类型不同,但是实现大体相同的代码,由于模板的存在,编写者只需要编写一份和类型无关的代码,而是把实例化编写5份大体重复的代码的任务交给编译器去做,不可避免的会造成代码膨胀),同时由于模板需要进行实例化,也会导致编译的时间变长出现模板编译错误的时候,编译器的提示信息比较凌乱,不易于编写者根据提示信息去定位错误

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

Read more

CPP-Summit-2022 学习:Modern C++基于性能的重构优化

C++软件性能现状与优化挑战 一、软件性能优化本质 性能优化的核心目标: 减少指令数,提高每秒可执行浮点操作数(GFLOPS),充分利用硬件特性。 通过表格可以看出,不同语言/优化策略的性能提升非常悬殊: 二、性能优化的数学本质 1. 运行时间与指令数关系 运行时间 T T T 与总指令数 I I I 和每秒执行速率 R R R 的关系为: T = I R T = \frac{I}{R} T=RI * I I I:总指令数(越少越快) * R R R:硬件执行速率(可受矢量化、并行化影响) 2.

By Ne0inhk
【C++】多态

【C++】多态

多态 ✨前言:在 C++ 的世界里,“多态(polymorphism)” 是面向对象编程的灵魂之一。 它让同一个接口在不同对象上表现出不同的行为,从而大大提升了代码的复用性、扩展性与灵活性。 本文将带你深入理解多态的核心原理,从概念、实现条件、虚函数、重写规则,到虚函数表与动态绑定机制,逐步揭开多态背后的运行逻辑。 📖专栏:【C++成长之旅】 目录 * 多态 * 一、多态的概念 * 二、多态的定义及实现 * 2.1 多态的构成条件 * 2.1.1 实现多态还有两个必须重要条件: * 2.1.2 虚函数 * 2.1.3 虚函数的重写/覆盖 * 2.1.4 多态场景的⼀个选择题 * 2.1.

By Ne0inhk