文章目录
- 一、非类型模板参数
- 1.非类型模板参数的基本形式
- 2.指针作为非类型模板参数
- 3.引用作为非类型模板参数
- 4.非类型模板参数的限制和陷阱:
- 5.几个问题
- 二、模板的特化
- 1.概念
- 2.函数模板特化
- 3.类模板特化
- (1)全特化
- (2)偏特化
- (3)类模板特化应用示例
- 三、模板分离编译
- 1.概念
- 2.模板的分离编译
- 模版总结
一、非类型模板参数
模板参数分类类型形参与非类型形参
- 非类型模板参数(non-type template parameter) 是指模板参数列表中一个非类型的参数。这些参数可以是常量表达式,比如整数、指针或引用等,而不是类型(如 int, double, class 等)。
- 非类型模板参数的主要作用是让模板可以根据常量值进行不同的实例化,而不仅仅是根据类型。
- 在类(函数)模板中可将该参数当成常量来使用。
1.非类型模板参数的基本形式
template <typename T, int N>
class Array {
public:
T data[N];
};
在这个例子中,N 就是一个非类型模板参数,它表示数组的大小。这意味着你可以创建不同大小的 Array 实例,而不需要定义不同的类型。例如:
Array<int, 10> arr1; // 一个包含10个整数的数组
Array<double, 20> arr2; // 一个包含20个双精度浮点数的数组
常见的非类型模板参数:
- 整型值:可以是 int, char, bool, enum 等。
- 指针:指向常量数据的指针,例如:指向函数、对象、字符串字面量等。
- 引用:可以是常量引用。
- 枚举:可以使用枚举常量。
非类型模板参数的限制:
- 参数值必须在编译时是一个常量表达式。
- 非类型模板参数不能是浮点类型或类类型。
namespace s
{
// 定义一个模板类型的静态数组,模板参数包括类型T和数组大小N(默认为10)
template <class T, size_t N = 10>
class array
{
public:
// 重载下标操作符,允许通过数组的索引访问元素
T &operator[](size_t index)
{
return _array[index]; // 返回数组中对应索引位置的元素
}
// const 版本的下标操作符,保证不会修改数组内容,用于常量对象
const T &operator[](size_t index) const
{
return _array[index]; // 返回数组中对应索引位置的元素
}
// 返回数组的实际大小(_size 表示当前数组中实际存储的元素数量)
size_t size() const
{
return _size;
}
// 判断数组是否为空
bool empty() const
{
return 0 == _size;
}
private:
T _array[N]; // 定义一个大小为N的数组,类型为模板参数T
size_t _size = 0; // 用于记录数组中的实际元素数量,默认为0
};
}
2.指针作为非类型模板参数
除了整型值,指针也可以作为非类型模板参数。这允许你在编译时传递某些内存地址或指向常量的指针。
在这个例子中,Ptr 是一个非类型模板参数,它接收一个指向 global_value 的指针。
template<int* Ptr>
class PointerWrapper {
public:
void print() const {
std::cout << *Ptr << std::endl;
}
};
int global_value = 42;
int main() {
PointerWrapper<&global_value> pw;
pw.print(); // 输出42
return 0;
}
3.引用作为非类型模板参数
在这个例子中,Ref 是一个常量引用的非类型模板参数。
template<const int& Ref>
class ReferenceWrapper {
public:
void print() const {
std::cout << Ref << std::endl;
}
};
int global_value = 99;
int main() {
ReferenceWrapper<global_value> rw;
rw.print(); // 输出99
return 0;
}
4.非类型模板参数的限制和陷阱:
- 浮点类型不能作为非类型模板参数,这是由于浮点数在不同平台上可能存在的精度差异,无法保证其在编译时的一致性。
- 类对象以及字符串也不能作为非类型模板参数
5.几个问题
- 非类型模板参数和类型模板参数的区别:
- 类型模板参数允许我们根据不同的类型实例化模板。可以用 typename 或 class 来声明类型模板参数,例如 template。
- 非类型模板参数是根据值(而不是类型)来进行实例化的。这些值必须在编译时是已知的常量。比如 int、指针、引用 等。
问题:什么时候使用非类型模板参数,而不是类型模板参数?
如果需要根据常量值(如整数、指针)进行编译时的优化、数组大小设定或策略选择,非类型模板参数是更合适的选择。而如果只是根据类型变化来进行不同实例化,类型模板参数更直接。
- 为什么非类型模板参数必须是编译时常量?
- 非类型模板参数必须是编译时常量,这意味着模板的实例化发生在编译期间,允许编译器在编译时做出优化并生成不同的代码。
- 如果非类型模板参数是运行时的值,编译器将无法在编译时对其进行优化或实例化。
问题:什么样的值可以作为编译时常量?
- 常量表达式(constexpr)
- 常量整数、字符、布尔类型
- 指向常量对象的指针或引用
- 枚举常量 浮点数不能作为编译时常量,因为浮点数的精度在不同的硬件平台上可能会有差异。
- 编译时和运行时的区别是什么?
- 编译时是指程序被编译器处理生成可执行文件的阶段。在这个阶段,模板会被根据类型或非类型模板参数进行实例化。
- 运行时是指程序执行的阶段,在这个阶段,所有的变量、对象和指令都会实际运行。
问题:编译时与运行时的边界是什么?
编译时发生的事情是静态的,例如模板实例化和优化。而运行时则动态处理输入数据、调用函数并执行指令。非类型模板参数的特殊之处就在于它们能让编译器根据编译时的常量生成不同的代码路径。
- 在实际项目中,非类型模板参数的常见应用场景是什么?
非类型模板参数主要用于:
- 固定大小数组:用于处理编译时已知的数组大小,减少运行时的检查和动态分配。
- 元编程:实现编译时计算,例如递归的阶乘、斐波那契数列。
- 策略选择:根据编译时常量选择不同的算法或实现,从而提高性能。
问题:什么时候我应该选择使用非类型模板参数,而不是简单的函数或类?
当你的算法或数据结构依赖于编译时的常量值,并且你希望编译器在编译时生成不同的代码,而不是在运行时进行选择时,非类型模板参数是理想的选择。
二、模板的特化
1.概念
模板特化(template specialization) 是一种为特定类型或值定制模板的机制。通常,模板是通用的,适用于任何类型或值。但有时希望为某些特定类型或值提供不同的实现或对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。这时可以使用模板特化。
// 函数模板 -- 参数匹配
template <class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
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; // 可以比较,结果错误
return 0;
}
p1指向的d1显然小于p2指向的d2对象,但是Less内部并没有比较p1和p2指向的对象内容,而比较的是p1和p2指针的地址,这就无法达到预期而错误。此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。
2.函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
#include <iostream>
using namespace std;
// 定义 Date 类
class Date
{
public:
int year;
int month;
int day;
// 构造函数
Date(int y, int m, int d) : year(y), month(m), day(d) {}
// 重载 < 运算符,用于比较两个 Date 对象
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;
}
};
// 函数模板 -- 参数匹配
template <class T>
bool Less(T left, T right)
{
return left < right; // 使用 < 运算符比较两个 T 类型的对象
}
// 对 Less 函数模板进行特化,专门处理 Date* 指针类型
template <>
bool Less<Date *>(Date *left, Date *right)
{
return *left < *right; // 解引用指针,比较指针指向的 Date 对象的内容
}
int main()
{
// 比较两个整数,调用通用模板
cout << Less(1, 2) << endl; // 输出 1(true),因为 1 < 2
// 创建两个 Date 对象
Date d1(2022, 7, 7);
Date d2(2022, 7, 8);
// 比较两个 Date 对象,调用通用模板,使用 < 运算符
cout << Less(d1, d2) << endl; // 输出 1(true),因为 d1 < d2
Date *p1 = &d1;
Date *p2 = &d2;
// 比较两个 Date* 指针,调用特化后的版本
cout << Less(p1, p2) << endl; // 输出 1(true),因为特化版本解引用指针后比较 d1 和 d2
return 0;
}
3.类模板特化
(1)全特化
全特化即是将模板参数列表中所有的参数都确定化。
#include <iostream>
using namespace std;
// 通用模板类定义,适用于任意类型 T1 和 T2
template <class T1, class T2>
class Data
{
public:
// 构造函数,输出"Data<T1, T2>",表明使用的是通用模板
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 针对特定类型 <int, char> 的全特化
template <>
class Data<int, char>
{
public:
// 构造函数,输出"Data<int, char>",表明使用的是特化版本
Data()
{
cout << "Data<int, char>" << endl;
}
private:
int _d1;
char _d2;
};
// 测试函数,用于创建不同类型的 Data 对象
void TestVector()
{
// 创建 Data<int, int> 对象,调用的是通用模板版本
Data<int, int> d1;
// 创建 Data<int, char> 对象,调用的是特化的版本
Data<int, char> d2;
}
int main()
{
TestVector(); // 运行测试函数
return 0;
}
(2)偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本
偏特化是指模板的部分特化,它只对模板参数中的部分类型进行定制,而保持其他部分为泛型。这与全特化(完全特化)不同,全特化是针对特定的类型组合进行完全独立的实现。
偏特化允许我们为一部分类型组合定制实现,而不必对所有类型参数进行特化。这样可以在保留通用模板的同时,针对某些特殊情况进行优化或修改行为。
#include <iostream>
using namespace std;
// 通用模板类 Data 的定义
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 对象时输出 "Data<T1, int>"
Data()
{
cout << "Data<T1, int>" << endl;
}
private:
T1 _d1; // 成员变量类型为 T1
int _d2; // 成员变量类型为 int
};
// 将两个参数偏特化为指针类型,即当两个模板参数均为指针类型时使用该特化版本
template <typename T1, typename T2>
class Data<T1 *, T2 *>
{
public:
// 构造函数:创建 Data 对象时输出 "Data<T1*, T2*>"
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1 _d1; // 成员变量类型为指针类型的 T1
T2 _d2; // 成员变量类型为指针类型的 T2
};
// 将两个参数偏特化为引用类型,即当两个模板参数均为引用类型时使用该特化版本
template <typename T1, typename T2>
class Data<T1 &, T2 &>
{
public:
// 构造函数:接受两个引用类型的参数并进行初始化,同时输出 "Data<T1&, T2&>"
Data(const T1 &d1, const T2 &d2)
: _d1(d1), _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1 &_d1; // 成员变量类型为 T1 的常量引用
const T2 &_d2; // 成员变量类型为 T2 的常量引用
};
// 测试函数
void test2()
{
// 使用特化版本,第二个模板参数为 int
Data<double, int> d1; // 输出 "Data<T1, int>"
// 使用基础模板,没有进行特化
Data<int, double> d2; // 输出 "Data<T1, T2>"
// 使用特化版本,当两个模板参数均为指针类型时
Data<int *, int *> d3; // 输出 "Data<T1*, T2*>"
// 使用特化版本,当两个模板参数均为引用类型时
Data<int &, int &> d4(1, 2); // 输出 "Data<T1&, T2&>"
}
(3)类模板特化应用示例
#include <vector>
#include <algorithm>
using namespace std;
// 通用的 Less 模板类
template <class T>
struct Less
{
bool operator()(const T &x, const T &y) const
{
return x < y;
}
};
// 对 Less 类模板进行特化,处理 Date* 类型
template <>
struct Less<Date *>
{
// 重载运算符 (),比较两个指向 Date 对象的指针
bool operator()(Date *x, Date *y) const
{
return *x < *y; // 比较指针所指向的 Date 对象
}
};
int main()
{
// 假设 Date 类已经定义并实现了 < 运算符
Date d1(2022, 7, 7);
Date d2(2022, 7, 6);
Date d3(2022, 7, 8);
// 创建一个存储 Date 对象的向量 v1
vector<Date> v1 = {d1, d2, d3};
// 使用 Less<Date> 进行排序
sort(v1.begin(), v1.end(), Less<Date>());
// 创建一个存储 Date 指针的向量 v2
vector<Date *> v2 = {&d1, &d2, &d3};
// 使用特化后的 Less<Date*> 进行排序
sort(v2.begin(), v2.end(), Less<Date *>());
return 0;
}
三、模板分离编译
1.概念
- 分离编译(Separate Compilation)是指将一个程序的各个部分(通常是源代码文件)单独编译成目标文件(如 .obj 文件),然后在链接阶段将所有目标文件链接为一个最终的可执行文件。
- 分离编译的主要好处是提高编译效率:当修改了某个源文件时,只需重新编译修改后的文件,而不需要重新编译整个项目。
2.模板的分离编译
- 模板的分离编译是指将模板代码分离到不同的源文件中进行编译。但是,模板的分离编译和普通的分离编译略有不同,因为模板的定义和实例化在编译时必须是可见的,这使得模板代码的编译和管理变得复杂。
- 有几种方法可以实现模板的分离编译:
- 将模板定义放在头文件中:
通常,模板的定义和声明都放在头文件中,以确保编译器在编译每个使用模板的源文件时能够看到模板的完整定义。这是最常见的解决方案。 - 显式实例化: 模板的定义仍然可以放在 .cpp 文件中,但需要在这个文件中显式地为你需要的模板参数实例化模板。方法不实用,不推荐使用。
模版总结
- 优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
- 缺点
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误