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

目录

非类型模板参数

类模板的特化

  概念

  函数模板特化

  类模板特化

        全特化

        偏特化

        类模板特化应用示例

模板的分离编译

  分离编译概念

  模板的分离编译

  解决方法

模板总结


非类型模板参数

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

        类型形参: 出现在模板参数列表中,跟在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. 过度设计风险:有时过度依赖模板和泛型编程可能导致代码结构过于抽象,降低代码的可读性和可维护性。

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

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

相关文章

韬光养晦的超绝项目

发展方向 竞技闯关类 可以加入对战系统积累积分&#xff0c;竞技类的接受程度更高&#xff0c;小孩&#xff08;我和我身边大多数人小时候&#xff09;都喜欢玩王者吃鸡这种经济类游戏&#xff0c;开放世界探索&#xff08;本项目、一梦江湖、逆水寒&#xff09;的受众群体年…

8. C++通过epoll+fork的方式实现高性能网络服务器

epollfork 实现高性能网络服务器 一般在服务器上&#xff0c;CPU是多核的&#xff0c;上述epoll实现方式只使用了其中的一个核&#xff0c;造成了资源的大量浪费。因此我们可以将epoll和fork结合来实现更高性能的网络服务器。 创建子进程函数–fork( ) 要了解线程我们先来了解…

Linux下Qt Creator无法输入中文(已解决)

1. 首先确保安装了搜狗输入法&#xff0c;且能正常运行。 2.克隆源码到本地。 git clone https://gitcode.com/fcitx/fcitx-qt5.git 3.检查Qt Creator版本&#xff0c;如下图所示&#xff0c;为基于Qt6的。 4. 进入源码目录&#xff0c;建立build文件夹&#xff0c;修改CMak…

COD20使命召唤20新赛季免费玩 COD20免费体验在哪下

使命召唤20&#xff08;COD20&#xff09;的免费周已经正式启动&#xff0c;这是一个为期一周的特别活动&#xff0c;为玩家们带来了前所未有的游戏体验。在这个特殊的周期里&#xff0c;多人模式和僵尸模式将向公众免费开放&#xff0c;玩家们可以尽情地探索和体验游戏的精彩内…

2022全国大学生数学建模竞赛ABC题(论文+代码)

文章目录 &#xff08;1&#xff09;2022A波浪能最大输出功率&#xff08;2&#xff09;2022B无人机定位&#xff08;3&#xff09;2022C古代玻璃制品成分分析&#xff08;4&#xff09;论文和代码链接 &#xff08;1&#xff09;2022A波浪能最大输出功率 &#xff08;2&#x…

3D开发工具HOOPS在BIM系统中的应用

建筑信息模型是一种革命性的建筑设计、施工和管理方法。它通过创建和利用数字信息来优化建筑项目的设计、施工和运营过程。在这个过程中&#xff0c;3D开发工具HOOPS扮演着至关重要的角色&#xff0c;为BIM系统提供了强大的技术支持和丰富的功能。HOOPS中文网http://techsoft3d…

Linux如何在目录下灵活创建、浏览、删除百万个文件

文章目录 一、创建百万级小文件1、单核CPU情况2、多核CPU情况3、执行效率对比3.1、单核的顺序执行3.2、多核的并发执行 二、如何列出/浏览这些文件1、查看目录下文件的数量2、列出&#xff1f;3、ls -f&#xff08;关闭排序功能&#xff09;3.1、执行效率对比 4、通过重定向导入…

探索Python函数参数的奥秘

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、揭开函数参数的神秘面纱 1. 位置参数&#xff1a;按序传值的基石 2. 关键字参数&#…

作业-day-240527

Cday1思维导图 定义自己的命名空间my_sapce&#xff0c;在my_sapce中定义string类型的变量s1&#xff0c;再定义一个函数完成对字符串的逆置 #include <iostream>using namespace std;namespace my_space {string s1"abc123";string recover(string s){int i0…

C语言基础——数组

{\▁/} ( / 。\ ) / ⊃&#x1f494;\⊃ 为什么我那么努力还是得不到那么多赞 ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言基础&#xff1b; 文章目录 前言…

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…

【软件测试】bug篇|软件测试的生命周期|描述bug的要素|bug的级别|bug的生命周期|高频面试题:与开发产⽣争执怎么处理

目录 一、软件测试的⽣命周期 二、BUG 2.1 bug的概念 2.2 描述bug的要素 2.3 bug级别 2.4 bug的⽣命周期 &#x1f4a1;2.5 与开发产⽣争执怎么办&#xff08;⾼频考题&#xff09; &#x1f4a1; 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&…

java对象的比较

一.PriorityQueue中插入对象 优先级队列在插入元素时有个要求&#xff1a;插入的元素不能是null或者元素之间必须要能够进行比较&#xff0c;那优先级队列中能否插入自定义类型对象呢&#xff1f; 堆中插入元素时&#xff0c;必须要进行元素的比较&#xff0c;而此时Card是没有…

Java入门-java的集合框架

集合概念 集合&#xff0c;有时也称作容器(Container), 是对象的持有者&#xff0c;它们可以有助于高效访问的方式存储的组织对象。以生活中的案例为例&#xff1a; 集合就像装衣服的柜子&#xff0c;衣服就是集合中的元素。 集合框架图 Collection中每次操作的都是一个对象&a…

智慧林业云巡平台 客户端和移动端(支持语音和视频)自动定位巡护,后端离线路线监测

目前现状 无法客观、方便地掌握护林员的到位情况&#xff0c;因而无法有效地保证巡护人员按计划要求&#xff0c;按时按周期对所负责的林区开展巡护&#xff0c;使巡护工作的质量得不到保证。遇到火情、乱砍滥伐等灾情时无法及时上报处理&#xff0c;现场状况、位置等信息描述…

pycharm打开服务器(linux)上的项目

先在本地打开项目 一、项目文件配置 tools-deployment-configuration 新增一个sftp连接 测试服务器是否可以连通 mappings中设置本地路径和服务器上的路径 二、环境配置 先参考文章 复现论文的conda环境&#xff08;win和联网、离线linux&#xff09;_conda复现环境-CSDN博…

Qt pro工程文件编写汇总(区分debug和release、32位和64位的方法,编译输出目录等)

前言&#xff1a; 从事qt开发已经好几年了&#xff0c;但有关pro编写的一些细节问题一直没有一个很好的梳理汇总——因为实际工作开发中&#xff0c;往往只需要编译特定版本的软件&#xff08;例如32位release版本&#xff09;&#xff0c;项目创建好后并设置好编译路径&#x…

美团Java社招面试题真题,最新面试题

如何处理Java中的内存泄露&#xff1f; 1、识别泄露&#xff1a; 使用内存分析工具&#xff08;如Eclipse Memory Analyzer Tool、VisualVM&#xff09;来识别内存泄露的源头。 2、代码审查&#xff1a; 定期进行代码审查&#xff0c;关注静态集合类属性和监听器注册等常见内…

逻辑分析仪的调试使用

调试软件下载&#xff1a;点击跳转 一、接线 逻辑分析仪 设备 GND --- GND CHX&#xff08;数据通道&#xff09; --- 通信引脚 二、数据采集 图中两个可以选择数字大小的地方分别表示 采样深度 &#xff08;10M Samples&a…

Swift 类和结构体

类和结构体 一、结构体和类对比1、类型定义的语法2、结构体和类的实例3、属性访问4、结构体类型的成员逐一构造器 二、结构体和枚举是值类型三、类是引用类型1、恒等运算符2、指针 结构体和类作为一种通用而又灵活的结构&#xff0c;成为了人们构建代码的基础。你可以使用定义常…