模板进阶 | 非类型模板参数 | 类模板的特化 | 模板的分离编译 | 模板的优缺点

2024-04-13 16:20

本文主要是介绍模板进阶 | 非类型模板参数 | 类模板的特化 | 模板的分离编译 | 模板的优缺点,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  非类型模板参数

我们可以认为非类型模板参数就是一个常量,在我们的类里面我们是不能对它进行改造

为什么会有这样的场景,其次就是C语言那里我们一般使用什么。

场景1

#include<iostream>
using namespace std;#define N 10
template<class T>
class Array
{
public:T& operator()(size_t index){return _arr[index];}const T& operator[](size_t index){return _arr[index];}size_t size(){return _size;}bool empty(){return _size == 0;}private:T _arr[N];size_t _size;};int main()
{Array<int> a;return 0;
}

 在我们上面这个场景中,我们要开辟这样一个数组,通过宏来控制我们静态数组的大小,这个也是C语言经常使用的场景,但是假如我们需要开辟另一个空间很大的数组,现在面对这种情况有两种方法,一种是改变宏的大小,这样就可以改变了,但是这个方法还是有一定的缺陷,比如我们一个数组的空间需要很大的时候,但是这个时候还有一个数组的空间是很小的时候,那这个时候两边都是需要进行满足的,所以就会在存在有一个数组的空间是会存在浪费的,第二个方法就是我们在写一个模板,需要一个数组就去再写一个模板,ctrl c + ctrl v

就能解决

所以面对上面的问题,我们也是有两种选法,但是都不能从根本上解决问题,所以就有了我们现在C++用到的非类型模板参数可以解决

改变代码

template<class T,size_t N = 10>
class Array
{
public:T& operator()(size_t index){return _arr[index];}const T& operator[](size_t index){return _arr[index];}size_t size(){return _size;}bool empty(){return _size == 0;}private:T _arr[N];size_t _size;};int main()
{Array<int> a;Array<int,20> b;return 0;
}

这样就可以解决我们的问题,这里需要记住的是非类型模板参数其实是个常量,而且必须是整型

总结使用场景

我们需要定义一个静态数组的时候就可以是用的到,我们的库里面其实就是有一个array,但是实际上非类型模板参数其实是苦了我们的编译器,我们的模板这样写之后,编译器就能根据你的需求去进行实例化              

                                                                                   --------很像到家思想的 死道友不死贫道

注意: 当前编译器只支持整型,非类型模板也是支持缺省参数(类型也是支持的,比如优先级队列就是这样的)

区别函数传参数和模板传参数

 函数传参数

是我们在运行的时候来确定参数的,就比如我们之前写的一些函数,这里用 add函数来说

int Add(int x, int y)
{return x + y;
}

比如在这个场景下我们要进行函数传参的时候,如果我们不去调用其实就没有传参数的过程,所以也就是只有在我们调用的时候才是会出现传参数

模板传参数

模板传参数就是在我们实例化的时候会进行,虽然有我们的按需实例化(后面讲),但是模板传参数的时候一定是在我们编译的时候编译器就是得知道的事

array真的很好吗

上面讲的array我们库里面其实是有的,但是array这个东西真的有这么好用吗,其实不是的,我们的vector可以完全替代array,array这个类是有点鸡肋的功能,而且他是静态的数组,在我们进行扩容的时候或者空间满的时候就不能进行调整了,所以array煤油vector好

但是这个发明肯定不是区别于我们的vector,是区别于我们的普通数组,因为它的检查更加严格,在我们的数组的时候对越界的检查其实是抽查,就和酒驾是一样的,而且越界读一般是不能检查出来的,越界写是抽查,但是array这些越界访问的操作都是能检查出来, 这就是array的唯一的优点了。但是还是想吐槽它的优点太有限了,我们vector也能进行检查访问,因为检查其实就是一个断言而已,这样就是一个小小的函数调用而已。

它还有缺陷 : 会导致栈溢出,因为我们栈的空间不是很大,堆的空间才很大。所以我们能用vector绝对不用array

按需实例化

演示按需实例化


template<class T,size_t N = 10>
class Array
{
public:T& operator()(size_t index){return _arr[index];}const T& operator[](size_t index){size(1);//有问题return _arr[index];}size_t size(){return _size;}bool empty(){return _size == 0;}private:T _arr[N];size_t _size;};int Add(int x, int y)
{return x + y;
}
int main()
{Array<int> a;Array<int,20> b;return 0;
}

这个地方我注释的地方语法是不是有问题的,但是我们如果编译的时候是没有报错误的。

我们模板在预处理的时候和编译过程中增加了一个环节 

根据模板实例化 - > 半成品模板 - > 实例化具体的类或者函数 - > 语法进行编译

所以我们可以猜测一下这里我们应该是 没有进行实例化!!!

哪怕我们这里写了实例化,也是没有检查出这个语法,所以结论就是我们的编译器是按需实例化的

也就是我们的成员函数调用哪个就进行哪个的实例化,我们这里没有调用这个operator[]函数,所以就不会进行报错,因为我们的编译就没有对他进行调用,那只有我们去调用的时候就会去实例化,这样就能检查出来这个问题了。

 模板的特化

函数模板的特化

场景1: 如果我们要对我们的日期进行比较大小 所以需要先有一个日期类的实现的代码

class Date
{
public:friend ostream& operator<<(ostream& _cout, const Date& d);Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}

如果正常比较日期的话我们的代码是没有问题的,比如比较两个日期的大侠

class Date
{
public:friend ostream& operator<<(ostream& _cout, const Date& d);Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}// 函数模板
template<class T>
bool Less(T left, T right)
{cout << "bool Less(T left, T right)" << endl;return left < right;
}
int main()
{Date d1(2024, 1, 1);Date d2(2023, 2, 2);cout << Less(d1, d2) << endl;return 0;
}

这样我们就是可以正常的进行比较的,但是现在我们给出场景2,如果遇到的是Date* 的指针呢,我们这个时候就是需要模板的特化

场景2 ------ 引出特化

我们现在要进行比较的是我们的Date* 指针的大小,我们可以写一个这样的模板解决。

class Date
{
public:friend ostream& operator<<(ostream& _cout, const Date& d);Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}
private:int _year;int _month;int _day;
};ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}// 函数模板
template<class T>
bool Less(T left, T right)
{cout << "bool Less(T left, T right)" << endl;return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right)
{return *left < *right;
}
int main()
{Date d1(2024, 1, 1);Date d2(2023, 2, 2);cout << Less(d1, d2) << endl;Date* pd1 = new Date(2023, 12, 1);Date* pd2 = new Date(2023, 12, 3);cout << Less(pd1, pd2) << endl;return 0;
}

这样就对我们的Date* 进行比较了。

但是这个只是特化的最基础的使用。这是对于某种特殊类型的特殊处理

场景3

我们上面只是解决了我们Date* 的比较,如果我们还是需要对我们所有的指针进行比较的时候,我们的代码需要怎么写呢????

template<class T>
bool Less(T* left, T* right)
{return *left < *right;
}

只需要这样写就能解决,这样可以测试不同的指针了。

注意 : 上面写的都是全特化 ,建议 : 函数模板不建议使用特化,使用重载能解决大部分的问题。

类模板的特化

全特化

template<class T1, class T2>
class Date
{public:Date(){cout << "Date<T1,T2>" << endl;}
private:T1 _d1;T2 _d2;
};//全透化template<>
class Date<int, char>
{
public:Date(){cout << "Date<int,char>" << endl;}
};
int main()
{Date<int, char> d1;Date<int, int> d2;return 0;
}

其实看代码就是能看出来,我们给出了他们的具体类型,这个就是全透化。

半特化/偏特化
template<class T1>
class Date<T1, char>
{
public:Date(){cout << "Date<T1,char>" << endl;}
};

上面的这个就是我们的特化的全部语法,但是要知道半特化不一定是特化部分的参数,只是对一些参数的限制,我们这里也能使用指针和引用(list的迭代和反向迭代器就是这样的)。就不演示了

template<class T>
class Date<T*,T*>
{
public:bool operator()(T* x, T* y){return *x < *y;}
};

总结 : 想针对某种类型进行特殊处理的时候就可以考虑使用特化。

模板的分离编译

模板是不支持声明和定义分离进行编译的,因为我们的编译器是不知道我们需要实例化成什么类,这里不支持分离编译是指在两个不同的文件下,而不是在同一个文件,我们来举出一个例子,然后进行分析,首先就是我们需要创建一个头文件,一个就是Fun.h,然后就是fun.cpp,还有就是test.cpp

Fun.h

#pragma once#include<iostream>
using namespace std;template<class T>
T Add(const T& x, const T& y);void Fun();

fun.cpp


#include"Fun.h"template<class T>
T Add(const T& x, const T& y)
{return x + y;
}void Fun()
{cout << "void Fun()" << endl;}

test.cpp

     

#include"Fun.h"int main()
{int ret = Add(1, 3);return 0;
}

   然后进行编译就会出现这样的报错信息

但是我们的普通函数就是可以通过,而且要注意这里的报错不是编译问题,而是链接问题

分析程序在预处理,编译,汇编以及链接做的事情

 

预处理

预处理这一部分需要做的是条件编译,宏替换,去注释,头文件展开,所以我们的每个.cpp文件里的头文件都会进行展开,因为编译器是不会对我们写的.h文件进行编译的。

编译

编译这一步很重要,会进行的事情很多,比如要语法分析,语意分析,还会生成语法树,就是在检出我们写的代码语法是不是正确,最后就是生成汇编代码。

汇编

生成二进制代码,形成目标文件

链接

合并生成可执行文件

注意: 这里只是讲个大概,具体的之前的文章也是讲过的。

两个文件都进行编译之后生成的目标文件,然后进行链接之后,我们的fun函数是有它的地址的,这个地址相当于一个跳转的指令,和我们之前所得call地址可以认为是一样的,fun函数是能成功生生call地址的,但是我们的函数模板,它会按需实例化,我们也不知道T要变成什么类型,这也就导致了最后链接时候找不到地址,所以才会链接错误

解决办法

1.模板的声明和定义放在同一个头文件,不要进行分离编译

2.声明类型,让编译器能实例化。也就是显示实例化

结论就是我们还是最好模板的声明和定义放在一起,这才是最好的解决方法。

模板的优缺点

【优点】
1. 模板复用了代码,节省资源,更快的迭代开发, C++ 的标准模板库 (STL) 因此而产生
2. 增强了代码的灵活性
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误
今天的分享就到这里,下次分享的是oo语言的三大特性之一继承

这篇关于模板进阶 | 非类型模板参数 | 类模板的特化 | 模板的分离编译 | 模板的优缺点的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/900621

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

4B参数秒杀GPT-3.5:MiniCPM 3.0惊艳登场!

​ 面壁智能 在 AI 的世界里,总有那么几个时刻让人惊叹不已。面壁智能推出的 MiniCPM 3.0,这个仅有4B参数的"小钢炮",正在以惊人的实力挑战着 GPT-3.5 这个曾经的AI巨人。 MiniCPM 3.0 MiniCPM 3.0 MiniCPM 3.0 目前的主要功能有: 长上下文功能:原生支持 32k 上下文长度,性能完美。我们引入了