240604 模板进阶
模板进阶
1. 类模板的非类型模板参数
模板参数分为:
(1) 类型形参:出现在模板参数列表中,跟在class或typename之类的参数类型名称
(2)非类型形参:用一个常量作为类(函数)模板的一个参数,在模板中可将其当作常量使用
假设我们想要创建一个具有固定大小的栈,可以使用非类型模板参数来指定栈的最大尺寸:
#include <array>template <typename T, std::size_t Maxsize>
class Stack {
private:std::array<T, Maxsize> elems; // 元素std::size_t numElems; // 当前元素数量public:Stack() : numElems(0) {}void push(T const &elem); // 压入元素void pop(); // 弹出元素T const &top() const; // 返回顶部元素bool empty() const { return numElems == 0; } // 是否为空std::size_t size() const { return numElems; } // 当前元素数量
};// 实现 push 方法
template <typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(T const &elem) {assert(numElems < Maxsize);elems[numElems] = elem;++numElems;
}
类模板没有实例化时,编译器不会去里面查细节,无法确定里面是类型还是静态变量
加typename
明确告诉是类型
template<class T>
void PrintVector(const vector<T>& v) {typename vector<T>::const_iterator it = v.begin();while (it != v.end()) {cout << *it << " ";++it;}cout << endl;
}
或者直接auto it = v.begin()
2. 函数模板的专用化/特化
仅作了解,不太实用
原模板:
template<class T>
bool Less(const T& l, const T& r){return l < r;
}
以日期类举例,原模板专用化后:
template<>
bool Less<Date*>(Date* const& l, Date* const& r) {return *l < *r;
}
其他类型都正常走原模板,Date* 走专用化的模板
坑多,一般不建议使用函数模板的专用化
推荐做法:
template<class T>
bool Less(T l, T r){return l < r;
}bool Less(Date* l, Date* r){return *l < *r;
}
3. 类模板的特化
如果不使用函数模板的特化,更推荐的做法是使用函数重载来处理不同类型的情况。函数重载比模板特化更直观和灵活,在类型明确的情况下可以更好地表达意图。可以通过为特定类型(如Date*)编写专门的重载函数,而不需要对模板进行专门化。
(1)全特化:
template<class T1, class T2>
class Show {
public:Show() {cout << "Show<T1, T2>" << endl;}
private:T1 _a;T2 _b;
};
// 类模板的特化:
template<>
class Show<int, char> {
public:Show() {cout << "Show<int, char>" << endl;}
};
(2)部分特化示例1:
// 偏特化:特化部分参数
template<class T1>
class Show<T1, int> {
public:Show() {cout << "Show<T1, int>" << endl;}
private:T1 _a;int _b;
};// 偏特化:限定模板类型
// 涉及指针走下面,T1和T2对应的是原类型(不是指针)
template<typename T1, typename T2>
class Show<T1*, T2*> {
public:Show() {cout << "Show<T1*, T2*>" << endl;}
};
(3)部分特化示例2:
实现自定义的比较器,用于指针类型之间的比较。
通常用于需要比较两个对象(通过指针)的场合:
template<class T>
class Less<T*>{
public:bool operator()(T* const& x, T* const& y){return *x< *y;}
}
在不同的情况下,T 就是指针所指向的具体类型。例如:
Date* 对应 T = Date
int* 对应 T = int
std::string* 对应 T = std::string
部分特化/偏特化 应用较多
4. 模板分离编译的问题
在C++中,当你在头文件中声明一个模板函数,并在源文件(.cpp文件)中实现它时,可能会遇到模板函数无法成功调用的问题。以下是原因及其解释:
(1)原因分析
-
模板的实例化要求:模板函数需要在调用时能够看到其完整的定义。由于模板是基于类型参数生成的,每次使用模板时,编译器都需要知道模板的具体实现。如果实现放在
.cpp
文件中,而没有显式地让编译器知道该模板的定义,就会导致编译错误。 -
普通函数的处理:普通函数(非模板函数)的定义在
.cpp
文件中,编译器在链接时能够找到它的实现,因此可以被成功调用。
(2)示例代码
假设有以下的文件结构:
func.h
#ifndef FUNC_H
#define FUNC_Htemplate <typename T>
void templateFunction(T value); // 模板函数的声明void regularFunction(); // 普通函数的声明#endif
func.cpp
#include "func.h"
#include <iostream>template <typename T>
void templateFunction(T value) { // 模板函数的实现std::cout << "Template value: " << value << std::endl;
}void regularFunction() { // 普通函数的实现std::cout << "Regular function called." << std::endl;
}// 显式实例化
template void templateFunction<int>(int); // 显式实例化模板
test.cpp
#include "func.h"int main() {regularFunction(); // 调用普通函数templateFunction(10); // 调用模板函数return 0;
}
(3)解决方案
要使模板函数在 test.cpp
中成功调用,通常有以下几种方法:
-
将模板函数的实现放在头文件中:
将templateFunction
的实现直接放在func.h
文件中,这样每次包含func.h
时都会可见。template <typename T> void templateFunction(T value) { // 模板函数的实现std::cout << "Template value: " << value << std::endl; }
-
显式实例化:很被动,需要不断添加显式实例,具有局限性,不方便
在func.cpp
中使用显式实例化声明,例如template void templateFunction<int>(int);
。这样,编译器在编译func.cpp
时会生成针对特定类型的代码。 -
在调用前包含实现:
通过在test.cpp
中包含func.cpp
,确保模板的实现可用。但这不是推荐的做法,因为它会导致代码的重复编译。
(4)总结
- 模板函数必须在调用时是可见的。为了避免这种问题,建议将模板的实现放在头文件中,或者使用显式实例化。
- 普通函数因为有具体的实现和链接,因此在
test.cpp
中可以直接调用。
优点:本质上是把活交给编译器
a. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
b. 增强了代码的灵活性。
缺点:
a. 模板会导致代码膨胀问题,也会导致编译时间变长。
b. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。