深入理解C++模板
- typename和class的区别
- 非类型模板参数
- 模板的特化
- 函数模板特化
- 类模板特化
- 全特化
- 偏特化
- 模板分离编译
- 模板的分离编译
- 解决方法
- 总结
- 🍀小结🍀
🎉博客主页:小智_x0___0x_
🎉欢迎关注:👍点赞🙌收藏✍️留言
🎉系列专栏:C++初阶
🎉代码仓库:小智的代码仓库
typename和class的区别
在C++中,typename
和class
关键字都可以用于模板参数声明,它们的作用是相同的,用于指定一个类型参数。但是,在一些情况下,typename
比class
更加灵活。
首先,当模板参数是一个嵌套类型的时候,必须使用typename
关键字来告诉编译器这是一个类型而不是一个静态成员变量或者函数。例如:
template<class Container>
void Print(const Container& v)
{
// 编译不确定Container::const_iterator是类型还是对象
// typename就是明确告诉编译器这里是类型,等模板实例化再去找
typename Container::const_iterator it = v.begin(); //必须使用 typename 关键字
auto it = v.begin();//当然使用auto就不会有以上的各种问题
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
【补充】:当模板参数是一个模板类型的时候,必须使用class
关键字来指定这个模板参数。例如:
template <class T, template <class> class Container>
class MyClass {
public:
Container<T> c;
};
在这个例子中,Container
是一个模板类,它接受一个类型参数,因此必须使用class
关键字来指定。
总的来说,虽然在大多数情况下typename
和class
可以互换使用,但是在一些特殊情况下,必须使用其中的一个关键字来保证代码正确性。
非类型模板参数
- 模板参数分类类型形参与非类型形参。
- 类型形参即:出现在模板参数列表中,跟在
class
或者typename
之类的参数类型名称。 - 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
静态栈是一种使用数组实现的栈,它的大小在编译时确定。我们可以使用非类型模板参数来指定静态栈的大小。下面是一个简单的静态栈的实现:
template <typename T, int N>
class StaticStack {
public:
void push(const T& value) {
if (size_ < N) {
data_[size_++] = value;
} else {
cout<<"StaticStack is full"<<endl;
}
}
void pop() {
if (size_ > 0) {
--size_;
} else {
cout<<"StaticStack is empty"<<endl;
}
}
T& top() {
if (size_ > 0) {
return data_[size_ - 1];
} else {
cout<<"StaticStack is empty"<<endl;
}
}
bool empty() const {
return size_ == 0;
}
int size() const {
return size_;
}
private:
T data_[N];
int size_ = 0;
};
在这个例子中,StaticStack
是一个模板类,它接受两个参数:T
表示栈中元素的类型,N
表示栈的大小。data_
是一个长度为N
的数组,用于存储栈中的元素。其他成员函数实现了栈的基本操作。
我们可以使用这个静态栈来存储任何类型的数据,并且在编译时指定栈的大小。例如,下面的代码创建了一个能够存储10个整数的静态栈:
StaticStack<int, 10> s;
s.push(1);
s.push(2);
s.push(3);
std::cout << s.top() << std::endl; // 输出 3
s.pop();
std::cout << s.top() << std::endl; // 输出 2
在这个例子中,我们使用了非类型模板参数10
来指定静态栈的大小。这使得我们可以在编译时就确定静态栈的大小,从而避免了动态分配内存的开销,提高了代码的效率。
【注意】:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
模板的特化
在C++中,模板的特化是指为某些特定的模板参数提供一个专门的实现。模板特化可以用于优化代码、解决特殊情况下的问题等。
模板特化有两种形式:完全特化和部分特化。完全特化是指为某些特定的模板参数提供一个完全不同的实现,而部分特化是指为某些特定的模板参数提供一个更为通用的实现。
函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 函数模板的特化
template<>
bool Less<int*>(int* left, int* right)
{
return *left < *right;
}
但是一般函数遇到需要特化的情况可以直接重载:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<class T>
bool Less(T* left, T* right)
{
return *left < *right;
}
int main()
{
cout << Less(1, 2) << endl;
int a = 1, b = 2;
cout << Less(&a, &b) << endl;
double c = 1.1, d = 2.2;
cout << Less(&c, &d) << endl;
return 0;
}
类模板特化
全特化
全特化是指为某些特定的模板参数提供一个完全不同的实现。在C++中,全特化是指为所有的模板参数提供一个专门的实现。全特化可以用于优化代码、解决特殊情况下的问题等。
下面是一个使用全特化的例子:
template <typename T>
class MyClass {
public:
void print() {
std::cout << "Generic implementation" << std::endl;
}
};
template <>
class MyClass<int> {
public:
void print() {
std::cout << "Specialized implementation for int" << std::endl;
}
};
template <>
class MyClass<double> {
public:
void print() {
std::cout << "Specialized implementation for double" << std::endl;
}
};
int main() {
MyClass<char> c1;
c1.print(); // 输出 "Generic implementation"
MyClass<int> c2;
c2.print(); // 输出 "Specialized implementation for int"
MyClass<double> c3;
c3.print(); // 输出 "Specialized implementation for double"
return 0;
}
在这个例子中,MyClass
是一个模板类,我们为MyClass<int>
和MyClass<double>
提供了专门的实现。当我们创建一个MyClass<int>
或者MyClass<double>
对象时,它会调用对应的专门实现;而对于其他类型的对象则会使用通用的实现。
需要注意的是,在使用全特化时,必须使用空的模板参数列表来表示这是一个特化版本。例如,上面例子中的template <> class MyClass<int>
和template <> class MyClass<double>
就是空的模板参数列表。
总的来说,全特化可以使得模板更加灵活和通用,同时也可以优化代码性能。但是,在使用全特化时需要谨慎,避免过度使用导致代码难以维护。
偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类:
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
偏特化有两种表现方式:
- 部分特化
将模板参数类表中的一部分参数特化。
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
- 参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版
本。
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
void test()
{
Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
模板分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链
接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
//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;
}
//Test.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
我们以上面这个例子来测试一下模板分离编译>
可以发现这里报错了。
解决方法
- 将声明和定义放到一个文件
xxx.cpp
里面或者xxx.h
其实也是可以的。推荐使用这种。 - 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
template int Add<int>(const int& left, const int& right);//显示实例化
template double Add<double>(const double& left, const double& right);
这样实例化之后就可以正常编译运行了。
总结
【优点】:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
【缺点】:
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
🍀小结🍀
今天我们学习了深入理解C++模板
相信大家看完有一定的收获。
种一棵树的最好时间是十年前,其次是现在!
把握好当下,合理利用时间努力奋斗,相信大家一定会实现自己的目标!加油!创作不易,辛苦各位小伙伴们动动小手,三连一波💕💕~~~
,本文中也有不足之处,欢迎各位随时私信点评指正!
本篇代码已上传gitee仓库