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

本文代码已同步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项目持续更新

相关推荐
豆沙沙包?8 小时前
C++~~~stack容器、queue容器、list容器(p45-P56)
c++·windows·list
Frank_refuel8 小时前
【C++八股】面向对象
开发语言·c++
h_a_o777oah8 小时前
【图论】Tarjan 缩点:解决有向图中环的问题
c++·算法·图论·acm·强连通分量·缩点·tarjan
别动我齐刘海10 小时前
机器人运动控制学习4——状态估计 State Estimation
c++·人工智能·学习·目标检测·机器学习·机器人·自动驾驶
学习星球10 小时前
OpenHarness 全面配置教学——从游戏开发工作流引入
c++·游戏·ai·ai编程
wWYy.11 小时前
C++与C的区别
c语言·c++
戒了,最后一次15 小时前
DeepSeek Harness 源码安装教程(Windows 篇)
windows·腾讯云·deepseek·harness
程序员与背包客_CoderZ15 小时前
高性能分布式KV存储引擎RocksDB入门与C/C++编码实战
c语言·开发语言·数据库·c++·分布式·分布式数据库·rocksdb
进击的_鹏15 小时前
从零开始的 Redis 学习
服务器·数据库·c++·redis·缓存