目录
泛型编程
产生原因:当我们试图实现一个通用的交换函数,我们会重载多个接收不同参数类型的swap函数,但这样会使得代码复用率低,只要有新类型出现,就需要增加新的重载函数
基本概念:泛型编程是一种编程范式,它允许编写可以处理不同类型数据的代码,而无需针对每种数据类型都重复编写相同的逻辑,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;
}
......
函数模板
基本概念:表示了同一类功能的函数,在使用时该模板会根据实参类型产生函数的特定类型版本
格式:
template <typename T1,typename T2......typename Tn>
返回值类型 函数名(参数列表){函数体}
template <class T1,class T2......class Tn>
返回值类型 函数名(参数列表){函数体}
#include <iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 2;
Swap(a, b);
cout << a <<" "<< b << endl;
double d1 = 1.1, d2 = 2.2;
Swap(d1, d2);
cout << d1 <<" "<< d2 << endl;
return 0;
}
注意事项:
1、typename
和 class
都可以用于定义模板参数T,它们在模板声明中常常是等价的,但也有一些细微的区别,使用嵌套类型时,必须使用 typename,
因为 class
不能在这种上下文中使用
template <typename T>
void func(typename T::value_type val)
{
// 使用 T 的嵌套类型 value_type
}
T::value_type
是依赖于模板参数T
的类型。为了告诉编译器value_type
是一个类型而不是其他东西,需要使用typename
2、函数模板是一个蓝图,不是函数,模板就是会将本来应该程序员要做的重复的事情交给编译器
函数模板的实例化
基本概念:用不同类型的参数使用函数模板时,模板函数会被实例化出不同的函数
分类:隐式实例化(编译器根据实参自行推演实例化出的函数形参类型,但可能会出错)、显式实例化(在函数名后使用<>指定模板参数的实际类型)
#include <iostream>
using namespace std;
template<class T>
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);//正确
Add(a1, d1);//错误
return 0;
}
运行结果:编译错误,编译器在推演第三个要实例化出的Add函数时,实参a1将T推演为int,实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者double类型而报错
问题:为什么编译器不能将其中一个类型转化为另一个呢?这样不是就不会出错了吗?
解释:在涉及模板的操作中,编译器一般不会进行类型转换,因为一旦转化出问题,编译器就需要背黑锅,为了避免隐式实例化编译器无法正确推断类型的问题,需要用户进行显式实例化或者提前将进行类型的强制转换
//用户自行强转类型 Add((double)a1, d1); Add(a1, (int)d1); //显式实例化 Add<int>(a, b); Add<double>(a,b);
注意事项:模板函数不允许自动类型转换,因为编译器要求模板参数的类型必须精确匹配,以确保类型安全性,而普通函数可以进行自动类型转换,为了提升代码的灵活性
模板参数的匹配原则
1、同名的非模板函数和函数模板可以同时存在
#include <iostream>
using namespace std;
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int Add(const int& left, const int& right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
double d1 = 1.1, d2 = 2.2;
Add(a, b);
Add(d1, d2);
return 0;
}
2、如果其它条件相同,优先调用非模板函数而不是模板函数,如果模板可以产生一个具有更好匹配的函数,那么就选择模板
#include <iostream>
using namespace std;
template<typename T>
T Add(const T& left, const T& right)
{
return left + right;
}
int Add(const int& left, const int& right)
{
return left + right;
}
int main()
{
int a = 1, b = 2;
double d1 = 1.1, d2 = 2.2;
Add(a, b);//调用自定义的普通函数
Add(d1, d2);//调用函数模板
return 0;
}
类模板
格式:
template<class/typename T1,class/typename T2>//typename和class均能定义模板类型
class 类模板名
{
//类内成员定义
}
#include <iostream>
using namespace std;
template <class T>
class Stack
{
public:
void Push(const T& x)
{}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
//同一类模板实例化出的两个类型
Stack<int> st1;//栈中的数据类型实例化成int
Stack<double> st2;//栈中的数据类型实例化成double
return 0;
}
注意事项:
1、类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<T的类型>
2、 类模板中的函数放在类外进行定义时,需要加模板参数列表,但一般不会这样干
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
~Vector();
void PushBack(const T& data);
void PopBack();
size_t Size() { return _size; }
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
template <class T>
Vector<T>::~Vector()
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
3、类模板的声明和定义不建议声明和定义分离,因为未实例化的模板都是一个逻辑符号没有实际代码,同时需要注意的是类实例化发生在编译阶段,假设将A类的声明放在A.h、定义放在A.cpp、然后在B.cpp中要实力化一个A类对象(两个cpp文件均包含了头文件但并未互相包含),在编译阶段遇到B.cpp中的实例化请求时会去寻找A类的定义,但此时是编译阶段不是链接阶段,所以不能看到A.cpp中A类的定义,最终报错
显示实例化
基本概念:显式实例化是指,在某些特定情况下,程序员手动告诉编译器要生成特定类型的模板实例从而避免不必要的模板实例生成(在某个 .cpp
文件中明确指定生成某些类型的模板实例),减少模板的冗余编译(也是实现类模板的声明和定义分离的一种方式)
声明和定义的语法(函数模板类似):
//显式实例化定义的语法
template class ClassName<T>;
//显式实例化声明的语法
extern template class ClassName<T>;
- 指示编译器生成
ClassName<T>
的模板实例,一般用于.cpp
文件,表示模板的特定类型实例应在此文件中生成
编译器的查找过程
基本概念:在理解模板声明和定义分离的查找过程之前,我们需要先了解模板的实例化过程分为 声明(只有各种成员的名字)、使用(要使用模板实例化对象的请求) 和 定义(模板中成员的具体实现) 的三个主要阶段
承接上面分析模板定义和声明分离的例子:在
A.h
中声明类模板A
,在A.cpp
中为特定类型(例如A<int>
)提供显式实例化定义。当编译器在编译B.cpp
时,遇到对A<int>
的实例化请求,它会根据A.h
中的声明和extern template class A<int>;
(显式实例化声明),知道A<int>
的代码已经在A.cpp
中生成。因此,编译器不会尝试再次实例化A<int>
,而是等待链接阶段使用A.cpp
中已经生成的实例化代码。这样,模板的定义可以安全地与声明分离,而不会在编译时出错
显式实例化类模板
// MyTemplate.h
#ifndef MYTEMPLATE_H
#define MYTEMPLATE_H//条件编译,遇到#include "MyTemplate.h"才会展开下面的内容
#include <iostream>
template<typename T>
class MyTemplate {
public:
MyTemplate(T value);
void display() const;
private:
T data;
};
// 显式实例化声明
extern template class MyTemplate<int>;
#endif // MYTEMPLATE_H
// MyTemplate.cpp
#include "MyTemplate.h"
template<typename T>
MyTemplate<T>::MyTemplate(T value) : data(value) {}
template<typename T>
void MyTemplate<T>::display() const {
std::cout << data << std::endl;
}
// 显式实例化定义
template class MyTemplate<int>;
MyTemplate.h:
包含模板的声明和会进行外部模板实例化声明extern template class MyTemplate<int>
(
表示MyTemplate<int>
的实例化将在其他地方完成)
MyTemplate.cpp
:包含模板的定义和MyTemplate<int>
的显式实例化定义template class MyTemplate<int>
(
指示编译器在这个.cpp
文件中生成MyTemplate<int>
的代码)
- 这样,当你在其他地方使用
MyTemplate<int>
时,不会再次实例化int
类型,编译器只会使用MyTemplate.cpp
中生成的实例化代码
显式实例化函数模板
// MyFunction.h
template<typename T>
void printValue(T value);
// 声明显式实例化声明
extern template void printValue<int>(int);
// MyFunction.cpp
#include "MyFunction.h"
#include <iostream>
template<typename T>
void printValue(T value) {
std::cout << value << std::endl;
}
// 显式实例化定义
template void printValue<int>(int);
优缺点
优点:
- 减少重复实例化,提升编译速度:通过显式实例化,你可以避免多个文件对同一个模板进行重复实例化,从而加快编译速度。
- 隐藏实现细节:你可以将模板的实现放在
.cpp
文件中,而头文件中只保留接口声明,避免暴露实现细节。- 控制代码大小:只为特定类型实例化模板,可以减少二进制文件大小,避免不必要的代码膨胀。
- 提高灵活性:你可以精确控制哪些类型可以实例化模板,限制不必要的类型使用。
缺点:
- 灵活性降低:显式实例化意味着你需要提前决定哪些类型可以使用模板,如果后来需要支持新的类型,必须重新修改代码。
- 维护难度增加:在大型项目中,显式实例化可能增加维护的复杂性,因为你需要跟踪每个模板的实例化情况。
- 不适合广泛使用的模板:如果模板是一个通用的库组件,显式实例化可能过于繁琐,不如直接放在头文件中来得方便。
补充:显示实例化只是支持类模板声明和定义分离的方式之一,Pimpl 技术、C++20的模块化编程等都可以实现这一操作
~over~