引言
在 C++ 中,模板(Template) 是一种支持泛型编程(Generic Programming) 的核心机制,允许编写与数据类型无关的代码。通过模板,可以定义通用的函数或类,根据不同的数据类型生成具体的代码实例,模板是 C++ 强大灵活性的核心体现,也是学习现代 C++ 的必经之路!
接下来这篇文章将由浅入深的讲解模板的概念、语法以及各类需要注意的地方。
一、泛型编程如何实现一个通用的交换函数?
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
使用函数重载虽然可以实现,但是有几个不够完美的地方:
- 重载的函数仅是类型不同,代码复用率低,当需要一个新的类型时就需要自己新增对应的函数。
- 可维护性低,一个出错可能导致所有重载出错。
那么能否告诉编译器我们需要一个什么样的模型,让编译器帮我们完成这些重复的工作呢?
于是在C++中引入了模板这个概念:
通俗易懂的来讲就是,我们写一个模子告诉编译器这个应该是什么样,编译器在运行时通过这个模子,为我们生成一份实际的代码,这就避免了重复的工作,而是把它交给了编译器。
二、函数模板函数模板与类型无关,只在使用时参数化,根据我们实际传入的参数产生函数的特定类型版本。
2.1 函数模板语法这里就可以使用模板来实现一个交换函数:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
2.2 函数模板的原理
可以理解为函数模板本身是一份设计图,它本身并不是一个实际的函数,是编译器使用特定方式产生特定具体类型函数的模具,其实模板就是将本来我们的重复工作交给了编译器。
在编译阶段,编译器根据传入的实参类型来推演生成对应类型的函数,比如:当我们传的是int类型的数据,就会将int传给T,就会自动推导并生成相应的函数,其他类型同理。
2.3 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化与显式实例化。
template<T>
Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
// 正确用法
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
// 下面的语句不能通过编译,在编译期间,当编译器实例化时,需要推导实参类型
// 通过a1将T推导为int,通过实参d1将T推导为double,但模板参数列表中只有一个T
// 编译器无法确定该将T推导为什么类型所以会报错
// Add(a1, d2);
// 有两种方法处理:1.我们自己来强制转换类型 2.显示实例化
Add(a1, (int)d2);
return 0;
}
- 2.显示实例化:在函数名后<>中指定模板参数实际类型
int main()
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
2.4 模板参数匹配规则- 一个非模板函数可以和一个同名的函数模板同时存在。
- 对于非模板函数和同名函数模板,如果条件相同情况下会优先调用非函数模板,但如果模板可以产生一个更好匹配的函数,那么将优先调用模板函数。
三、类模板
3.1 类模板语法
template<class T1, class T2, ...>
class 类名
{
// ...
};
template<class T>
class stack
{
public:
Stack(size_t capacity = 4)
:_arr(new T[capacity])
,_capacity(capacity)
,_size(0)
{}
private:
T* _arr;
size_t _capacity;
size_t _size;
};
3.2 类模板的实例化类模板实例化与函数模板不同,类模板实例化需要在类名后加上 <>,将需要实例化的类型放在 <> 指定,类模板名字不是真正的类,而实例化的结果才是真正的类。
int main()
{
// stack是类名,stack<int>才是类型
stack<int> s1;
stack<double> s2;
return 0;
}
四、class 和 typename 的区别4.1 模板参数声明中的 class 和 typename在声明模板类型参数时,class 和 typename 完全等价,可以互换:
template <class T> // √
void func1(T value)
{
// ...
}
template <typename T> // √(更推荐)
void func2(T value)
{
// ...
}
- 历史原因:早期 C++ 使用 class 声明模板参数,但 class 容易让人误解为“必须是类类型”。
- 改进:C++ 标准引入 typename,明确表示“可以是任何类型”(如 int、double 等基础类型)。
- 现代建议:优先用 typename 声明模板类型参数,避免歧义。
4.2 typename 的额外用途typename 有一个特殊用途是 class 无法替代的:在模板中标识“依赖类型” (即类型依赖于模板参数)。
当模板内部访问的嵌套类型依赖于模板参数时,必须用 typename 告诉编译器“这是一个类型”:
template <class T>
class Container
{
public:
// 假设 T 内部有一个嵌套类型 `NestedType`
typename T::NestedType* ptr; // √ 必须用 typename
// class T::NestedType* ptr; // × 编译错误,class 无法替代 typename
};
五、非类型模板参数模板参数分为:
- 类型形参:在模板参数列表中,跟在class或者typename的参数类型名称。
- 非类型形参:用一个常量作为模板的参数,在类(函数)模板中可当作常量使用。
template<typename T, size_t N = 10>
class Array
{
public:
// ...
private:
T arr[N];
size_t _size;
};
注意:
1.浮点数、类对象以及字符串不允许作为非类型模板参数。
2.非类型的模板参数必须在编译期就能确认结果。
六、模板的特化6.1 模板特化的概念通常情况下,模板可以实现一些与类型无关的代码,但对于一些特殊类型可能得到错误的结果,需要特殊处理,比如:实现一个专门用来比较的函数模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 结果正确
int a = 10;
int b = 20;
int* pa = &a;
int* pb = &b;
cout << Less(pa, pb) << endl; // 可以比较,但结果可能错误
}
大多数情况下都可以进行正常比较,但在特殊场景下就会得到错误的结果,在上面代码中,pa指向的对象明显小于pb指向的对象,但是并没有正确得到比较的结果,因为指针在每次运行时都会有不同的地址,所以无法达到预期。
此时就需要对模板进行特化,即:在原模板的基础上,针对某些需要特殊处理的类型进行特殊化的实现方式。模板特化也分为函数模板特化与类模板特化。
6.2 函数模板特化函数模板的特化步骤:
- 先有一个基础的函数模板。
- 关键字template后接一对空的尖括号<>。
- 特化模板函数后跟一对尖括号<>,里面需要指定特化的类型。
- 函数形参表必须要和模板函数的参数完全相同。
cpp
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 模板特化
template<>
bool Less<int*>(int* left, int* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl; // 结果正确
int a = 10;
int b = 20;
int* pa = &a;
int* pb = &b;
cout << Less(pa, pb) << endl; // 调用特化之后的版本
}
但一般情况下如果函数模板达不到我们所预期要的效果,为了实现简单通常是将该函数直接写出。
6.3 类模板特化
6.3.1 全特化
全特化就是将模板参数列表中所有参数都确定化。
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};
int main()
{
Data<int, int> d1; // 走函数模板
Data<int, char> d2; // 走模板特化
return 0;
}
6.3.2 偏特化偏特化就是任何针对模板参数进一步进行条件限制设计的特化版本,偏特化有两种表现方式:
template<class T>
class Data<T, int>
{
public:
Data() { cout << "Data<T, int>" << endl; }
private:
T _d1;
int _d2;
};
- 参数更进一步限制:针对模板参数更进一步的条件限制所设计出来的特化版本。
template<class T>
class Data<T, int>
{
public:
Data() { cout << "Data<T, int>" << endl; }
private:
T _d1;
int _d2;
};
七、模板分离编译一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后通过链接形成单一可执行程序的过程称为分离编译模式。
7.1 模板的分离编译假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,在源文件中完成定义:
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.1, 2.2);
return 0;
}
|