当前位置: 首页 > ds >正文

C++模板进阶

目录

一、非类型模板参数

1. 什么是非类型模板参数?

2. 它能干什么?

二、模板的特化

1. 为什么需要模板特化?

2. 函数模板特化

3. 类模板特化

3.1 全特化

3.2 偏特化

3.3 类模板特化应用实例:解决排序问题

三、模板的分离编译

1. 分离编译的困惑

2. 问题的根源

3. 解决方案

方法一:显示实例化

方法二:把模板定义放在头文件中

四、总结:模板的优缺点与应用场景

优点

缺点

应用场景


一、非类型模板参数

1. 什么是非类型模板参数?

        非类型模板参数,顾名思义,就是模板参数中不是类型的部分。它允许我们用一个常量作为类或函数模板的参数,在模板中把这个参数当作常量来使用。就好比给模板增加了一个特殊的“标签”,直接告诉编译器一些确定的信息。

2. 它能干什么?

        举个简单的例子,我们要实现一个静态数组的类。静态数组的大小在编译时就必须确定,这时候非类型模板参数就派上大用场了。

template<typename T, size_t N>
class StaticArray 
{
public:size_t arraysize() {return N;}private:T _array[N];
};

        在上面这段代码中,N 就是一个非类型模板参数。它直接决定了 _array 这个静态数组的大小。当我们实例化对象时,就可以指定这个大小了。

int main() 
{StaticArray<int, 10> a1;std::cout << a1.arraysize() << std::endl; // 输出 10StaticArray<int, 100> a2;std::cout << a2.arraysize() << std::endl; // 输出 100return 0;
}

        是不是很方便?而且,非类型模板参数不仅限于整数类型,像枚举类型等也是可以的。不过,浮点数、类对象以及字符串等就不行了。因为编译器在编译阶段就得确定非类型模板参数的值,而这些类型在编译时很难直接确定。

二、模板的特化

1. 为什么需要模板特化?

        模板确实很强大,能让我们写出通用的代码。但有些时候,对于某些特殊类型,通用的模板实现可能并不能达到我们想要的效果,甚至可能会出错。这时候,模板特化就成为了我们的“救星”。

2. 函数模板特化

先来看看函数模板特化的例子。假设我们有一个用于比较两个相同类型数据是否相等的函数模板。

template<typename T>
bool IsEqual(T x, T y) 
{return x == y;
}

这个函数模板在大多数情况下都能正常工作。但如果传入的是字符数组呢?

char a1[] = "hello";
char a2[] = "hello";
std::cout << IsEqual(a1, a2) << std::endl; // 输出 0

结果和我们期望的不一样。因为这个函数模板比较的是两个字符数组的地址,而不是它们的内容。那怎么办?我们可以对 char* 类型进行特化。

函数模板的特化步骤:

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

这样,当我们比较字符数组时,就会调用这个特化版本的函数,得到正确的结果。

3. 类模板特化

类模板也可以进行特化,分为全特化偏特化

3.1 全特化

全特化就是把模板参数列表中所有的参数都确定下来。比如,我们有一个类模板:

template<typename T1, typename T2>
class apple
{
public:apple() {std::cout << "apple<T1, T2>" << std::endl;}
};

现在,我们想要对 T1doubleT2int 的情况进行特殊处理。

template<>
class apple<double, int> 
{
public:apple() {std::cout << "apple<double, int>" << std::endl;}
};

这样,当我们实例化 apple<double, int> 时,就会调用这个特化版本的类模板。

3.2 偏特化

        偏特化是类模板特化的“升级版”,它允许我们针对模板参数设置特定条件,当这些条件满足时,就使用特化版本的类模板。

偏特化的两种表现方式

🌴部分特化

        部分特化就是只对模板参数表中的部分参数进行特化,就像给模板参数设置了一部分“专属规则”。比如,对于一个有两个模板参数的类模板 Data,我们把第二个参数特化为 int

template <class T1>
class Data<T1, int> 
{
public:Data() { std::cout << "Data<T1, int>" << std::endl; }
private:T1 _d1;int _d2;
};

这样,当实例化对象时,只要第二个模板参数是 int,就会调用这个特化版本。

🌴更进一步的参数限制

        偏特化还可以通过更进一步的参数限制,实现更灵活的模板定制。例如,我们把两个参数都特化为指针类型:

template <typename T1, typename T2>
class Data<T1*, T2*> 
{
public:Data() { std::cout << "Data<T1*, T2*>" << std::endl; }
private:T1 _d1;T2 _d2;
};

或者特化为引用类型:

template <typename T1, typename T2>
class Data<T1&, T2&> 
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2) {std::cout << "Data<T1&, T2&>" << std::endl;}
private:const T1& _d1;const T2& _d2;
};

这样,当实例化的模板参数满足这些特定条件时,就会调用相应的特化版本。

3.3 类模板特化应用实例:解决排序问题

假设我们有一个专门用于比较的类模板 Less,用于在排序算法中按照小于关系比较元素:

template <class T>
struct Less 
{bool operator()(const T& x, const T& y) const {return x < y;}
};

        当我们对日期对象进行排序时,如果直接对日期指针进行排序,会出现问题。因为默认的 Less 模板会比较指针的地址,而不是指针指向的日期对象本身。

Date d1(2025, 5, 1);
Date d2(2025, 5, 2);
Date d3(2025, 5, 3);std::vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);std::sort(v2.begin(), v2.end(), Less<Date*>());

        这段代码会导致排序结果不符合预期,因为它是按照指针的地址排序,而不是按照日期的大小排序。

        为了解决这个问题,我们可以对 Less 类模板针对指针类型进行特化:

template <>
struct Less<Date*> 
{bool operator()(Date* x, Date* y) const {return *x < *y;}
};

        特化后的 Less<Date*> 模板会比较指针指向的日期对象的值,而不是指针的地址。这样,当我们对日期指针进行排序时,就能得到正确的结果。

三、模板的分离编译

1. 分离编译的困惑

        在实际开发中,我们经常会对代码进行分离编译。也就是把代码分成多个源文件,分别编译后再链接成一个可执行文件。然而,对于模板来说,分离编译可能会引发一些问题。

假设我们有一个函数模板:

// Add.h
#ifndef ADD_H
#define ADD_Htemplate<typename T>
T Add(T a, T b) 
{return a + b;
}#endif

然后我们在另一个源文件中使用它:

// main.cpp
#include "Add.h"
#include <iostream>int main()
{std::cout << Add<int>(1, 2) << std::endl;return 0;
}

如果我们尝试将这两个文件分别编译,再链接成一个可执行文件,就会在链接阶段报错。为什么呢?

2. 问题的根源

        问题在于,模板的实例化是在编译阶段进行的。如果在一个源文件中使用了模板,但模板的定义在另一个源文件中,那么编译器在编译这个源文件时并不知道要实例化哪些模板。所以在编译后的目标文件中,就不会生成对应的模板实例化代码。链接器在链接时找不到这些代码,就会报错。

3. 解决方案

为了解决这个问题,我们可以采用以下两种方法:

方法一:显示实例化

在模板定义的源文件中,手动对常用类型进行实例化。

// Add.cpp
#include "Add.h"// 显示实例化
template int Add<int>(int, int);
template double Add<double>(double, double);

这样,在编译 Add.cpp 时,就会生成这些实例化后的函数。链接时就不会出错了。

不过,这种方法有个缺点:每次需要使用一个新的类型,都要手动添加实例化代码,很麻烦。

方法二:把模板定义放在头文件中

        其实,最简单的方法是直接把模板的定义放在头文件中。这样,在每个使用模板的源文件中包含头文件时,编译器就能在需要的时候实例化模板了。

// Add.h
#ifndef ADD_H
#define ADD_Htemplate<typename T>
T Add(T a, T b) 
{return a + b;
}#endif
// main.cpp
#include "Add.h"
#include <iostream>int main() 
{std::cout << Add<int>(1, 2) << std::endl;std::cout << Add<double>(1.1, 2.2) << std::endl;return 0;
}

这种做法虽然可能会导致一些代码冗余,但对于大多数情况来说,是最简单有效的解决方案。

四、总结:模板的优缺点与应用场景

优点

  • 代码复用:通过模板,我们能写出通用的代码,适用于多种类型,大大提高了代码的复用性。

  • 灵活性:模板让我们能根据不同的类型进行不同的操作,增强了代码的灵活性。

  • 性能:模板在编译时期就确定了类型,所以不会像运行时的多态那样有额外的性能开销。

缺点

  • 代码膨胀:每个模板实例化都会生成一份代码,可能会导致生成的可执行文件体积变大。

  • 编译时间:复杂的模板代码可能会增加编译时间。

  • 错误信息难以理解:模板相关的编译错误信息通常比较复杂,可能会让开发者难以快速定位问题。

应用场景

  • 通用算法库:像 STL 这样的标准模板库,利用模板实现了各种通用的算法和数据结构。

  • 类型安全的容器:模板能让容器类在编译时期就确定存储的类型,提高了类型安全性。

  • 自定义通用工具:在项目中实现一些通用的工具类或函数,提高开发效率。

http://www.xdnf.cn/news/876.html

相关文章:

  • 5.2.1 CallerMemberName的使用
  • Java 服务器端 jar 包内 class 文件替换与配置文件修改高级技术指南
  • SQL 使用 UPDATE FROM 语法进行更新
  • C++回溯算法详解
  • 在线查看【免费】vsd, vsdx/wmf, emf /psd, eps/pdf ,ofd, rtf/xmind/bpmn/eml/epub文件格式网
  • Elasticsearch插件:IDEA中的Elasticsearch开发利器
  • 【Unity笔记】Unity音视频播放监听器封装笔记:VideoPlayer + AudioSource事件触发与编辑器扩展
  • leetcode:LCP 01. 猜数字(python3解法)
  • 并发设计模式之双缓冲系统
  • 10天学会嵌入式技术之51单片机-day-4
  • 安装WSL2.0
  • LX4-数据手册相关
  • 一个很简单的机器学习任务
  • 我用deepseek做了一个提取压缩文件夹下pdf和word文件工具
  • 解决 Ubuntu 下 VTune 无法收集 CPU 硬件时间计数数据的问题
  • Android Kotlin+Compose首个应用
  • 服务器在国外国内用户访问慢会影响谷歌排名吗?
  • Python 写一个带参数的EXE函数
  • SystemVerilog语法之内建数据类型
  • 数字IC后端PR阶段Innovus,ICC,ICC2修复short万能脚本分享
  • 20.3 使用技巧9
  • Linux:权限相关问题
  • 实验六- Linux网络管理
  • 【MySQL】MySQL中的数据类型详解
  • 【React】获取元素距离页面顶部的距离
  • Spark(20)spark和Hadoop的区别
  • 数据赋能(203)——原则与原理——原则方法
  • 应用层核心协议详解:HTTP, HTTPS, RPC 与 Nginx
  • 健康养生,开启新生活
  • 随机深林算法是分类还是回归?