本文代码已同步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. 函数模板特化
怎么进行函数模板特化呢?
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
对于刚才例子中出现的情况,我们进行特化
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. 类模板特化
① 全特化
全特化:将模板参数列表中所有的参数都确定化
我们给出全特化的步骤:
- 定义主模板。
- 使用
template<>。 - 类名后指定具体类型。
- 重新实现成员。
我们通过一个例子来理解
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;
};
我们来进行测试

② 偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本
我们先给出偏特化步骤
- 前提:必须先存在一个主模板。
- 声明 :使用
template<保留的模板参数>(尖括号内写未确定的类型)。 - 指定 :类名后跟
<部分限制的类型> - 实现 :在偏特化版本中完全重新实现所有成员
对于刚才的类模板,我们进行偏特化
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;
}

四、模板总结
通过本篇的学习,我们进一步了解了模板在实际使用中的一些重要特性:
- 非类型模板参数
- 模板参数除了可以是类型,还可以是编译期确定的常量。
- 进一步提高了模板的灵活性。
- 模板特化
- 当通用模板无法正确处理某些特殊类型时,可以针对特定类型提供特殊实现。
- 函数模板可以进行特化,但通常直接使用普通函数会更加简单。
- 类模板特化分为全特化 和偏特化。
- 模板分离编译
- 模板的实例化需要看到完整的模板定义。
- 因此,模板的声明和定义通常需要放在同一个文件中,实际开发中一般使用
.hpp文件。
- 模板的优缺点
- **优点:**提高代码复用性、增强代码灵活性,减少重复代码。
- **缺点:**可能造成代码膨胀、增加编译时间,并且模板编译错误信息通常比较复杂。
通过这些内容,可以看到模板并不仅仅是"让一个函数支持多种类型",而是一套能够根据不同需求生成和定制代码的机制。在实际开发中,需要根据具体场景合理使用模板。
如果觉得有帮助,可以关注Github项目持续更新