知识点#
15.1 模板概念与函数模板的定义、调用#
- @ 函数模板图
- ![[白板/P44 函数模板.canvas|P44 函数模板]]
15.1.1 模板概念#
[!tip] 模板与泛型
- @ 泛型编程: 以独立于任何特定类型的方式编写代码
- @ 模板: 泛型编程的基础,是创建类或者函数的蓝图或者公式
- 模板支持将==类型作为参数==的程序设计方式
- 模板一般分为函数模板和类模板
- 通过给模板提供足够的信息,让蓝图转变为具体的类或函数,这种转变发生在==编译时==
15.1.2 函数模板的定义#
[!tip] 函数模板定义
- @ 函数模板: 以
template关键字开头,后面跟尖括号中的模板参数列表- 模板参数用
typename或class修饰(两者等价,与类定义的class无关)- 多个模板参数用逗号分隔
- 模板参数列表中的
T是类型参数,具体代表什么类型由编译器根据调用推断
template <typename T>
T funcadd(T a, T b) {
T addhe = a + b;
return addhe;
}
15.1.3 函数模板的调用#
[!tip] 函数模板调用
- 调用时编译器根据实参推断模板参数类型
- 推断成功后,编译器实例化一个特定版本的函数
- ! 如果实参类型不一致(如
int和float),编译器无法推断,报错- 可以用
< >显式指定模板参数
int he = funcadd(3, 4); // 推断 T 为 int
float he2 = funcadd(3.1f, 1.2f); // 推断 T 为 float
int he3 = funcadd<int>(3, 4); // 显式指定
15.1.4 非类型模板参数#
[!tip] 非类型模板参数
- @ 非类型模板参数: 表示一个值而非类型,用传统类型名指定(如
int size)- ! 非类型模板参数必须是==常量表达式==,值在编译时确定
- 可以与类型模板参数混合使用
- 显式指定模板参数时,以
< >中提供的类型为准- 函数模板的
inline写在模板参数列表之后
template <int a, int b>
int funcaddv2() {
int addhe = a + b;
return addhe;
}
int result = funcaddv2<12, 13>(); // 通过 <> 传递常量
template <typename T, int a, int b>
int funcaddv3(T c) {
int addhe = (int)c + a + b;
return addhe;
}
int result = funcaddv3<int, 11, 12>(13); // 混合使用
[!tip] 模板参数分类
| 分类 | 说明 | 值来源 |
|---|---|---|
| 类型模板参数 | typename/class 修饰 | 编译器推断或用户提供 |
| 非类型模板参数 | 传统类型名修饰 | 用户提供(常量表达式) |
15.2 类模板概念与类模板的定义、使用#
- @ 类模板图
- ![[白板/P45 类模板.canvas|P45 类模板]]
15.2.1 类模板概念#
[!tip] 类模板概念
- @ 类模板: 产生类的模具,通过给定的模板参数生成具体的类
- ! 编译器==不能为类模板推断模板参数==,必须用
< >提供额外信息- 类模板名不是类型名,实例化后的才是(如
myvector<int>才是类型名)- 每种类型编译器都会生成一个不同的类
15.2.2 类模板的定义#
[!tip] 类模板定义
- 类模板的所有信息(声明和实现)都必须写在一个
.h文件中- 因为实例化具体类时必须有类模板的全部信息
- 其他源文件只需
#include这个.h文件即可
// myvector.h
template <typename T>
class myvector {
public:
typedef T* iterator;
public:
myvector(); // 构造函数声明
myvector& operator=(const myvector&); // 赋值运算符
public:
iterator mybegin();
iterator myend();
};
15.2.3 类模板的成员函数#
[!tip] 类模板成员函数
- 在类模板定义内部实现的成员函数被隐式声明为内联函数
- 在类模板定义外部实现时:
- 1 必须以
template开头,后接模板参数列表- 2 类名后用
< >列出所有模板参数名- ! 只有被使用到的成员函数才会被实例化
// 类模板外部实现
template <typename T>
void myvector<T>::myfunc() { /* ... */ }
template <typename T>
myvector<T>::myvector() { /* ... */ }
15.2.4 类模板名字的使用#
[!tip] 类模板名字使用规则
- 在类模板定义内部,可以直接使用类模板名,不需要跟模板参数
- 编译器会假定
myvector等价于myvector<T>- ! 在类模板定义之外,必须带上模板参数
// 类内部
myvector& operator=(const myvector&); // 可以
// 类外部
template <typename T>
myvector<T>& myvector<T>::operator=(const myvector<T>&) {
return *this;
}
15.2.5 非类型模板参数的使用#
[!tip] 类模板非类型模板参数
- 非类型模板参数可以给默认值
- 使用默认值时用空的
< >(不能省略)- ! 浮点型和类类型==不能==作为非类型模板参数
template <typename T, int size = 10>
class myarray {
private:
T arr[size];
};
myarray<int, 100> tmparr; // 指定 size=100
myarray<int> tmparr; // 使用默认 size=10
// 类外部实现
template <typename T, int size>
void myarray<T, size>::myfunc() { /* ... */ }
15.3 使用 typename 的场合、函数模板、默认模板参数与趣味写法分析#
15.3.1 typename 的使用场合#
[!tip] typename 两种用法
- % 用法一:在模板定义里,表明其后的模板参数是类型参数
template <typename T>中的typename可替换为class- % 用法二:用
typename标明这是一个类型(类型成员)
- ! 在类模板定义之外,访问依赖于模板参数的类型成员时,必须加
typename- 默认情况下 C++ 假定通过
::访问的是静态成员变量而非类型- ! 此处
typename不能用class替换
// 用法二:标明类型成员
template <typename T>
typename myvector<T>::myiterator myvector<T>::mybegin() { /* ... */ }
// 另一个范例
template <typename T>
typename T::size_type getlength(const T& c) {
if (c.empty()) return 0;
return c.size();
}
15.3.2 函数指针作为其他函数的参数#
[!tip] 函数指针作参数
- 使用
typedef定义函数指针类型- 将函数指针类型作为函数形参类型
typedef int(*FuncType)(int, int);
void testfunc(int i, int j, FuncType funcpoint) {
int result = funcpoint(i, j);
cout << result << endl;
}
testfunc(3, 4, mf); // mf 是函数名,代表函数首地址
15.3.3 函数模板趣味用法举例#
[!tip] 可调用对象与函数模板
- @ 可调用对象: 重载了
()运算符的类的对象,可以像函数一样调用- 同一个函数模板,根据传递参数不同,可推断出不同类型:
- 函数指针类型
- 可调用对象类型
- ! 可调用对象必须重载
()运算符,且参数和返回值类型需匹配
template <typename T, typename F>
void testfunc(const T& i, const T& j, F funcpoint) {
cout << funcpoint(i, j) << endl;
}
// 可调用对象类
class tc {
public:
int operator()(int v1, int v2) const { return v1 + v2; }
};
tc tcobj;
testfunc(3, 4, tcobj); // 推断 F 为 tc 类类型
testfunc(3, 4, tc()); // 传递临时对象,节省拷贝构造
15.3.4 默认模板参数#
[!tip] 默认模板参数
- 类模板和函数模板都可以有默认模板参数
- ! 从有默认值的参数开始,后面的所有参数都得有默认值
- 类模板使用默认值时用空的
< >(不能省略)- C++11 新标准允许为函数模板提供默认模板参数
// 类模板默认参数
template <typename T = string, int size = 5>
class myarray { /* ... */ };
myarray<> abc; // 完全使用默认值
myarray<int> def; // 只提供第一个参数
// 函数模板默认参数
template <typename T, typename F = tc>
void testfunc(const T& i, const T& j, F funcpoint = F()) {
cout << funcpoint(i, j) << endl;
}
testfunc(3, 4); // 使用默认模板参数和默认函数参数
15.4 成员函数模板、模板显式实例化与声明#
15.4.1 普通类的成员函数模板#
[!tip] 普通类的成员函数模板
- 普通类的成员函数本身可以是一个函数模板
- ! 成员函数模板==不可以是虚函数==
class A {
public:
template <typename T>
void myft(T tmpt) {
cout << tmpt << endl;
}
};
A a;
a.myft(3); // 自动推断 T 为 int
15.4.2 类模板的成员函数模板#
[!tip] 类模板的成员函数模板
- 类模板和其成员函数模板有各自独立的模板参数
- 在类模板外部实现时,先写类模板参数列表,再写成员函数模板参数列表
- 构造函数也可以引入自己的模板参数,与类的模板参数无关
template <typename C>
class A {
public:
template <typename T2>
A(T2 v1, T2 v2); // 构造函数模板
template <typename T>
void myft(T tmpt);
};
// 类外部实现
template <typename C> // 类模板参数
template <typename T2> // 构造函数模板参数
A<C>::A(T2 v1, T2 v2) { /* ... */ }
15.4.3 模板显式实例化与声明#
[!tip] 模板显式实例化
- 多个
.cpp文件独立编译时,可能重复实例化相同模板,增加编译开销- @ 实例化定义:
template A<float>;— 让编译器实例化,只写在一个.cpp中- @ 实例化声明:
extern template A<float>;— 告诉编译器其他文件已实例化,不重复生成- 函数模板同理:
template void myfunc(int&, int&);- ! 实例化定义只有一个,实例化声明可以有多个
- ! 显式实例化会把类模板所有成员函数都实例化出来
// test.cpp 中(实例化定义)
template A<float>;
template void myfunc(int&, int&);
// MyProject.cpp 中(实例化声明)
extern template A<float>;
extern template void myfunc(int&, int&);
15.5 using 定义模板别名与显式指定模板参数#
15.5.1 using 定义模板别名#
[!tip] using 定义模板别名
typedef只能给固定类型起别名,无法定义类型不固定的别名- C++11 的
using可以定义别名模板(类型相关的模板)using包含了typedef的所有功能,语法顺序相反- 别名模板既不是类模板也不是函数模板,是一种新的模板形式
// typedef 定义固定类型别名
typedef std::map<std::string, int> map_s_i;
// using 定义固定类型别名(语法顺序相反)
using map_s_i = std::map<std::string, int>;
// using 定义模板别名(typedef 做不到)
template <typename T>
using str_map_t = std::map<std::string, T>;
str_map_t<int> map1; // 等价于 std::map<std::string, int>
str_map_t<string> map2; // 等价于 std::map<std::string, string>
// using 定义函数指针类型
using FunType = int(*)(int, int); // 比 typedef 更直观
// using 定义函数指针模板别名
template <typename T>
using myfunc_t = int(*)(T, T);
15.5.2 显式指定模板参数#
[!tip] 显式指定模板参数
- 显式模板实参从左到右按顺序与模板参数匹配
- 能推导出来的可以省略,但省略后后面的也得省略
- ! 无法推断的模板参数必须显式提供
template <typename T1, typename T2, typename T3>
T1 sum(T2 i, T3 j) {
T1 result = i + j;
return result;
}
auto result = sum<double, double, double>(2000000000, 2000000000); // 三个都指定
auto result = sum<double>(2000000000, 2000000000); // 只指定 T1
15.6 模板全特化与偏特化(局部特化)#
- @ 模板特化图
- ![[白板/P46 模板特化.canvas|P46 模板特化]]
15.6.1 类模板特化#
[!tip] 泛化与特化
- @ 泛化: 模板可以指定任意模板参数,这就是泛化
- @ 特化: 针对某种独特类型做单独设计和代码编写
- ! 先有泛化版本才能有特化版本
[!tip] 类模板全特化
- @ 全特化: 所有类型模板参数都用具体类型代替
- 格式:
template <> struct TC<int, int> { /* ... */ };- 特化版本具有==优先被选择权==
- 可以有任意多个全特化版本
- 也可以只特化某个成员函数
// 泛化版本
template <typename T, typename U>
struct TC {
TC() { cout << "TC泛化版本构造函数" << endl; }
void functest() { cout << "TC泛化版本" << endl; }
};
// 全特化版本
template <>
struct TC<int, int> {
TC() { cout << "TC<int,int>特化版本构造函数" << endl; }
void functest() { cout << "TC<int,int>特化版本" << endl; }
};
// 特化成员函数
template <>
void TC<double, double>::functest() {
cout << "TC<double,double>的functest()特化版本" << endl;
}
[!tip] 类模板偏特化(局部特化)
- % 模板参数数量上的偏特化: 绑定部分模板参数,留下部分
- % 模板参数范围上的偏特化: 将类型范围缩小(如
T→const T、T*、T&、T&&)- 偏特化完了本质上还是一个模板
// 数量上的偏特化
template <typename T, typename U, typename W>
struct TCP { /* 泛化 */ };
template <typename U>
struct TCP<int, U, double> { /* 偏特化:绑定第1、3个参数 */ };
// 范围上的偏特化
template <typename T>
struct TCF { /* 泛化 */ };
template <typename T>
struct TCF<const T> { /* const 特化 */ };
template <typename T>
struct TCF<T*> { /* 指针特化 */ };
template <typename T>
struct TCF<T&> { /* 左值引用特化 */ };
template <typename T>
struct TCF<T&&> { /* 右值引用特化 */ };
15.6.2 函数模板特化#
[!tip] 函数模板全特化
- 格式:
template <> void tfunc<int, double>(int&, double&) { /* ... */ }- 全特化等价于实例化一个函数模板,不等价于函数重载
- ! 编译器选择优先级:普通函数 > 函数模板特化版本 > 函数模板泛化版本
- ! 函数模板==不能偏特化==,只有类模板才能偏特化
// 泛化版本
template <typename T, typename U>
void tfunc(T& tmprv, U& tmprv2) {
cout << "tfunc泛化版本" << endl;
}
// 全特化版本
template <>
void tfunc<int, double>(int& tmprv, double& tmprv2) {
cout << "tfunc<int,double>特化版本" << endl;
}
15.6.3 模板特化版本放置位置建议#
[!tip] 特化版本放置位置
- 模板特化版本应和模板泛化版本放在同一个
.h文件中- 特化版本一般放在泛化版本的后面
15.7 可变参模板与模板模板参数#
- @ 可变参模板图
- ![[白板/P47 可变参模板.canvas|P47 可变参模板]]
15.7.1 可变参函数模板#
[!tip] 可变参函数模板
- @ 可变参模板: C++11 引入,允许模板定义中含有 0 到多个模板参数
- 用
...(省略号)表示可变参typename... T— 可变参类型(一包类型)T... args— 可变形参(一包形参)sizeof...(args)或sizeof...(T)— 获取参数个数
template <typename... T>
void myfunct1(T... args) {
cout << sizeof...(args) << endl; // 参数个数
}
myfunct1(); // 0
myfunct1(10, 20); // 2
myfunct1(10, 25.8, "abc", 68); // 4,类型各不相同
[!tip] 参数包展开
- 通过递归函数方式展开参数包
- 需要两个函数:
- 1 参数包展开函数(函数模板):一个单独参数 + 一包参数
- 2 递归终止函数(普通函数):0 个参数的同名函数
- 每次调用剥离一个参数,直到参数包为空时调用终止函数
// 递归终止函数
void myfunct2() {
cout << "参数包展开时执行了递归终止函数" << endl;
}
// 参数包展开函数
template <typename T, typename... U>
void myfunct2(const T& firstarg, const U&... otherargs) {
cout << "收到的参数值为" << firstarg << endl;
myfunct2(otherargs...); // 递归调用
}
myfunct2(10, "abc", 12.7);
// 输出: 10 → abc → 12.7 → 递归终止函数
15.7.2 可变参类模板#
[!tip] 可变参类模板
- 参数包展开方式与函数模板不同
- % 方式一:通过递归继承方式展开
- % 方式二:通过递归组合方式展开
- % 方式三:通过
tuple和递归调用展开
[!tip] 递归继承方式
- 偏特化版本继承参数包剩余部分
- 每次继承拆分出一个参数
- 需要一个 0 个模板参数的特化版本作为终止
- 所有子部分共享同一个
this地址
// 主模板定义(泛化版本)
template <typename... Args>
class myclasst {
public:
myclasst() { printf("泛化版本\n"); }
};
// 特殊特化版本(0个参数,递归终止)
template <>
class myclasst<> {
public:
myclasst() { printf("特殊特化版本\n"); }
};
// 偏特化版本(递归继承)
template <typename First, typename... Others>
class myclasst<First, Others...> : private myclasst<Others...> {
public:
First m_i;
myclasst(First parf, Others... paro) : m_i(parf), myclasst<Others...>(paro...) {}
};
[!tip] 递归组合方式
- 将继承改为成员变量(组合关系)
- 产生多个不同的对象(
this地址不同)
[!tip] tuple 与递归调用方式
- 使用
std::tuple存储参数- 通过计数器从 0 递增,用
get<计数器>(tuple)提取每个参数- 需要偏特化版本作为递归终止条件
tuple<float, int, int> mytuple(12.5f, 100, 52);
cout << get<0>(mytuple) << endl; // 12.5
cout << get<1>(mytuple) << endl; // 100
cout << get<2>(mytuple) << endl; // 52
15.7.3 模板模板参数#
[!tip] 模板模板参数
- @ 模板模板参数: 模板参数本身又是一个模板
- 写法:
template<class> class Container- 也可以写成
template<typename W> typename Container(W可省略)- ! 不能用
typename Container代替模板模板参数- 使用
using定义模板别名解决容器分配器参数不匹配问题
template <typename T, template<class> class Container>
class myclass {
public:
Container<T> myc; // Container 作为类模板使用
};
// 解决 vector 分配器参数不匹配
template <typename T>
using MYVec = vector<T, allocator<T>>;
myclass<int, MYVec> myvecobj; // 编译通过

