模板进阶:深入理解模板机制

本文代码已同步Github

一、非类型模板参数

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

模板参数分为类型形参非类型形参

类型形参:出现在模板参数列表中,跟在class或者typename之后的类型形参

非类型形参:用一个常量作为模板的一个参数,在模板中当成常量来使用

我们通过一个例子来理解

cpp 复制代码
namespace stl
{
	//N即为非类型模板参数
	template<size_t N = 2>
	class Stack
	{
	private:
		int _a[N];
		int _top;
		int _size;
	};
}


void test_template_value()
{
	//通过传入的N的大小来控制底层的_a
	stl::Stack<> st0;
	stl::Stack<5> st1;
	stl::Stack<10> st2;
}

2. 非类型模板参数的限制

  • 非类型模板参数必须在编译期就能够确定
  • 浮点数、类对象以及字符串不能作为非类型模板参数。

3、理解非类型模板参数

我们通过STL中的array来理解非类型模板参数

发现array就是一个标准的使用非类型模板参数实现的容器;

底层就是一个大小为N的静态数组

不放我们直接手动模拟实现一个array类,来体会非类型模板参数的应用

首先来搭建基础框架

cpp 复制代码
template<class T, size_t N = 10>
class array
{
public:
    
private:
	size_t _size;
	T _array[N];//N的大小来控制静态数组的大小
};

顺便实现一下operator[]以及基础接口

cpp 复制代码
T& operator[](size_t pos)
{
	return _array[pos];
}

const T& operator[](size_t pos) const
{
	return _array[pos];
}

size_t size()
{
	return _size;
}

bool ematy()
{
	return _size == 0;
}

来测试一下

二、模板特化

1. 为什么需要模板特化

在使用模板时,通常可以提高代码效率,简化处理;

最常见的便是比较函数模板,实现模板之后,直接传入参数即可进行比较;

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

我们再来看一个列子

cpp 复制代码
void test_compare()
{
    int a = 1;
    int b = 2;
    std::cout << less(a, b) << std::endl;

    std::string s1 = "hello";
    std::string s2 = "world";
    std::cout << less(s1, s2) << std::endl;

    std::string* p1 = &s1;
    std::string* p2 = &s2;
    //此时比较的是什么?
    std::cout << less(p1, p2) << std::endl;

}

显然,比较的是地址

模板实例化的T推导出类型为string*,因此会比较地址大小;

而我们期望的是比较指针指向的内容

此时就需要函数模板的特化来进行处理;

2. 函数模板特化

怎么进行函数模板特化呢?

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

对于刚才例子中出现的情况,我们进行特化

cpp 复制代码
template<class T>
bool less(const T& x,const T& y)
{
	return x < y;
}
	
//特化
template<>
bool less<std::string*>(const std::string*& x, const std::string*& y)
{
	return *x < *y;
}

这样特化之后,编译器报错了:

为什么说我们特化的版本那无法与原版本匹配呢?

关键就在const上;

对于函数模板,const修饰的就是变量本身;

如果x是指针,那就是修饰指针本身不被修改

对于特化版本,const此时修饰的是指针所指向的内容

因此,导致两者不匹配;

该怎么解决呢?

在特化的版本中,将const改成修饰变量本身即可

对于函数模板,T仅仅表示类型,x仅仅表示变量;

因此,无法将函数模板中的const改成修饰指针所指向的内容

cpp 复制代码
template<class T>
bool less(const T& x,const T& y)
{
	return x < y;
}
	
//特化
template<>
bool less<std::string*>(std::string* const & x, std::string* const & y)
{
	return *x < *y;
}

3. 为什么函数模板特化不推荐

首先,面对这种情况,我们可以直接选择实现出特定的函数即可

cpp 复制代码
bool less(std::string* const& x, std::string* const& y)
{
	return *x < *y;
}

直接设计成一个普通函数;

其次,特化实现起来有点麻烦;不如直接实现成普通函数;

对于const的把控,也需要有一定认识;

因此,不推荐使用函数模板特化

4. 类模板特化

① 全特化

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

我们给出全特化的步骤:

  1. 定义主模板。
  2. 使用 template<>
  3. 类名后指定具体类型。
  4. 重新实现成员。

我们通过一个例子来理解

cpp 复制代码
template<class T1,class T2>
class example
{
public:
	example()
	{
		std::cout << "example<T1, T2>" << std::endl;
	}
private:
	T1 _a;
	T2 _b;
};

//全特化
template<>
class example<int,char>
{
public:
	example()
	{
		std::cout << "example<int, char>" << std::endl;
	}
private:
	int _a;
	char _b;
};

我们来进行测试

② 偏特化

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

我们先给出偏特化步骤

  1. 前提:必须先存在一个主模板。
  2. 声明 :使用 template<保留的模板参数>(尖括号内写未确定的类型)。
  3. 指定 :类名后跟 <部分限制的类型>
  4. 实现 :在偏特化版本中完全重新实现所有成员

对于刚才的类模板,我们进行偏特化

cpp 复制代码
//偏特化
template<class T>
class example<T,int>
{
public:
	example()
	{
		std::cout << "example<T,int>" << std::endl;
	}
private:
	T _a;
	int _b;
};

除此之外,还可以进一步对参数进行限制;

cpp 复制代码
//参数偏特化为指针
template<class T1,class T2>
class example<T1*,T2*>
{
public:
	example()
	{
		std::cout << "example<T1*,T2*>" << std::endl;
	}
private:
	T1* _a;
	T2* _b;
};

//参数偏特化为引用
template<class T1, class T2>
class example<T1&, T2&>
{
public:
	example(const T1& _a, const T2& _b)
		:_a(_a)
		,_b(_b)//引用必须要初始化
	{
		std::cout << "example<T1&, T2&>" << std::endl;
	}
private:
		
	const T1& _a;
	const T2& _b;
};

我们来测试一下

三、模板分离编译

1. 什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有

目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

2. 为什么模板不能正常进行分离编译

cpp 复制代码
//.h
//声明
template<class T>
T Add(const T& x, const T& y);

//.cpp
void test_add()
{
    //error LNK2019: 无法解析的外部符号 "int __cdecl stl::Add<int>(int const &,int const &)" (??$Add@H@stl@@YAHAEBH0@Z),函数 "void __cdecl stl::test_add(void)" (?test_add@stl@@YAXXZ) 中引用了该符号
	std::cout << Add(1, 2) << std::endl;
    //error LNK2019: 无法解析的外部符号 "double __cdecl stl::Add<double>(double const &,double const &)" (??$Add@N@stl@@YANAEBN0@Z),函数 "void __cdecl stl::test_add(void)" (?test_add@stl@@YAXXZ) 中引用了该符号
	std::cout << Add(1.1, 2.2) << std::endl;
}

根本原因在于:模板是编译时的"蓝图",实例化时必须看到完整定义(声明+实现)

正常情况下,程序运行前有以下步骤:

预处理 -> 编译 -> 汇编 -> 链接

链接之前,多文件之前互不干扰;

在链接时,编译器会去找main函数中各个函数的地址

对于普通函数,直接就能找到其地址;

而对于模板,由于声明与定义是分离的,导致只能找到声明,找不到定义;

因此,导致最终编译失败;

3. 如何解决

① 将声明和定义放在一起

cpp 复制代码
template<class T>
T Add(const T& x, const T& y)
{
	return x + y;
}

② 显式实例化

cpp 复制代码
//.h
//声明
template<class T>
T Add(const T& x, const T& y);
	
//显示实例化
template int Add(const int&, const int&);
template double Add(const double&, const double&);
cpp 复制代码
//.cpp
//Add函数定义
template<class T>
T Add(const T& x, const T& y)
{
	return x + y;
}

void test_add()
{
	std::cout << Add(1, 2) << std::endl;
	std::cout << Add(1.1, 2.2) << std::endl;

}

四、模板总结

通过本篇的学习,我们进一步了解了模板在实际使用中的一些重要特性:

  1. 非类型模板参数
    • 模板参数除了可以是类型,还可以是编译期确定的常量。
    • 进一步提高了模板的灵活性。
  2. 模板特化
    • 当通用模板无法正确处理某些特殊类型时,可以针对特定类型提供特殊实现。
    • 函数模板可以进行特化,但通常直接使用普通函数会更加简单。
    • 类模板特化分为全特化偏特化
  3. 模板分离编译
    • 模板的实例化需要看到完整的模板定义。
    • 因此,模板的声明和定义通常需要放在同一个文件中,实际开发中一般使用 .hpp 文件。
  4. 模板的优缺点
    • **优点:**提高代码复用性、增强代码灵活性,减少重复代码。
    • **缺点:**可能造成代码膨胀、增加编译时间,并且模板编译错误信息通常比较复杂。

通过这些内容,可以看到模板并不仅仅是"让一个函数支持多种类型",而是一套能够根据不同需求生成和定制代码的机制。在实际开发中,需要根据具体场景合理使用模板。

如果觉得有帮助,可以关注Github项目持续更新

相关推荐
Elsa️7464 小时前
算法一周刷题总结
c++·算法
风合星语4 小时前
2026 机器人工程实例(五):让 Allegro Hand 在 MuJoCo 中完成接触抓取——抓取状态机、稳定抬升与受控释放
c++·python·机器人·仿真
小羊没烦恼!4 小时前
Memory 记忆设计讨论:Agent Memory 的数据模型可以怎么设计
java·开发语言·前端·c++·c#
welfare9625 小时前
新号别搞:内存管理+模板初阶+STL简介
开发语言·c++
雪落漂泊6 小时前
C++11(上)
开发语言·c++
zh路西法7 小时前
【玩转VLA具身智能机械臂】(四):点云分割与 OBB 夹取——从 PCA 失效到主动扫描
c++·机械臂·pca·vla·planner·obb·moveit2
爱和冰阔落7 小时前
【Linux】条件变量为什么必须配合互斥锁:从 pthread_cond_wait 到阻塞队列
linux·运维·c++·缓存·中间件·安卓
星间都市山脉7 小时前
Android16 SystemService.onBootPhase 调用时机
android·java·linux·windows·ubuntu
初願致夕霞7 小时前
Linux 进程间通信(IPC)机制详解:管道、FIFO 与 System V 共享内存
linux·服务器·c++
Tairitsu_H8 小时前
[C++] C++11 Lambda与包装器深度解析
开发语言·c++·c++11·lambda·包装器