各位老铁们好,我在前面文章分享过函数的模板和类的模板,今天我们在前面那篇文章的基础上再分享一篇关于模板的进阶内容,随着我们学习c++的深入,我们会越来越认识到模板的重要性,所以希望各位老铁能认真看这篇文章,相信你看完一定会有所收获。
今天分享的模板进阶内容主要分为三部分,1.非类型模板参数,2.类模板的特化,3.模板的分离编译
1.非类型模板参数
首先我们来看看一段代码:
template<class T>
class Array
{
public:
private:
T _a[N];
};
int main()
{
Array<int> a1;
}
上面这段代码是一个含有一个数组的类模板,在创建Array类对象时就会开辟出一个数组,但是这个数组大小是不确定的,现在我们想开辟一个大小为100的数组,我们就会发现上面的类模板并不能满足我们创建一个数组大小为100的数组,所以C++引入一个非类型的模板参数。
什么是非类型的模板参数呢?说的通俗点就是一个常量,既然是一个常量我们就可以把他它设置成数组的大小,接下来我们看看下面的带代码
template<class T,int N>//N不是类型参数
class Array
{
public:
private:
T _a[N];
};
int main()
{
Array<int,100> a1;//非类型模板参数是为了固定数组的大小(常量)
return 0;
}
这段代码是不是让我们实现了通过Array类的模板创建出了一个大小为100的整形数组。看到这里可能有老铁就会疑惑了,既然是常量,那么除了整形以外的其他内置类型能不能也充当非类型模板参数呢?那么我们来认证一下吧
template<class T,double N>//N不是类型参数
class Array
{
public:
private:
T _a[N];
};
int main()
{
Array<double,100> a1;//非类型模板参数是为了固定数组的大小(常量)
return 0;
}
看一下执行结果
直接就报错了,说C++20才支持浮点型的非类型模板参数,其实还有不支持字符串类型和类对象,这里我们就不一一演示了,感兴趣的老铁可以自己试一试。
我们知道类和函数模板会在编译阶段就会生成对应的类对象和函数,由于非类型模板参数属于类模板的形参,所以非类型模板参数也会在编译阶段就能确定结果了。
2.模板的特化
在一般情况下,我们使用模板可以实现一些与类型无关的代码,但在遇到一些特殊情况时会出现一些错误的结果,下面我们来看一段比较大小的代码
class Date
{
public:
Date(const size_t year = 0, const size_t month = 0, const size_t day = 1)
:_year(year)
, _month(month)
, _day(day)
{};
bool operator<(const Date& other) const
{
if (_year != other._year) return _year < other._year;
if (_month != other._month) return _month < other._month;
return _day < other._day;
}
private:
size_t _year;
size_t _month;
size_t _day;
};
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl;
return 0;
}
结果没有任何问题
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
cout << Less(d1, d2) << endl;
return 0;
}
结果也没有任何问题
接下来我们再看看这个
int main()
{
Date d1(2022, 7, 8);
Date d2(2022, 7, 7);
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
结果错误,这是为什么呢?其实p1和p2并不是在比较它们指向的内容而是比较p1和p2本身的地址。
由此可见模板对于一些特殊类型可能会出现结果错误。
因此出现了模板特化,在原模板的基础上,对于一些特殊的类型进行特殊化实现方式,模板有函数模板和类模板,那么模板特化也有函数模板特化和类模板特化。
那么该如何将函数和类模板进行特化呢?还是以上面代码为例子。
函数模板特化
//1.先有一个基础的函数模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//2.在template关键字后面加一个<>
template<>
//3.函数名后需要跟一个<>尖括号后面需要加"需要特化成的类型"
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
Date d1(2022, 7, 8);
Date d2(2022, 7, 7);
Date* p1 = &d1;
Date* p2 = &d2;
cout << Less(p1, p2) << endl;
return 0;
}
上面的代码就是将函数模板进行特殊化,把参数限定成是Date的类型,参数就只能传Date类型了,我们看看代码结果是否正确。
代码结果正确!
类模板特化
特化也分为完全特化和偏特化
完全特化(全特化通常用于非常具体的类型组合):将模板列表中所以参数都确定化
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;
}
偏特化(用于处理具有某种共性的类型):针对模板参数进一步条件限制
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
3.分离编译
项目工程中一般将函数或者类的声明放到.h文件中,将函数/类的定义放到.cpp中,让每个原文件进行单独的编译,最后将所有的目标文件链接起来形成单一的可执行文件的过程就是分离编译。
模板的分离编译:
如果我们把模板的声明和定义分离开来,在.h文件中声明,在.cpp文件中定义,那么在编译阶段,.cpp和main.cpp文件是分开编译的,所以在.cpp文件中并没有找到函数模板的实例化,因此不会生成具体的函数,main.obj文件是在链接阶段才会去找其地址,但由于并没有生成具体的函数,所以无法找到,编译器就会报链接错误。
那么我们该如何解决这个问题呢?
首先第一解决方法:把模板的声明和定义放到同一个文件中(推荐使用)
第二种解决方法 :直接把模板进行显示实例化(不常用)
推荐大家去看看这篇文章:
总结:
模板对代码进行了复用,节约了资源,提高了效率,STL因此产生,模板也增强了代码的灵活性。
模板同时会导致代码膨胀,导致编译时间过长,如果出现代码错误,不易定位错误。