C++模板——非类型模板参数、模板的特化以及模板的分离编译

目录

非类型模板参数

模板的特化

概念

函数模板特化

类模板特化

全特化

偏特化

模板的分离编译

什么是分离编译

模板的分离编译

解决方法

模板总结


非类型模板参数

模板参数可分为类型形参和非类型形参。
类型形参: 出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。
非类型形参: 用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

例如,我们要实现一个静态数组的类,就需要用到非类型模板参数。

#include <iostream>

template <class T, std::size_t N>
class StaticArray {
public:
    // 获取数组大小
    constexpr std::size_t arraysize() const {
        return N;
    }

    // 获取数组中的元素(带边界检查)
    T& operator[](std::size_t index) {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return _array[index];
    }

    // 获取数组中的元素(常量版本,带边界检查)
    const T& operator[](std::size_t index) const {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return _array[index];
    }

    // 填充数组中的所有元素
    void fill(const T& value) {
        for (std::size_t i = 0; i < N; ++i) {
            _array[i] = value;
        }
    }

    // 打印数组内容
    void print() const {
        for (std::size_t i = 0; i < N; ++i) {
            std::cout << _array[i] << " ";
        }
        std::cout << std::endl;
    }

private:
    T _array[N]; // 利用非类型模板参数指定静态数组的大小
};

int main() {
    StaticArray<int, 5> arr;

    // 填充数组
    arr.fill(10);

    // 打印数组内容
    arr.print();

    // 访问和修改数组元素
    arr[2] = 20;
    arr.print();

    // 获取数组大小
    std::cout << "Array size: " << arr.arraysize() << std::endl;

    // 尝试访问越界元素
    try {
        std::cout << arr[5] << std::endl; // 这将抛出异常
    } catch (const std::out_of_range& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}
  1. 模板声明

    • template <class T, std::size_t N>:这个类模板接受两个参数:一个类型参数T和一个非类型模板参数N,表示数组的大小。
  2. 成员函数

    • constexpr std::size_t arraysize() const:返回数组的大小,使用constexpr保证在编译时常量。
    • T& operator[](std::size_t index)const T& operator[](std::size_t index) const:重载的索引运算符用于访问数组元素,包含边界检查以防止越界访问。
    • void fill(const T& value):填充数组的所有元素为指定的值。
    • void print() const:打印数组内容。
  3. 数据成员

    • T _array[N]:声明一个静态数组作为类的成员,大小为模板参数N
  4. main函数

    • 创建一个StaticArray<int, 5>对象。
    • 使用fill方法填充数组,打印数组内容。
    • 修改数组元素并打印内容。
    • 获取并打印数组大小。
    • 尝试访问越界元素,捕获并处理异常。

运行结果

 

这样,静态数组类不仅能够存储和管理固定大小的数组,还能提供便捷的方法进行访问和修改,同时具有边界检查功能来提高安全性。

模板的特化

概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
 return left < right;
}
int main()
{
 cout << Less(1, 2) << endl; // 可以比较,结果正确
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 8);
 cout << Less(d1, d2) << endl; // 可以比较,结果正确
 Date* p1 = &d1;
 Date* p2 = &d2;
 cout << Less(p1, p2) << endl; // 可以比较,结果错误

 return 0
}
可以看到,Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指 针的地址,这就无法达到预期而错误。
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化类模板特化。

函数模板特化

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

 

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
 return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
 return *left < *right;
}
int main()
{
 cout << Less(1, 2) << endl;
 Date d1(2022, 7, 7);
 Date d2(2022, 7, 8);
 cout << Less(d1, d2) << endl;
 Date* p1 = &d1;
 Date* p2 = &d2;
 cout << Less(p1, p2) << endl; // 调用特化之后的版本,而不走模板生成了
 return 0;
}

注意: 一般情况下,如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。例如,上述实例char*类型的特化还可以这样给出:

bool Less(Date* left, Date* right)
{
 return *left < *right;
}

类模板特化

类模板特化允许为特定的类型提供自定义的实现。特化可以分为完全特化和偏特化。 

全特化

全特化即是将模板参数列表中所有的参数都确定化。 

#include <iostream>
using namespace std;

// 通用模板
template <typename T> // 声明一个类模板
class Printer {
public:
    void print(const T& value) { // 声明一个名为 print 的公共成员函数,参数是一个常量引用类型 T
        cout << "Generic Printer: " << value << endl; 
    }
};

// 完全特化版本,用于 const char*
template <> // 声明一个完全特化版本,不需要类型参数
class Printer<const char*> { // 特化 Printer 类模板用于 const char* 类型
public:
    void print(const char* value) { // 声明一个名为 print 的公共成员函数,参数是一个 const char* 类型
        cout << "Specialized Printer for const char*: " << value << endl;
    }
};

int main() {
    Printer<int> intPrinter; // 声明一个 Printer<int> 类型的对象 intPrinter,使用通用模板
    intPrinter.print(123);  // 

    Printer<const char*> stringPrinter; // 声明一个 Printer<const char*> 类型的对象 stringPrinter,使用完全特化版本
    stringPrinter.print("Hello, world!");  // 调用 stringPrinter 对象的 print 成员函数,输出 "Specialized Printer for const char*: Hello, world!"

    return 0; 
}

全特化的特点

  1. 全特化的声明
    • template <>: 声明一个完全特化版本。不需要类型参数,因为这是为特定类型(const char*)提供的实现。
  2. 全特化的定义
    • class Printer<const char*>: 为 Printer 类模板定义一个专门用于 const char* 类型的特化版本。
  3. 全特化的用途
    • 对于特定类型(如 const char*),我们可以提供不同于通用模板的实现,以满足特定需求或优化性能。
    • 在这个例子中,完全特化版本的 print 函数处理 const char* 类型的字符串,并输出特定的信息。

通用模板和全特化的对比

  • 通用模板:可以处理任何类型 T,提供了一个通用的实现。适用于大多数情况。
  • 完全特化:针对特定类型 const char* 提供了一个专门的实现。这种实现可以与通用模板不同,用于满足特定的需求或优化。

偏特化

偏特化是指任何针对模板参数进一步进行条件限制设计的特化版本。 

#include <iostream>
using namespace std;

// 通用模板
template <typename T, typename U>
class Pair {
public:
    void print() {
        cout << "Generic Pair" << endl;
    }
};

// 偏特化版本,当两个参数类型相同时
template <typename T>
class Pair<T, T> {
public:
    void print() {
        cout << "Specialized Pair with same types" << endl;
    }
};

int main() {
    Pair<int, double> p1; // 声明一个 Pair<int, double> 类型的对象 p1,使用通用模板
    p1.print();  // 调用 p1 对象的 print 成员函数,输出 "Generic Pair"

    Pair<int, int> p2; // 声明一个 Pair<int, int> 类型的对象 p2,使用偏特化版本
    p2.print();  // 调用 p2 对象的 print 成员函数,输出 "Specialized Pair with same types"

    return 0; 
}
  1. 偏特化的声明

    • 在模板参数列表中使用了相同的模板参数 T,表示只有当两个参数类型相同时才会触发偏特化。
  2. 偏特化的定义

    • template <typename T>: 声明一个类模板,T 是一个模板参数,占位符类型。
    • class Pair<T, T>: 声明一个偏特化版本,当两个模板参数的类型相同时触发。
  3. 偏特化的用途

    • 当模板参数满足特定条件时,我们可以提供一个不同于通用模板的特化实现。
    • 在这个示例中,偏特化版本的 print 函数处理两个参数类型相同的情况,并输出特定的信息。

通用模板和偏特化的对比

  • 通用模板:提供了一个通用的实现,适用于大多数情况。
  • 偏特化:针对特定模板参数满足特定条件时,提供了一个特定的实现。这种实现可以与通用模板不同,用于满足特定的需求或优化。

偏特化可以在满足特定条件的情况下提供更特定的实现,从而使模板更加灵活和适用于各种不同的情况。

模板的分离编译

什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。

模板的分离编译

在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:

但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。

下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:

预处理: 头文件展开、去注释、宏替换、条件编译等。
编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言
汇编:
把编译阶段生成的文件转成目标文件。
链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作。

这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了。

 

预处理后就需要进行编译,虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件。

之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件。

前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数。

模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。

解决方法

解决类似于上述模板分离编译失败的方法有两个,第一个就是在模板定义的位置进行显示实例化。
例如,对于上述代码解决方案如下:

在函数模板定义的地方,对T为int和double类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。

虽然第一种方法能够解决模板分离编译失败的问题,但是我们这里并不推荐这种方法,因为我们需要用到一个函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。

现在就来说说解决该问题的第二个方法,也是我们所推荐的,那就是对于模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。

模板总结

优点:

  1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
  2. 增强了代码的灵活性。

缺陷:

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/654030.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【python】删除一个列表中的所有的1

删除所有的1 x [1, 1, 6, 3, 9, 4, 5, 1, 1, 2, 1, 9, 6, 4] 使用lambda函数和filter来过滤掉x中的1 filtered_x list(filter(lambda n: n ! 1, x)) 不是1的数字&#xff0c;存进x列表&#xff0c;filter用于插入元素到第二个位置 print(filtered_x) # 输出: [6, 3, 9, …

第13章 层次式架构设计理论与实践

层次式架构的核心思想是将系统组成为一种层次结构&#xff0c;每一层为上层服务&#xff0c;并作为下层客户。其实不管是分层还是其他的架构都是为了解耦&#xff0c;更好的复用&#xff0c;只要秉承着这种思想去理解一切都迎刃而解了。 13.1 层次上体系结构概述 回顾一下软件…

【docker】安装harbor出现问题: Running 1/1 ✘ Network harbor_harbor Error

安装harbor出现问题&#xff1a; [] Running 1/1 ✘ Network harbor_harbor Error 0.2s failed to create network harbor_harbor: Error response from daemon: Fa…

节水“云”科普丨北京昌平VR节水云展馆精彩上线

2024年5月15日上午&#xff0c;由北京昌平区水务局主办的“推进城市节水&#xff0c;建设美丽昌平——2024年全国城市节约用水宣传周暨‘坚持节水优先 树立节水标杆’昌平节水在行动主题实践活动”隆重举办&#xff0c;活动期间&#xff0c;昌平区水务局应用VR虚拟现实技术创新…

目标检测数据集 - 工地工人安全设备佩戴检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;工地工人安全设备佩戴检测数据集&#xff0c;真实场景数据生成增强后高质量图片数据&#xff0c;涉及场景丰富&#xff0c;比如楼宇建筑工地工人作业数据、道路建筑工地工人作业数据、室内工地工人作业数据、露天挖掘场景工人作业数据、工地工人自拍摆拍…

【数据分析】Numpy和Pandas库基本用法及实例--基于Japyter notebook实现

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 承接上篇的博客 数据分析—技术栈和开发环境搭…

使用Java 读取PDF表格数据并保存到TXT或Excel

目录 导入相关Java库 Java读取PDF表格数据并保存到TXT Java读取PDF表格数据并保存到Excel 在日常工作中&#xff0c;我们经常需要处理来自各种来源的数据。其中&#xff0c;PDF 文件是常见的数据来源之一。这类文件通常包含丰富的信息&#xff0c;其中可能包含重要的表格数据…

大数据面试题 —— Hive

目录 Hive 是什么为什么要使用 HiveHive 的优缺点Hive的实现逻辑&#xff0c;为什么处理小表延迟比较高你可以说一下 HQL 转换为 MR 的任务流程吗 ***你可以说一下 hive 的元数据保存在哪里吗 ***Hive与传统数据库之间的区别Hive内部表和外部表的区别 ***hive 动态分区与静态分…

28 Debian如何配置PXE网络装机(全自动无人值守)

作者:网络傅老师 特别提示:未经作者允许,不得转载任何内容。违者必究! Debian如何配置PXE网络装机(全自动无人值守) 《傅老师Debian小知识库系列之28》——原创 ==前言== 傅老师Debian小知识库特点: 1、最小化拆解Debian实用技能; 2、所有操作在VMware虚拟机实测完成…

Rocky Linux 9.4 正式版发布 - RHEL 100% 1:1 兼容免费发行版

Rocky Linux 9.4 正式版发布 - RHEL 100% 1:1 兼容免费发行版 Rocky Linux 由 CentOS 项目的创始人 Gregory Kurtzer 领导 请访问原文链接&#xff1a;Rocky Linux 9.4 正式版发布 - RHEL 100% 1:1 兼容免费发行版&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处…

登录记住密码背景颜色修改

1&#xff0c;在login.vue中&:-webkit-autofill里面的css替换成如下 &:-webkit-autofill {box-shadow: 0 0 0px 1000px $bg inset !important;-webkit-text-fill-color: $cursor !important;}

Postman实现批量发送json请求

最近有一个场景&#xff0c;需要本地批量调用某个接口&#xff0c;从文件中读取每次请求的请求体&#xff0c;实现方法记录一下。 1.读取请求体 在 Postman 中&#xff0c;如果你想在 Pre-request Script 阶段读取文件内容&#xff0c;比如为了将文件内容作为请求的一部分发送…

易查分小程序 学生成绩管理小程序

亲爱的老师们&#xff0c;是不是每次成绩公布后&#xff0c;家长们的连环夺命call让你头大&#xff1f;担心孩子们的成绩信息安全&#xff0c;又想快速分享给家长&#xff0c;这可咋整&#xff1f;别急&#xff0c;易查分小程序来帮忙啦&#xff01; 安全有保障 智能验证码&a…

老板:2个亿的销售额,利润只有55万!电商这个生意真的到头了?

近来&#xff0c;一段对话轰动了半个电商圈的老板&#xff0c;干拼多多&#xff0c;2亿的销售额&#xff0c;利润只有55万&#xff01; 其实造成这一现象的原因就是“内卷” 说一句电商行业真实的现状&#xff0c;电商发展了十几年&#xff0c;网友对网购已经完全熟悉&#x…

airtest做web端UI自动化实战

安装 官网下载客户端 airtest库安装 pip install airtest pip install pocoui脚本录制 利用airtest客户端录制脚本 web端辅助插件-selenium windows打开: 设置chrome路径 开始调式录制 脚本运行 # -*- coding: utf-8 -*- """ Time &#xff1a; 2024/5/…

C/C++连接MySQL

本章Gitee仓库地址&#xff1a;mysql连接基本操作 文章目录 1. mysql connect库2. mysql相关接口2.1 mysql_init()2.2 mysql_real_connect()2.3 mysql_query()2.4 mysql_store_result()2.41 mysql_num_rows2.42 mysql_num_fields2.43 mysql_fetch_row2.44 mysql_fetch_fields 2…

openLayers加载wms图层并定位到该图层

openLayers定位到wms图层 我们的wms是加载geoserver发布的服务&#xff0c;wms加载的图层是没法通过layer.getSource().getExtent()来获取到extents&#xff08;边界&#xff09;的&#xff1b;实现思路是通过postgis的函数(st_extent(geom))来获取extents; 返回前端后格式化一…

WWW24因果论文(3/8) |通过因果干预实现图分布外泛化

【摘要】由于图神经网络 (GNN) 通常会随着分布变化而出现性能下降&#xff0c;因此分布外 (OOD) 泛化在图学习中引起了越来越多的关注。挑战在于&#xff0c;图上的分布变化涉及节点之间错综复杂的互连&#xff0c;并且数据中通常不存在环境标签。在本文中&#xff0c;我们采用…

C#电子名片(vCard)

目录 1.介绍 2.基本定义 3.字段信息 4&#xff0c;字段详解。 4.1&#xff0c;预定义类型的用法 4.2&#xff0c;基本类型 4.3&#xff0c;通讯地址类型 4.4&#xff0c;电信通信类型 4.5&#xff0c;地理类型 4.6&#xff0c;解释类型 5&#xff0c;应用。 6&…

Rviz 复选框插件

Rviz 复选框插件 0.引言1.实现效果 0.引言 参考1参考2参考3参考4 我想做的插件是类似于 pangolin 侧面的复选框&#xff0c;动态传递 bool 值给程序内部使用。查了一下只能是通过插件的方式进行实现。但是Display 的参数在编译阶段就写死了&#xff0c;我想要在运行期给定参数…