深入探索C++模板进阶:掌握非类型参数、特化技巧与分离编译的艺术

news2024/9/22 3:30:29

目录

非类型模板参数

类模板的特化

  概念

  函数模板特化

  类模板特化

        全特化

        偏特化

        类模板特化应用示例

模板的分离编译

  分离编译概念

  模板的分离编译

  解决方法

模板总结


非类型模板参数

        模板参数可分为类型形参和非类型形参。

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

类型形参(Type Parameters)

        类型形参允许你在定义模板时指定一个或多个类型占位符,这些占位符将在实例化模板时被实际的类型所替换。这为编写泛型代码提供了基础,使得单个模板定义可以应用于多种数据类型。类型形参通常跟在classtypename关键字之后,并赋予一个名称,这个名称在模板体内部代表一个未知但具体的类型。

 例如,在定义一个通用的容器类模板时,你可以这样使用类型形参:

template <typename T>
class Vector {
    T* data;
    size_t size;
public:
    Vector(size_t sz) 
    : size(sz)
    , data(new T[sz]) 
    {}
    // ... 其他成员函数
};

        在这个例子中,T就是一个类型形参,当你实例化Vector时,比如Vector<int>T就会被替换为int类型。 

非类型形参(Non-Type Parameters)

        非类型形参允许你在模板声明中使用除了类型之外的参数,最常见的形式是常量表达式(通常是编译时期可知的值)。这意味着你可以在模板中直接使用具体数值,而不仅仅是类型。非类型形参通常用于指定大小、维度等固定值,它必须是一个整数、枚举、指针或引用类型的常量表达式。

例如,定义一个计算阶乘的函数模板,其中阶乘的上限是一个非类型模板参数:

template <unsigned int N>
unsigned long long factorial() 
{
    if constexpr (N > 1)
        return N * factorial<N-1>();
    else
        return 1;
}

        在这个例子中,N是一个非类型形参,表示阶乘运算的上限。当你调用factorial<5>()时,N就被替换为5,实现计算5的阶乘。

注意:

  • 非类型模板参数只允许使用整型家族,浮点数、类对象以及字符串是不允许作为非类型模板参数的。
  • 非类型的模板参数在编译期就需要确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数。

模板的特化

  概念

        通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板。

// 函数模板 -- 参数匹配
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<>开始声明,后面跟上原模板的模板头,但这次要将模板参数替换为具体的类型或值。例如,如果你有一个接受两个类型参数的模板,全特化时需要明确指定这两个类型。

    template <>
    void myFunction(int a, double b) 
    {
        // 特化的实现
    }
  3. 实现特化逻辑:在特化声明之后,编写针对这些特定类型的实现代码。注意,这里的函数签名应与原模板匹配,只是参数类型或返回类型可能根据特化需求有所不同。

  4. 使用特化版本:在代码中,当调用函数模板并传递的参数匹配特化的类型时,编译器会自动选择特化版本。

代码示例:

//基础的函数模板
template<class T>
bool IsEqual(T x, T y)
{
	return x == y;
}

//对于char*类型的特化
template<>
bool IsEqual<char*>(char* x, char* y)
{
	return strcmp(x, y) == 0;
}

  类模板特化

        不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化和偏特化(半特化)。

        全特化

 类模板全特化步骤:

  1. 识别需求:确定你希望为类模板的哪一种具体类型或类型组合提供专门的实现。
  2. 声明全特化:使用template<>开始声明,后面紧跟着类模板的模板参数列表,但此时需将所有模板参数替换为具体的类型或值。例如,一个原本接受两个类型参数的类模板,全特化时需要明确指定这两个参数的具体类型。
    // 假设原类模板定义如下
    template <typename T, typename U>
    class MyClass 
    {
        // ...
    };
    
    // 全特化声明
    template <>
    class MyClass<int, double> 
    {
        // 特化的实现
    };
  3. 实现特化:在特化声明之后,提供这个特化版本的实现代码。根据特化类型的特点,你可能需要重写或添加成员函数,以及修改数据成员等,以满足特定类型的需要。
  4. 使用特化版本:在代码中实例化类模板时,如果使用的类型匹配全特化的类型组合,编译器将自动使用特化版本。

        偏特化

 类模板偏特化步骤:

  1. 分析模式:识别出模板参数中的某些参数或参数组合,你希望为这些特定模式提供不同的实现。偏特化允许你为模板参数的一个子集指定具体类型,而保留其他参数为泛型。
  2. 声明偏特化:使用template关键字开始声明,但只需特化部分模板参数,其他参数仍保持为模板参数。例如,如果原模板有两个参数,你可以特化第一个参数为特定类型,而保留第二个参数为泛型。
    // 假设原类模板定义如下
    template <typename T, typename U>
    class MyClass 
    {
        // ...
    };
    
    // 偏特化声明,特化T为int
    template <typename U>
    class MyClass<int, U> 
    {
        // 特化的实现,针对T为int的情况
    };
  3. 实现偏特化版本:提供偏特化版本的实现代码,这应该针对特化参数的特性进行优化或调整。
  4. 应用偏特化:当实例化类模板时,如果提供的类型参数匹配你的偏特化模式,编译器将自动选择对应的偏特化版本。

         那么如何证明各个类模板实例化使用的就是我们自己特化的类模板呢?

代码示例:

#include <iostream>
using namespace std;

template <typename T, typename U>
class MyClass
{
public:
    MyClass()
    {
        cout << "MyClass<T, U>" << endl;
    }
private:
    T _d1;
    U _d2;
};

// 全特化声明
template <>
class MyClass<int, double>
{
public:
    MyClass()
    {
        cout << "MyClass<int, double> (Full Specialization)" << endl;
    }
private:
    int _d1;
    double _d2;
};

// 偏特化声明,特化T为int
template <typename U>
class MyClass<int, U>
{
public:
    MyClass()
    {
        cout << "MyClass<int, U> (Partial Specialization)" << endl;
    }
private:
    int _d1;
    U _d2;
};

int main()
{
    // 实例化类并构造对象以观察输出
    MyClass<double, double> a;  // 应该输出 "MyClass<T, U>"
    MyClass<int, double> b;     // 应该输出 "MyClass<int, double> (Full Specialization)"
    MyClass<int, int> c;        // 应该输出 "MyClass<int, U> (Partial Specialization)"

    return 0;
}

代码结果:

类模板特化应用示例 

        假设我们要设计一个简单的日志记录器类,它可以处理不同类型的日志消息(如字符串、整数等)。首先,我们创建一个通用的日志记录器模板,然后针对字符串类型进行特化,提供一种更具体的处理方式,比如自动添加引号来包围字符串日志。

基础模板定义:

#include <iostream>

// 通用日志记录器模板
template <typename T>
class Logger 
{
public:
    void log(const T& message) 
    {
        std::cout << "Logging generic message: " << message << std::endl;
    }
};

//类模板特化:字符串日志

//对于字符串类型的消息,我们希望在输出时自动添加引号,以更清晰地标识这是一个文本日志项。因此,我们对Logger模板进行特化。


// 特化:针对std::string类型
template <>
class Logger<std::string> 
{
public:
    void log(const std::string& message) 
    {
        std::cout << "Logging string message: \"" << message << "\"" << std::endl;
    }
};

        现在,我们可以使用这个模板及其特化版本来记录不同类型的消息: 

int main() 
{
    Logger<int> intLogger;
    intLogger.log(123);  // 输出: Logging generic message: 123

    Logger<std::string> stringLogger;
    stringLogger.log("Hello, World!");  // 输出: Logging string message: "Hello, World!"
    
    return 0;
}

 

        在这个例子中,Logger<int>使用的是基础模板的实现,而Logger<std::string>则使用了特化的实现,自动为字符串添加了引号。

模板的分离编译

  分离编译概念

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

  模板的分离编译

        在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。

按照此方法,我们对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:

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

程序要运行起来一般要经历以下四个步骤:

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

以上代码在预处理阶段需要进行头文件的包含以及去注释操作:

这三个文件经过预处理后实际上就只有两个文件了,若是对应到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)没有模板函数的定义,无法进行实例化。

  解决方法

        解决类似于上述模板分离编译失败的方法有两个。

第一个方法:就是在模板定义的位置进行显示实例化。

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

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

模板总结

优点:

  1. 代码重用:模板允许你编写一次代码,应用于多种数据类型,极大地减少了代码重复,并提高了维护效率。
  2. 类型安全:相比于使用泛型的编程语言中的类型擦除机制,C++模板在编译时生成具体类型的代码,保证了类型安全,减少了运行时错误。
  3. 性能:模板代码在编译时实例化,意味着没有运行时的类型检查或转换开销,通常能获得与手写特定类型代码相近的性能。
  4. 灵活性:模板特化允许为特定类型定制行为,使得库或框架能够适应更广泛的需求。
  5. 泛型编程:促进了泛型编程范式的应用,使得算法和数据结构更加抽象和通用。

缺点:

  1. 编译时间:模板尤其是深度嵌套或大量特化的模板会显著增加编译时间和编译复杂度,影响开发效率。
  2. 二进制体积:每个被实例化的模板都会生成对应的代码,对于大型项目或大量类型实例化的情况,可能会导致可执行文件体积增大。
  3. 调试难度:模板错误通常以编译错误的形式出现,且错误信息可能冗长难懂,特别是涉及复杂模板元编程时,调试和理解问题的根源可能较为困难。
  4. 学习曲线:理解和有效使用模板,特别是高级特性如模板元编程,对开发者有较高的要求,初学者可能感到门槛较高。
  5. 过度设计风险:有时过度依赖模板和泛型编程可能导致代码结构过于抽象,降低代码的可读性和可维护性。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为云之Zabbix监控平台部署实践

华为云之Zabbix监控平台部署实践 一、本次实践介绍1.1 实践环境简介1.3 本次实践完成目标 二、 相关服务介绍2.1 华为云ECS云服务器介绍2.2 Zabbix介绍 三、环境准备工作3.1 预置实验环境3.2 查看预置环境信息 四、登录华为云4.1 登录华为云4.2 查看ECS状态4.3 连接ECS弹性云服…

CMake-gui中选择vs编译器没有指定版本解决办法

CMake-gui中选择vs编译器&#xff0c;但是没有指定版本怎么办&#xff1f; 正文 cMake-gui中选择vs编译器时&#xff0c;有Visual Studio 2008、2013、2015、2017、2019&#xff0c;但就是没有的2022的版本&#xff0c;如图&#xff1a; 因为小编目前使用的Visual Studio版…

由于找不到d3dx9_39.dll,无法继续执行代码的5种解决方法

在现代科技发展的时代&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;由于各种原因&#xff0c;我们可能会遇到一些电脑问题&#xff0c;其中之一就是“d3dx9_39.dll丢失”。这个问题可能会导致我们在运行某些游戏或应用程序时遇到错误提示&#xff0c;…

机器学习入门指南:Jupyter Notebook实战

前言 机器学习作为人工智能领域的核心组成&#xff0c;是计算机程序学习数据经验以优化自身算法、并产生相应的“智能化的“建议与决策的过程。随着大数据和 AI 的发展&#xff0c;越来越多的场景需要 AI 手段解决现实世界中的真实问题&#xff0c;并产生我们所需要的价值。 机…

eDNA热门应用案例一网打尽

一份环境样品中&#xff0c;可包含的DNA片段能达到上千万个&#xff0c;而每一个物种能携带的DNA又是独一无二的。将从样品中获得的遗传标记序列与DNA数据库中的序列相比对&#xff0c;从而确认分析的DNA来自什么物种&#xff0c;这就是环境DNA宏条形码技术&#xff08;eDNA Me…

拼多多商品详情商品标题sku等信息抓取接口API调用步骤演示

接口名称&#xff1a;item_get_app_pro 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,item_get,item_search_sho…

低代码的原理、发展历史、使用场景和优势。

在数字化转型的浪潮中&#xff0c;低代码开发平台&#xff08;YDUIbuilder&#xff09;以其独特的优势迅速崛起&#xff0c;为各行各业带来了创新的解决方案。本文将深入探讨低代码的原理、发展历史、使用场景以及它所带来的优势。 gitee下载&#xff1a;yduibuilder: 快速开发…

Spring AOP失效的场景事务失效的场景

场景一&#xff1a;使用this调用被增强的方法 下面是一个类里面的一个增强方法 Service public class MyService implements CommandLineRunner {private MyService myService;public void performTask(int x) {System.out.println("Executing performTask method&quo…

LeetCode题练习与总结:将有序数组转换为二叉搜索树--108

一、题目描述 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵平衡二叉搜索树。 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#xff1a;[0,-10,5,null,-3,null,9] 也将…

流程引擎之compileflow idea 2024.*插件支持

之前有使用过多种类型工作流&#xff0c;但最近研究工作流引擎对比各有优劣&#xff0c;compileflow内存支持性能不错&#xff0c;但在idea新版本使用的时候发现插件不支持&#xff0c;干脆自己修改源码手撸一个&#xff08;当前版本2024.1验证可用&#xff0c;如果有其他版本不…

Vue框架动态引入省份个性化代码

项目需求有产品的功能&#xff0c;但是功能下部分小功能每个省份有不同的控制&#xff0c;所以需要引入省份个性化代码。 思路是&#xff0c;页面一开始加载产品化的代码&#xff0c;有个性化的代码就加载个性化的逻辑&#xff0c;个性化代码是产品化代码的重写&#xff0c;所…

QT如何将生成的exe文件打包成安装包

一、生成exe文件 1、生成exe文件 QT编译模式选择release&#xff0c;然后点击编译&#xff1a; 2、找到exe文件 在开发文件夹下找到build-xxxxxxx-Release文件夹里面找到exe文件 3、相关依赖dll文件查找 新建个空文件夹将exe文件拷贝进去&#xff1a; 然后找到如下程序…

数据结构-二叉树系统性学习(四万字精讲拿捏)

前言 这里我会从二叉树的概念开始讲解&#xff0c;其次涉及到概念结构&#xff0c;以及堆的实现和堆排序。 目的是&#xff0c;堆比二叉树简单&#xff0c;同时堆本质上是二叉树的其中一种情况&#xff0c;堆属于二叉树顺序结构的实现 最后完善二叉树的讲解&#xff0c;也就是…

SpringBoot 结合 WebSocket 实现聊天功能

目录 一、WebSocket 介绍 二、源码 2.1 pom.xml 2.2 WebSocket配置类&#xff0c;用于配置WebSocket的相关设置 2.3 自定义WebSocket处理器类&#xff0c;用于处理WebSocket的生命周期事件 2.4 自定义WebSocket握手拦截器&#xff0c;用于增强WebSocket的握手过程 2.5 Ses…

网络编程-TCP

一、TCP的相关IP 1.1 SeverSocket 这是Socket类,对应到网卡,但是这个类只能给服务器使用. 1.2 Socket 对应到网卡,既可以给服务器使用,又可以给客户端使用. TCP是面向字节流的,传输的基本单位是字节. TCP是有连接的,和打电话一样,需要客户端拨号,服务器来听. 服务器的内核…

夏日将至,给手机装个“液冷”降温可行吗?

夏天出门在外&#xff0c;手机总是更容易发热&#xff0c;尤其是顶着大太阳用手机的时候&#xff0c;更是考验手机的散热能力。如果你也是一个对手机体验有追求的人&#xff0c;比较在意手机的温度&#xff0c;那么可以考虑入手一个微泵液冷手机壳。 【什么是微泵液冷壳&#…

mybatisplus填充公共字段MetaObjectHandler后不生效解决方式

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component;import java.util.Date;/*** 拦截处理公共字…

高清短视频:四川京之华锦信息技术公司

高清短视频&#xff1a;视觉盛宴与时代的脉搏 在数字化时代&#xff0c;短视频已成为人们生活中不可或缺的一部分。其中&#xff0c;高清短视频以其卓越的画质和精彩的内容&#xff0c;成为了人们追求视觉享受的首选。四川京之华锦信息技术公司将从高清短视频的定义、特点、影…

简单微信企业群消息推送接口

群管理 群发送接口 POST: JSONURL http://localhost:65029/m/wxapi/sendwxmsg{ "nr":"试", --消息 "at":"wxid_y0k4dv0xcav622,wxid_y0k4dv0xcav622",--群wxid "key":"F98F354F1671A2D21BC78C76B95E96EB",--群k…