深入讨论模板

再谈模板

1、非类型模板参数

在我们之前的学习中,模板可以传类型参数。

其实模板还可以传类型之外的参数:

模板传参,支持:

  • 传递类型形参
  • 传递非类型形参
    • C++20前,只能传整型常量

那么非类型传参有什么用呢?

假设我们想要一个静态的stack。

如果我们不用非类型传参,要想得到一个存10个元素的静态stack,我们就可以这样写:

cpp 复制代码
template<class T>
class my_stack
{
public:

private:
	T a[10];// 10个元素
	int _top;
};

现在我们有了新的需求:想得到一个存1000个元素的静态stack。

我们就必须直接改代码,或者再声明一个stack类。太麻烦了。

所以支持非类型传参的模板,就派上了用场:

cpp 复制代码
template<class T, size_t n = 10>
class my_stack
{
public:

private:
	T a[n];
	int _top;
};

需要注意的是:


1、传入非类型参数n的,只能是常量:


2、非类型参数在C++20之前只能传整型值,虽然C++20之后放开了限制,但是依旧不能传一些类型,比如string。

1.1、array

C++的有些类模板是使用了非类型参数的,比如静态数组array:

我们可以发现,array就不支持诸如push_back, pop_front等操作了,而是支持重载[ ],因为它的容量是固定的,只需下标访问就可以修改。

array支持迭代器。

指向数组的指针,是天然的迭代器。

array对于内置类型作参数,默认不会初始化:

我们可以调用其中的方法fill(),手动初始化:

array有什么作用呢?有C语言的数组不就行了?

我们可以通过对比array和C语言的数组,来找出array的优势:

1、越界访问

  • C语言越界访问的检查行为是抽查,而且只能检查写时的访问,不能检查读时的访问。
  • array的访问行为是经过重载的,重载函数体内可以进行严格的检查。

2、作其它容器的类型参数

  • array可以作其它容器的类型参数:

3、传参的退化问题

  • C语言数组传参会退化为指针,会出现一些问题,比如使用不了范围for:
  • array无需担心,甚至可以传引用提高效率:

2、模板的特化

模板的特化,就是对模板作特殊化处理。

2.1、函数模板的特化

比如对于比较日期大小的函数的特化

cpp 复制代码
template<class T>
bool DateLess(T x, T y)
{
	return x < y;
}

直接比较Date,没有问题:

如果直接比较Date*,可能会有问题:

那么我们就可以将DateLess进行特化。

cpp 复制代码
template<>// 特化:专门给Date*使用
bool DateLess<Date*>(Date* x, Date* y)
{
	return *x < *y;
}

对于函数模板的特化,我们有两点需要注意:

1、const引用的使用


对于原DateLess,我们期望加上const引用,以减少拷贝:

而我们依葫芦画瓢,为专门给Date*使用的特化DateLess,加上const引用:

编译报错了。

首先,我们之前学过,const修饰指针,有两种情况:

  • const int* p1:const修饰*p1,即指针指向的内容
  • int* const p1:const修饰p1,即指针

对于原DateLess,传入的是Date类型,那么const修饰的是Date类型。

那么相对的,对于专门给 Date* 使用的特化DateLess,const修饰的应该是 Date* 类型(指针)。

所以应该这样写:


2、不特化,而匹配


对于函数模板,我们可以直接写一个匹配度高的普通函数,从而不使用函数模板:

  • "吃现成的":有现成的函数,就不使用函数模板。
  • "吃好吃的":有更匹配的参数类型,就不使用其它的类型,以避免隐式类型转换。

2.2、类模板的特化

特化:specialization

cpp 复制代码
template<class T1, class T2>
class A
{
public:
	A() :_a1(0),_a2(0) { cout << "A<T1, T2>" << endl; }
private:
	T1 _a1;
	T2 _a2;
};

template<>// 特化
class A<int, int>
{
public:
	A() :_a1(0) { cout << "A<int, int>" << endl; }
private:
	int _a1;
};

类模板的特化,对内部成员没有要求,可以加,可以删:

cpp 复制代码
template<class T1, class T2>
class A
{
public:
	A() :_a1(0),_a2(0) { cout << "A<T1, T2>" << endl; }
private:
	T1 _a1;
	T2 _a2;
};

template<>// 特化
class A<int, int>
{
public:
	A() :_a1(0) { cout << "A<int, int>" << endl; }

	void func1() {}// 添加func1()
private:
	int _a1;// 删除_a2
};

vector< bool >就使用了类模板的特化:

我们可以简单理解为:一般的vector存int(0, 1)表示true, false,需要4个字节。

但是vector< bool >进行了空间优化的处理,只用一个bit的空间来表示true, false。

2.2.1、全特化与偏特化(半特化)

全特化:所有的模板参数进行特化。

cpp 复制代码
template<class T1, class T2>
class A
{
	// ...
};

template<>// 全特化
class A<int, int>
{
	// ...
};

偏特化比较复杂。

偏特化可以是固定部分类型参数

cpp 复制代码
template<class T1, class T2>
class A
{
public:
	A() :_a1(0), _a2(0) { cout << "A<T1, T2>" << endl; }
};

template<>
class A<int, int>// 全特化
{
public:
	A() :_a1(0) { cout << "A<int, int>" << endl; }
};

template<class T>
class A<T, int>// 偏特化
{
public:
	A() { cout << "A<T, int>" << endl; }
};

偏特化也可以是限制实例化出的类的参数

cpp 复制代码
template<class T1, class T2>
class A<T1*, T2*>
{
public:
	A() { cout << "A<T1*, T2*>" << endl; }
};

我们还可以发现,上面的模板,还可以根据传入的指针类型,推导出T1, T2:

cpp 复制代码
template<class T1, class T2>
class A<T1*, T2*>
{
public:
	A() { cout << "A<T1*, T2*>" << endl; }

	void func1() { cout << typeid(T1).name() << " " << typeid(T2).name() << endl; }
};

除了限制为指针、引用,还可以限制为指针+引用、指针+int、引用+int等。

2.3、特化的应用

我们可以对之前模拟实现的priority_queue作进一步优化,利用特化实现专门给Date*比较大小的仿函数:

我们这样写,又出问题了:

const修饰的是*left(*right),而引用修饰的是left, right,所以这里的引用为普通引用,而不是const引用。

push, pop需要调用调整算法,而调整算法使用了仿函数对象_com,_com接收的是 Date* 类型值,但是传入到 _com 的重载()时, Date* 类型值需隐式转换为const Date*&,而隐式转换产生临时拷贝,临时拷贝具有常性,权限放大:

两个解决办法:

1、再加一个const,变成const引用:

2、不使用引用:

3、模板的分离编译

模板不支持声明和定义直接分离到不同文件内:

要搞清楚这个问题,我们可以简单模拟编译器编译、链接的全过程。

现在我们有一个项目,里面有三个文件;

cpp 复制代码
// a.h
#include<iostream>

using namespace std;

int Add(int x, int y);

template<class T>
T TAdd(T x, T y);
cpp 复制代码
// a.cpp
#include"a.h"

int Add(int x, int y)
{
	return x + y;
}

template<class T>
T TAdd(T x, T y)
{
	return x + y;
}
cpp 复制代码
// main.cpp
#include"a.h"

int main()
{
	cout << Add(1, 2) << endl;
	cout << TAdd(1, 2) << endl;

	return 0;
}

预处理阶段:展开头文件、条件编译、宏替换、删除注释...

预处理结束,生成文件a.i main.i

然后进入编译阶段:检查语法是否有错误。

如果语法没有问题,就将代码转化成汇编代码,生成文件a.s main.s

这里我们不关注其他细节,只看main.s的函数调用部分。

函数调用用call指令。但是当前函数Add()和TAdd()的定义不在main.s处,所以找不到函数地址,按道理编译阶段是不会通过的。

但是存在函数的声明,编译阶段就通过了。

函数的声明,相当于提醒call指令,在链接阶段再找函数的地址。

接着来到汇编阶段 :将汇编代码转化为二进制机器码。最终生成文件:a.o main.o

汇编结束,进入链接阶段 :合并生成可执行程序,链接在其他文件定义的函数、变量 等。生成文件:a.out

在这个过程中,会有一个符号表,上面记录着函数的地址。那么这时call指令就可以在这个符号表上寻找函数的地址。

问题就出在这里:TAdd函数的地址找不到。

因为TAdd没有实例化出函数

而没有实例化,是因为直到多个文件合并成一个可执行程序的时候,编译器也没有向TAdd模板传入类型参数,因为TAdd模板的声明和定义是分离的,整个编译链接过程也是分开进行的。也就是说,TAdd不知道要实例化成什么函数。

所以,TAdd模板的分离编译会报错。

我们可以这么理解:

  • 知道实例化的地方,只有声明没有定义
  • 不知道实例化的地方,却有声明
  • 结果是模板没有实例化出函数 / 类

有两种补救方案:

第一个是在a.cpp文件内显示实例化模板。但是遇到一种情况就必须显示写一个实例化函数,太麻烦了。

第二种方法,就是直接将模板直接定义在a.h。也是我们迄今为止一直在做的。

相关推荐
AI进化营-智能译站2 小时前
ROS2 C++开发系列18-STL容器实战:deque缓存激光雷达数据|priority_queue调度任务
开发语言·c++·缓存·ai
hehelm2 小时前
C++11 新特性
c++
我不是懒洋洋2 小时前
【数据结构】排序算法(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序)
c语言·数据结构·c++·经验分享·算法·排序算法
邪修king2 小时前
UE5:C++ 实现 游戏逻辑 ↔ UI 双向联动
c++·游戏·ue5
汉克老师12 小时前
GESP2025年3月认证C++五级( 第三部分编程题(1、平均分配))
c++·算法·贪心算法·排序·gesp5级·gesp五级
智者知已应修善业15 小时前
【51单片机2个按键控制流水灯运行与暂停】2023-9-6
c++·经验分享·笔记·算法·51单片机
云泽80816 小时前
C++11 核心特性全解:列表初始化、右值引用与移动语义实战
开发语言·c++
AI进化营-智能译站17 小时前
ROS2 C++开发系列12-用多态与虚函数构建可扩展的ROS2机器人行为模块
开发语言·c++·ai·机器人
Morwit17 小时前
QML组件之间的通信方案(暴露子组件)
c++·qt·职场和发展