模版进阶
一、非类型模版参数
模版参数分为类型形参 与非类型形参
类型形参:出现在模版参数列表中,跟在class或者typename之类的参数类型名称
非类型形参:用一个常量作为类模板的一个参数,在类模板中可将该参数当成常量来使用
cpp
namespace little_monster
{
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index)
{
return _array[index];
}
const T& operator[](size_t index) const
{
return _array[index];
}
size_t size() const
{
return _size;
}
bool empty() const
{
return 0 == _size;
}
private:
T _array[N];
size_t _size;
};
}
浮点数、类对象和字符串是不允许作为非类型模版参数的
非类型的模版参数必须在编译期就能确认结果
这里就是允许给类模板定义常量参数,可以在类模板中使用
二、模版的特化
1、概念
通常情况下,使用模版可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理
cpp
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
template<class T>
bool Less(T left, T right)
{
return left < right;
}
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
void test1()
{
cout << Less(1, 2) << endl;
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
}
可以看到,在test1测试函数中,第一组是内置类型进行比较,第二组是自定义类型进行比较,这两种得出的结果都是正确的,而第三种是指针进行比较,但是Less函数模版认为它们是两个数字,就会造成结果可能不对的现象,因为比较的是地址而不是指向的对象,此时就需要对模版进行特化,就是在原模版类的基础上,针对特殊类型进行特殊化的实现方式,模版特化分为函数模版特化和类模板特化
2、函数模版特化
函数模版特化必须先有一个基础的函数模版,格式是在关键字template后边只加<> ,但是函数名后跟<> ,在该<>中指定需要特化的类型 ,函数的形参必须要和模版参数的基础参数类型完全相同
cpp
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
但是对于函数模版的特化,失去了函数模版的灵活性,使得其优势荡然无存,所以我们在遇到这种情况的时候一般直接定义一个函数就可以,写起来也更方便,代码可读性也更高
cpp
bool Less(Date* left, Date* right)
{
return *left < *right;
}
所以我们不建议函数模版特化,这里只是作为一个引子来讲一下类模板的特化,类模板的特化是很有必要的,是十分有价值的
3、类模版特化
(1)全特化
全特化就是将模版参数列表中所有的参数都确定化
cpp
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;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;
}
(2)偏特化
任何针对模版参数进一步进行条件限制设计的特化版本
cpp
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
①部分特化
cpp
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout << "Data<T1, int>" << endl;
}
private:
T1 _d1;
int _d2;
};
②参数进一步的限制
cpp
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _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 test2()
{
Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(1, 2); // 调用特化的指针版本
}
我们可以看到类模版特化与函数模版特化相比,类模板特化有广阔的使用空间,相比之下函数模版特化显得十分鸡肋,所以我们通常建议进行类模板特化而不建议函数模版特化
三、模版分离编译
一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程叫做分离编译模式
在之前的学习C语言的过程中,我们通常在写一个某些功能的模拟实现时,会将声明写在头文件中,定义写在源文件中,声明与定义分离就是一种分离编译
一般C/C++程序运行的步骤为:
预处理-->编译-->汇编-->链接
预处理 过程就是对程序进行提前处理,包括展开头文件、宏定义的替换等
编译 过程就是对程序按照语言特性进行词法、语法、语义分析检查无误后生成汇编代码(头文件不参与编译,编译的过程是每个文件单独的)
汇编 过程就是翻译汇编指令,生成二进制机器码obj文件、准备链接
链接 过程就是将多个.obj文件合成为一个,处理地址问题,这里的地址问题指的就是在编译完成后,模版函数进行实例化,会形成具体的函数,在链接过程中寻址然后进行链接,但是模版分离编译会导致模版函数不会进行实例化,导致链接时找不到地址而报错
解决办法就是声明和定义都在一个.h文件当中,这也是最好的一种方式
四、对于模版的总结
优点:模版复用了代码,节省资源,更快的迭代开发,C++的STL也因此产生
增强了代码的灵活性
缺点:模版会导致代码膨胀问题,也会导致编译时间变长
出现模版编译错误时,错误信息凌乱,不易于定位错误
五、必须使用typename的情况
经过前面的学习,当我们要使用模版时一般来说使用class和typename都是一样的
cpp
template <class T>
template <typename T>
但是有的情况下,是不能使用class而必须使用typename的
1、依赖类型
当模板类型参数用于指定另一个类型的成员类型时,如果这种类型关系依赖于模板参数,则必须使用typename来指明这是一个类型,这是因为编译器在解析模板时可能无法立即确定某个名字是指代类型还是非类型(如静态成员变量或枚举值),而typename告诉编译器该名字是一个类型
cpp
//实例化
template<typename T>
//template<class T>
void print_list(const list<T>& lt)
{
typename list<T>::const_iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
list< T >未实例化的类模板,编译器不能去到里面去找list< T >::const_iterator ,就无法知道list< T >::const_iterator是内嵌类型还是静态成员变量,前面加一个typename就是告诉编译器,这里是一个类型,等list< T >实例化再去类里面去找
cpp
void test1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
print_list(lt);
}
一个改良版,就是模版的作用,这样所有支持迭代器的类型的数据就可以打印出来了,这就是模版的最大用途,泛型编程的本质,减少我们的工作,将繁琐的工作交给编译器
cpp
template<typename Container>
void print_container(const Container& con)
{
typename Container::const_iterator it = con.begin();
while (it != con.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
cpp
void test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
print_container(v);
}
2、模板模板参数中的类型成员
当模板参数本身也是一个模板,并且你需要引用这个模板参数模板中的类型成员时,也需要使用typename
cpp
template<template<typename> class Container, typename T>
class little_monster
{
public:
typename Container<T>::iterator begin()
{
// ...
}
};
第一个参数Container是一个模版模版参数,它用来接受一个模版类,第二个参数指定了Container中将要存储的元素类型,换句话说,T定义了little_monster类内部容器将包含哪种类型的对象
cpp
template<typename T>
using List = std::list<T>; // 创建一个只接受一个类型参数的别名
// 现在我们可以这样使用little_monster
little_monster<List, int> monster; // List<int>相当于std::list<int>
//上面这样就相当于对下面的进行封装,上面的包含下面这样的一个成员变量
list<int> monster;
今日分享完毕~