继上文
非类型模板参数:
- 类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
template<class T>
- 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
template<size_t N=10>
在此,N是一个常量,在编译器编译的时候就确定了,比起宏:
#define N 10;
来说,因为宏其实是写死的,而对于非类型模板参数,使用非类型模板参数就可以灵活使用
以下示例:
#include<iostream>
using namespace std;
namespace rose
{
template<class T, size_t N = 10>
class Stack
{
private:
int _a[N];
int _top;
};
}
int main()
{
rose::Stack<int, 5> s1;
rose::Stack<int,10> s2;
return 0;
}
本质还是底层生成了两个类,一个生成的是N=5,一个生成的是N=10
但是:非类型模板只能用于“整型”('int' 'short int' 'long long' 'size_t' 'bool' 'char'.....),像浮点型(double)是不支持非类型模板参数,C++20后就支持浮点型作为非类型模板参数
我们通过以上代码,我们的非类型模板参数也可以传缺省值,不过我们应该注意:
namespace rose
{
template<size_t N = 10>
class Stack
{
private:
int _a[N];
int _top;
};
}
int main()
{
rose::Stack<> s0;
rose::Stack s00;//只有C++20过后才可以怎么写
return 0;
}
因为C++具有向前兼容的特点,所以我还是推荐使用前者<>;
模板的特化:
- 必须要先有一个基础的模板
- 关键字template后面接一对空的尖括号<>(偏特化只空特定的要特化的对象)
- 函数名/类名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板:
日期类的代码:here
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确using namespace std;
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; // 可以比较,结果错误
return 0;
}
在C++中,模板是一种允许你编写通用代码的方式,这些代码可以用于不同的数据类型。然而,有时候你可能想要为特定的数据类型提供特定的实现。这就是模板特化发挥作用的地方。
模板特化是告诉编译器:“对于这个特定的类型(或类型组合),我想要一个不同的实现。” 这可以通过两种方式实现:类模板特化和函数模板特化。
函数模板的特化:
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
注意:
bool Less(T left, T right);
严格来说,在这个代码中写T是不好的,如果是内置类型还好,但如果是自定义类型的话,这个地方就会调用拷贝构造,应该这么写:
bool Less(const T& left, const T& right);
那么,我们的函数模板特化就应该改为:(因为没有与改变后的模板进行完美的匹配)
那我们改成这样如何呢:
template<>
bool Less<Date*>(const Date*& left, const Date*& right)
{
return *left < *right;
}
不过可惜了,他会报错!问题出在哪呢?出在了const身上:
- 在模板中,const修饰的是引用本身(left和right);
- 在函数模板特化中,const修饰的是的并不是left/right,修饰的是在*之前,修饰的是指向的内容,相当于是修饰*left/*right,而又template<Date*>(特化的是Date*)(是个指针,是要const的是本身,即指针本身)我们应该改成:
bool Less<Date*>(Date* const& left, Date* const& right)
{
return *left < *right;
}
如果main函数中改为:
const Date* p1 = &d1;
const Date* p2 = &d2;
对应的,我们的函数模板特化就应该写成:
template<>
bool Less<const Date*>(const Date* const& left, const Date* const& right)
{
return *left < *right;
}
函数模板特化的使用场景:
- 类型特定的行为:当你想要为特定类型提供不同的处理逻辑时。
- 性能优化:为特定类型提供优化的算法实现。
- 兼容性:确保函数与特定类型的库函数或操作兼容。
总之:不建议使用,坑比较多(指针与const的关系)
对于函数模板:(推荐)不使用特化的方式,可以使用函数的方式:
(有函数模板和现成的,优先用现成的)
bool Less(Date* left, Date* right)
{
return *left < *right;
}
- 函数模板特化必须在模板函数的原始定义之外进行。
- 特化版本会覆盖通用模板的实现,因此在使用时应确保特化是你所期望的。
- 函数模板特化可以提高代码的灵活性和性能,但也可能增加代码的复杂性。
类模板的特化:
类模板特化是为特定的类型提供一个定制的类定义。这通常用于优化性能或提供类型特定的功能。
全特化:
全特化通常用于以下情况:
- 当你想要为特定的类型提供完全不同的行为或数据结构。
- 当你想要优化特定类型的性能。
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;
}
使用全特化的注意事项
- 全特化必须在模板的原始定义之外进行。
- 全特化可以提高代码的灵活性和性能,但也可能增加代码的复杂性。
- 全特化应该谨慎使用,只在确实需要为特定类型提供完全不同的行为时使用。
偏特化/半特化:
- 部分特化:将模板参数类表中的一部分参数特化
//类模板的偏特化/半特化
template<class T1>
class Data<T1, char>
{
public:
Data() { cout << "Data<T1, char>" << endl; }
private:
T1 _d1;
char _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); // 调用特化的指针版本
}
全杀,只要是指针/引用,不管是int还是double通通用(针对指针,引用的特殊处理)
注意事项
- 偏特化必须在模板的原始定义之外进行。
- 偏特化可以与全特化共存,但全特化会优先于偏特化。
- 偏特化应该谨慎使用,以避免过度复杂化代码。
全特化与偏特化的区别:
- 全特化:提供了一个完全定制的实现,覆盖了所有通用模板的实现。
- 偏特化:提供了一个定制的实现,但只针对特定的参数组合,其他参数仍然使用通用模板的实现。
在C++中,如果同时存在函数模板的全特化和偏特化,编译器将根据模板参数匹配的具体情况来决定使用哪一个特化版本。但是,通常情况下,全特化(完整特化)具有更高的优先级,因为它提供了更具体的类型匹配。
优先级规则:
-
全特化:如果存在一个全特化的模板,它完全匹配了调用的类型参数,编译器将优先使用这个全特化版本。
-
偏特化:如果不存在完全匹配的全特化,编译器将查找偏特化版本。如果有多个偏特化版本,编译器将选择最具体的(即模板参数列表中特化数量最多的)偏特化版本。
-
通用模板:如果没有匹配的特化版本,编译器将回退到使用通用模板。
类模板特化应用示例:
有如下专门用来按照小于比较的类模板Less:
#include<vector>
#include<algorithm>
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 可以直接排序,结果是日期升序
sort(v1.begin(), v1.end(), Less<Date>());
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序
// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象
// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期
sort(v2.begin(), v2.end(), Less<Date*>());
return 0;
}
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
如果不是Date*而是int*时,我们为了更加方便,就可以:(偏特化)
// 对Less类模板按照指针方式特化
template<class T>
struct Less<T*>
{
bool operator()(T* x, T* y) const
{
return *x < *y;
}
};
模板分离编译 :
模板的分离编译(Separate Compilation with Templates)是C++中一种重要的编译策略,它允许模板的定义和实例化分开在不同的编译单元(Translation Units,简称TU)中进行。这种策略可以提高编译效率,减少编译时间,并允许更好的代码组织。
也就是说一般的类·函数都要声明和定义分离,但是模板就不支持,因为会导致链接链不上:
(声明放在.h 定义放在.cpp)
// a.h
template<class T>
T Add(const T& left, const T& right);
int Func(const int& left, const int& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int Func(const int& left, const int& right)
{
return left + right;
}
// Test.cpp
#include"a.h"
int main()
{
Add(1, 2);//call Add(?)
Add(1.0, 2.0);//call Add(?)
Func(1, 2);//call Func(?)
return 0;
}
为什么会链接错误呢?
编译过程通常包括以下几个主要阶段:(Linux下)(a.h a.cpp Test.cpp)
-
预处理(Preprocessing):
这个阶段处理源代码文件中的预处理指令,如宏定义(#define
)(宏替换)、文件包含(#include
)(其中有头文件展开(.h))、条件编译、去掉注释等。(a.h->a.i Test.cpp->Test.i) -
编译(Compilation):
编译器将预处理后的源代码转换成汇编语言。这个阶段涉及到语法分析、语义分析、中间代码生成等步骤。(语法错误的出处)(a.i->a.s Test.i->Test.s) -
汇编(Assembly):
将汇编语言转换成二进制的机器码。汇编器读取编译生成的汇编代码并生成目标文件(Object File),包含机器指令和符号表等信息。(符号·指令变为二进制的机器码)(a.s->a.o Test.s->Test.o) -
链接(Linking):
链接器将多个目标文件以及库文件链接在一起,生成可执行文件或库文件。链接过程解决外部符号引用问题,将分散在不同文件中的代码和数据整合到一起。(目标文件合并在一起生成可执行程序,并且把需要的函数地址等链接上)(xxx.exe/ a.out)
在链接之前,文件之间(Func Test)是没有交互的,在Test.s生成的时候,汇编语言就变成了
call Add(?)
call ADD(?)
call Func(?)
然后声明和定义分离的,要call对应的函数地址,而符号表就存了当前所有函数的对应的地址,链接不上是因为Func()找到了都对应的地址,而Add()去找的话,找不到,(因为在此之前(编译)有声明,所以在链接之前就让通过了)为什么找不到呢?
因为Add作为一个模板,并没有实例化(链接之前不互相交互),模板没有实例化是生成不了对应的指令的,也就是在符号表里找不到对应的Add(),因此,我们就可以使用显示实例化到达模板的声明和定义分离了:
//显式实例化
template//为了和特化进行区分,不加<>
int Add(const int& left, const int& right);
double Add(const double& left, const double& right);
//不过太龊了
所以,模板的解决方案就是在.h中直接定义(在编译时就有定义了,就直接实例化,就可以call到对应的地址了)
总结:
- 【优点】
模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
增强了代码的灵活性
- 【缺陷】
模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误