C++---模板进阶(非类型模板参数,模板的特化,模板分离编译)

2024-06-07 06:20

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

我们都学习和使用过模板,而这篇文章我们来将一些更深入的知识。在此之前,我们在使用C++编程时可以看到模板是随处可见的,它能支持泛型编程。模板包括函数模板和类模板,我们有的人可能会说是模板函数和模板类,但严格讲这样说是错误的。而在我们实际使用中,类模板用的场景是比函数模板多的,如STL中vector,list等都是类模板,而算法中sort,find等是函数模板。

非类型模板参数

模板参数分为类型模板形参,非类型形参。

类型形参:出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。(我们之前一直使用的)

非类型形参:用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

举个例子:

我们想要创建一个大小固定,可以存储不同类型元素的数组类,我们可以这样写:

#include<iostream>
using namespace std;
#define N 10template<class T>
class Array
{
private:int _a[N];	
};
int main()
{Array<int> arr1;Array<int> arr2;return 0;
}

这时实例化出来的arr1,arr2都是大小为10的定长数组。但我们假如说想改变一个数组的长度呢?例如我们想将数组长度改为20,怎么办呢?有同学说我们可以将N define为20呀。但是假如我们想要一个数组长为10,另一个长为20呢?

这时候我们的非类型模板参数就有作用了,我们可以将Array定义为下面的形式:

#include<iostream>
using namespace std;//这里的N叫做非类型模板参数,它是一个常量 
template<class T, size_t N>
class Array
{
private:int _a[N];
};
int main()
{Array<int, 10> arr1;Array<int, 20> arr2;int n = 10;Array<int, n> arr3;//error 这里非类型模板参数是常量,这里会报错return 0;
}

对于非类型模板参数的使用场景,STL中的deque和array容器中都用了。deque中的非类型模板参数用来传一个一个常量来控制buff的大小。

我们下面介绍一下array容器:

C++11支持array,它的结构类似于vector,只不过vector是动态数组,而array是静态的。它不提供头插,头插,尾插,尾删,任意位置插入删除这种操作,因为它根本不存在这种说法,而且它可以直接使用operator[ ] 访问修改任意位置的数据。

array的缺陷:

我们不推荐使用array,array底层是在栈上开辟空间的,而栈空间又是很有限的,例如:在32位的linux下栈空间只有8M,所以一般开大空间时极不推荐使用array,相比之下vector就很有优势。而且在知道要开多大空间的情况下,vector 也可以通过reserve一次性开好空间,在后续的使用中还可以自动增容,而array空间是固定的,不灵活。所以我们一般不使用array。

注意:

  • 非类型模板参数只允许使用整形家族(int,short,long,long long,char),而浮点型,类对象,字符串是不允许作为非类型模板参数的
  • 非类型模板参数需要在编译的时候就确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数

模板的特化

我们先举个例子,理解一下模板的特化,看下面的代码:

template<class T>
bool IsEqual(const T& left, const T& right)
{	return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;char p1[] = "hello";char p2[] = "world";cout << IsEqual(p1, p2) << endl;return 0; 
}

我们实现了IsEqual函数用来判断两个参数是否相等。我们总共调用了两次,第一次比较的是两个整数,是没问题的;而第二次实际上是有问题的,我们本意是想比较两个字符串是否相等,可是我们仔细看看:我们实际上比较的p1,p2两个指针。我们可能会想先判断一下传过来的参数的类型是什么,再进行不同的操作,就像下面这样:

template<class T>
bool IsEqual(const T& left, const T& right)
{if(T == char*)//error,C++中不支持类型比较{return *left==*right;}else{...}return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;//okchar* p1 = "hello";char* p2 = "world";cout << IsEqual(p1, p2) << endl;//errreturn 0; 
}

但是很可惜,C++中不支持类型比较,所以上面写的是错误的。

那么这里就需要我们的模板特化出手了,模板特化的目的就是在原模板类的基础上,针对某些类型进行特殊化处理。模板特化又分为函数模板特化和类模板特化。

函数模板的特化

  1. 首先必须有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对<>,尖括号内指定需要特化的类型
  4. 函数形参表必须要和函数模板的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误

我们上面的代码就可以改为:

#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}template<>
bool IsEqual<const char *&>(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}

说明:其实一般情况下,如果函数模板遇到不能处理或者处理有误的类型,可以直接将该函数直接给出,这叫做模板的匹配原则:有现成的完全匹配的,就直接调用,没有现成调用的,实例化模板生成。上面的代码就可以这样写:

#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}bool IsEqual(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}

对于模板匹配原则和函数模板特化,两者底层没有任何差别,如果能使用函数模板特化的时候更推荐使用函数模板特化。

类模板特化

1.全特化

就是将模板参数列表中所有参数确定化,例如:

#include<iostream>
using namespace std;template<class T1,class T2>
class A
{
public:A(){cout << "T" << endl;}
private:T1 a1;T2 a2;
};template<>
class A<int,int>
{
public:A(){cout << "int" << endl;}
private:
};int main()
{A<double, double>a1;A<int, int>a2;return 0;
}
2.偏特化

也叫半特化:即对模板参数进行一定的确定化,例如:

template<class T1>//第二个参数是int类型就会调用这个
class A<T1, int>
{
public:A(){cout << "T1,int" << endl;}
private:
};template<class T2>//第一个参数是int类型就会调用这个
class A<int, T2>
{
public:A(){cout << "int,T2" << endl;}
private:
};template<class T1,class T2>//两个参数都是指针类型就会调用这个
class A<T1*, T2*>
{
public:A(){cout << "T1*,T2*" << endl;}
private:
};template<class T1, class T2>//两个参数都是引用类型就会调用这个
class A<T1&, T2&>
{
public:A(){cout << "T1&,T2&" << endl;}
private:
};template<class T1, class T2>//第一个参数是指针类型,第二个参数是引用类型就会调用这个
class A<T1*, T2&>
{
public:A(){cout << "T1*,T2&" << endl;}
private:
};int main()
{A<double, int>a;A<int, char>b;A<double*, int*>c;A<char&, int&>d;A<double*, char&>e;return 0;
}

上面这几种特化版本都是偏特化。

偏特化并不仅仅是指特化,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,例如我们可以限制他的类型是引用,指针之类的。

关于特化的场景我们等到哈希表的时候会给大家介绍。

模板分离编译

我们在工作的时候写的项目都会由若干个源文件共同实现,每个源文件独立编译生成目标文件,最后将所有目标我呢见链接起来形成单一的可执行文件的过程称为分离编译模式

模板的分离编译

在前面我们模拟实现string,vector,list等STL容器的时候,都没有将声明和定义分开。我们实际上是习惯将声明放在头文件中,将定义放在.cpp文件中的。我们没有分离的原因就是C++的模板不支持分离编译。

我们以这段代码为例:

//Func.h代码:
#pragma once
#include<iostream>
using namespace std;void Print();template<class T>
void F(T a);//Func.cpp代码
#include"Func.h"void Print()
{cout << "print" << endl;
}template<class T>
void F(T a)
{cout << "F(T a)" << endl;
}//test.cpp代码
#include"Func.h"int main()
{Print();F(10);//这里编不过return 0;
}

不支持分离编译的原因:
我们都知道:程序的编译过程分为:预处理,编译,汇编,链接四个步骤

其中预处理会做的事情就是:头文件展开,宏替换,条件编译,去掉注释,在linux环境下就生成了.i文件

编译:检查语法错误后,生成汇编代码,在linux环境下生成.s文件

汇编:将汇编代码转成二进制机器码,在linux环境下生成.o文件

链接:会把.o文件里F或Print这样没有地址的地方,用被修饰过的函数名去Func.o中的符号表找对应的地址,然后填上地址就像上图中的地址一样。然后再把目标文件合并成可执行程序。

而问题就出现在链接的时候,编译器拿着我们的函数名去符号表中找时,能找到Print()函数,但是却找不到F()函数,这是因为在编译阶段,Print()函数有定义,可以生成。但是F()函数是一个函数模板,他不能生成,因为我们不知道T是什么类型。模板其实是调用时生成的。

如何解决

一、

这种方法实际上是不可行的,就是让编译器在编译的时候去各个地方查找实例化,例如我们在Func.i中看到一个模板,我们就去Test.i中找实例化,但是这样的话,如果是一个大项目的化,有几十几百个文件,对于编译器的实现就复杂了。所以实际在链接前,各文件间是不会交互的

二、

显示指定实例化,编译器看到后就知道要把这个模板参数T实例化成什么类型。但是这样做的问题也很大:就是我换个类型就又链接不上了,那我们就只能使用一种类型就实例化一次,这样就很麻烦。

三、

最后这种方法就很粗暴,STL中用的也是这种方法,就是不分离编译,声明和定义都放在一个.h文件里。这样的话,.h文件中就包含了模板的定义,就不需要链接的时候去查找了,在编译的阶段就能直接填地址了。而我们在项目中一般把这种文件命名为后缀为.hpp的文件,也就是说它即是.h文件,又是.cpp文件。

同样我们的类模板也不支持分离编译,最好的办法也是不分离,写在同一个文件中。

我在这里还要在解释我上文中的一句话:模板其实是调用时实例化生成的。我们在一个类模板或者函数模板中,假如写出了语法错误,假如我们没有实例化运行的话,编译器是检查不出错误的,这就是因为模板是调用时实例化的,没有实例化的时候,编译器不会去检查函数模板或类模板内部的语法错误。

总结

模板优点:

1.模板复用了代码,节约资源,更快的迭代开发,有了模板才有了C++的标准模板库STL的诞生

2.增强了代码的灵活性

模板缺点:

1.模板会导致代码膨胀问题,也会导致编译时间变长

2.假如我们使用模板时出现错误,编译器提示的错误信息会非常凌乱,而且准确度不高,我们不能盲目地相信模板的报错。可能只是一点小错误,最后却报出了一大堆错误,这时候我们要优先看第一个错误。

以上就是本章的全部内容,谢谢大家!

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



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

相关文章

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

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

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 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

如何在页面调用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