1. 介绍
众所周知,模板分为 函数模板 和 类模板。
两者的主要差别是:
- 函数模板定义的是函数和类方法,类模板定义的主要是
class
和struct
; - 函数模板不能特例化,但能重载,类模板能特例化,但不能重复定义;
- 函数模板能通过调用实参自动推导匹配类型,而类模板必须显性声明;
- 本文的实验环境是 RT-Thread Studio。
- 函数模板重载可以理解函数的重载,也可以理解为函数模板的重载,后面有解释。
- 类模板在同一个命名空间时,只能是唯一。
2. 函数模板的实例化
2.1 关于函数的重载
下面这段代码是用来测试 —— 函数被调用时,不同实参形式,到底会生成或匹配到哪些函数形式。其中注释部分分别代表几各种函数形式:
func(T)
func(T &)
func(T &&)
func(T *)
func(T const)
func(T volatile)
func(T const volatile)
//template <typename T>
//void func(T a) {rt_kprintf("func<T>()\n");}
//template <typename T>
//void func(T &a) {rt_kprintf("func<T &>()\n");}
//template <typename T>
//void func(T &&a) {rt_kprintf("func<T &&>()\n");}
//template <typename T>
//void func(T *a) {rt_kprintf("func<T *>()\n");}
//template <typename T>
//void func(T const a) {rt_kprintf("func<T const>()\n");}
//template <typename T>
//void func(T volatile a) {rt_kprintf("func<T volatile>()\n");}
//template <typename T>
//void func(T const volatile a) {rt_kprintf("func<T const volatile>()\n");}
int main(void)
{
int a = 13;
int &r = a;
int *p = &a;
const int ca = 13;
volatile int va = 13;
volatile const int cva = 13;
func(a);
func(r);
func(13);
func(p);
func(ca);
func(va);
func(cva);
return 0;
}
而测试的方法,除了观察 print
的内容外,还能查看调试窗口的 反汇编
如果没有这个窗口的话, 可以在此打开。
================ 总结测试的结果: ================
-
func(T)
、func(T const)
、func(T volatile)
和func(T const volatile)
在编译器看来是同一类型。即任意两者或以上同时存在均报错 重复定义,
redefinition of 'template<class T> void func(T)'
而其中访问修饰符或优化修饰符与实参同,换言之,你定义函数的形参无论是哪种cv
形式(const
和volatile
),影响的都只是函数内部的作用域。但如果实参有要求,则形参T
也自动推导为相同形式。
但请注意 引用版本 的不同,
func(T &&)
与func( const T &&)
是可以共存的。
传值版本
func(T)
和 引用版本 (func(T &)
和func(T &&)
) 不能共存,
call of overloaded 'func(int&)' is ambiguous
因为调用int a = 13; fun(a);
时,可理解为 复制值过去 ,也可理解为 传递引用过去 ,故存在歧义。func(T)
版本生成的实例,不管实参类型是int
、int&
还是int&&
,均是传值版本,会产生复制操作。func(T &&)
比func(T &)
更具有适用意义。众所周知,C++ 是有左右值引用之分的。一般的理解是T &
引用左值类型,T &&
引用右值类型。所以,假如你的程序是想分别对左右值有不同的处理的话, 可以分别定义两个函数模板。但 如果你只定义一个引用函数模板的话,你就应定义func(T &&)
。因此函数模板的引用存在折叠原则,具体网上可以查看。func(T *)
指针类型是比较特别的。上面说过,重载的版本中(形参数量相同的前提下),可只能定义 值版本 (T
) 或 引用版本 (T&
和T&&
) 两者中的一种。而对于 指针类型 而言,哪种都可以实例出来。
值版本中,生成实例T = <int *>
;而引用版本,则生成T = <int *&>
。
只有当你想单独区分传递是否为指针时,才定义func(T *)
。
我画了一个可能不完全合理的适当范围图,
2.2 关于函数模板形式选择的若干建议
- 若你不想实参被修改,则定义
func(T)
; - 若你想传递引用,则定义
func(T &&)
,使得传递字面量也适用; - 若想单独区分左右值引用或指针,则分别定义相应版本;
- 若想传递引用但又不修改其内容,可只定义
func(const T &&)
; - 若想区分可修改的引用或不可修改的引用,则要对两个版本分别定义;
2.3 关于函数模板不能特例化的说法的修改
不是说函数模板不能特例化,更确切的说法就是,函数模板不需要类模板特例化的形式。
template <typename T>
void func(T a) {rt_kprintf("func(T a)\n");}
template <>
void func<double>(double a) {rt_kprintf("func(double a)\n");}
上面的是模仿类模板特例化形式写的,你会发现很鸡胁。函数重载不是形式更漂亮吗?!
void func(double a) {rt_kprintf("func(double a)\n");}
template <typename T>
void func(T a) {rt_kprintf("func(T a)\n");}
另外,在同一个 namespace 里,是允许多个同名函数存在的,只要参数类型匹配不相同就行。
void func(double a) {rt_kprintf("func(double a)\n");}
template <typename T>
void func(T a) {rt_kprintf("func(T a)\n");}
template <typename T, typename U>
void func(T a, U b) {rt_kprintf("func(T a, U b)\n");}
这更是类模板所不能达到的。故特例化只适合类模板。
3. 类模板实例化
3.1 修饰符的特例化
先上测试代码,
template <typename T>
class A {
public:
A() {rt_kprintf("A<T>()\n");}
};
template <typename T>
class A<T*> {
public:
A() {rt_kprintf("A<T*>()\n");}
};
template <typename T>
class A<T&> {
public:
A() {rt_kprintf("A<T&>()\n");}
};
template <typename T>
class A<T&&> {
public:
A() {rt_kprintf("A<T&&>()\n");}
};
template <typename T>
class A<T const> {
public:
A() {rt_kprintf("A<T const>()\n");}
};
template <typename T>
class A<T volatile> {
public:
A() {rt_kprintf("A<T volatile>()\n");}
};
template <typename T>
class A<T const volatile> {
public:
A() {rt_kprintf("A<T cv>()\n");}
};
int main(void)
{
A<int> a1;
A<int &> a2;
A<int &&> a3;
A<int const> a4;
A<int volatile> a5;
A<int *> a6;
A<int const volatile> a7;
return 0;
}
类模板与函数模板不同,const T
与 T
视为不同的匹配模式。
上例中,若实例化 <const int &>
类型的,则会自动匹配到 <T &>
。其中 T = const int
。
这个修饰符的匹配是 元编程 里很重要的一个环节。
3.2 类型匹配
template <typename T>
class B {
public:
B() {rt_kprintf("B<T>()\n");}
};
template <>
class B<int> {
public:
B() {rt_kprintf("B<int>()\n");}
};
template <>
class B<int *> {
public:
B() {rt_kprintf("B<int *>()\n");}
};
template <>
class B<double> {
public:
B() {rt_kprintf("B<double>()\n");}
};
template <>
class B<void> {
public:
B() {rt_kprintf("B<void>()\n");}
};
template <>
class B<void *> {
public:
B() {rt_kprintf("B<void *>()\n");}
};
int main(void)
{
B<int> b1;
B<int *> b2;
B<double> b3;
B<void> b4;
B<void *> b5;
B<A<int>> b6; // => B<T>
return 0;
}