深入探索类模板:从基础到高级应用
1. 类模板中的表达式与异常处理
在类模板中,使用条件运算符的表达式可能会遇到编译问题。例如,使用条件运算符为第二个参数提供值时,若要确保值至少为 5,可能需要添加括号。以下代码展示了正确的写法:
Array<Box, (start > 5 ? start : 5)> boxes; // OK
当表达式涉及箭头运算符(->)或右移运算符(>>)时,括号也可能是必要的。
在异常处理方面,若索引超出上限,会抛出异常。异常可在
main()
函数体的
catch
块中捕获。参数是基类的引用,输出显示异常类型为
std::out_of_range
,这表明使用引用参数不会发生对象切片。不同的异常捕获方式会产生不同的结果,在
main()
函数体的
catch
块中捕获异常会导致程序在此处结束;而在嵌套
try
块的
catch
块中捕获异常则可使程序继续执行。
2. 非类型参数的参数规则
非类型参数的参数若不是引用或指针,必须是编译时常量表达式。这意味着不能使用包含非
const
整数变量的表达式作为参数,不过编译器会验证参数的有效性。例如:
int start {-10};
Array<double, start> values(21); // Won't compile because start is not const
正确的写法是将变量声明为
const
:
const int start {-10};
Array<double, start> values(21); // OK
编译器在必要时会对参数进行标准转换,以匹配参数类型。
3. 指针和数组作为非类型参数
指针作为非类型参数时,其参数必须是地址,且该地址必须是具有外部链接的对象或函数的地址。例如,不能使用数组元素的地址或非静态类成员的地址作为参数。若非类型参数为
const char*
类型,不能直接使用字符串字面量作为参数,需将字符串字面量的地址赋给指针变量,再将指针作为模板参数。
虽然指针是合法的非类型模板参数,但数组和指针在作为模板参数时并非总是可互换的。以下是相关示例:
template <long* numbers>
class MyClass
{
// Template definition...
};
long data[10]; // Global
long* pData {data}; // Global
MyClass<pData> values;
MyClass<data> values;
在上述代码中,数组名或合适类型的指针都可作为对应指针参数的参数。但如果模板定义如下:
template <long numbers[10]>
class AnotherClass
{
// Template definition...
};
则只能使用数组作为参数,使用指针会导致编译失败:
AnotherClass<data> numbers; // OK
AnotherClass<pData> numbers; // Not allowed!
这是因为数组类型与指针类型有很大差异,数组名本身代表地址,但不是指针,不能像指针那样被修改。
4. 模板参数的默认值
类模板中的类型和非类型参数都可以提供默认参数值,这与函数参数的默认值类似。若某个参数有默认值,则后续所有参数都必须指定默认值。若省略具有默认值的模板参数的参数,将使用默认值。例如:
template < typename T = int, int startIndex = 0>
class Array
{
// Template definition as before...
};
Array<> numbers {101};
Array<string, -100> messages {200};
Array<Box> boxes {101};
若类模板的参数有默认值,只需在源文件中首次声明模板时指定即可,通常是类模板的定义处。
5. 显式模板实例化
之前,类模板的实例通常是在定义模板类型的变量时隐式创建的。也可以在不定义模板类型对象的情况下显式实例化类模板。使用
template
关键字,后跟模板类名和尖括号内的模板参数,可显式创建模板实例。例如:
template class Array<double, 1>;
这将创建一个存储
double
类型值、索引从 1 开始的模板实例。显式实例化类模板会生成类类型定义,并从其模板实例化类的所有函数成员,即使这些函数成员未被调用,可执行文件中也可能包含未使用的代码。
6. 特殊情况处理
在很多情况下,类模板定义可能无法满足所有可能的参数类型。例如,使用重载的比较运算符可以比较
string
对象,但不能用于空终止字符串。若类模板使用比较运算符比较对象,它适用于
string
类型,但不适用于
char*
类型。要比较
char*
类型的对象,需要使用
<cstring>
头文件中声明的比较函数。
为处理这类问题,有两种选择:
-
使用
static_assert()
:可在类模板中使用
static_assert()
来检查类型参数是否合适。当第一个参数为
false
时,编译器会输出第二个参数指定的消息并停止编译。例如:
#include <stdexcept>
#include <string>
#include <type_traits>
template <typename T>
class Array
{
static_assert(std::is_default_constructible<T>::value, "A default constructor is required.");
// Rest of the template as before...
};
以下是一些常用的
type_traits
模板及其作用:
| 模板 | 结果 |
| — | — |
|
is_default_constructible<T>
| 仅当类型
T
可默认构造(即类有无参构造函数)时,
value
成员为
true
|
|
is_copy_constructible<T>
| 若类型
T
可复制构造(即类有复制构造函数),
value
成员为
true
|
|
is_assignable<T>
| 若类型
T
可赋值(即类有赋值运算符函数),
value
成员为
true
|
|
is_pointer<T>
| 若类型
T
是指针类型,
value
成员为
true
,否则为
false
|
|
is_null_pointer<T>
| 仅当类型
T
为
std::nullptr_t
时,
value
成员为
true
|
|
is_class<T>
| 仅当类型
T
是类类型时,
value
成员为
true
|
7. 类模板特化
类模板特化是类定义,而非类模板。对于特定类型,编译器会使用为该类型定义的特化版本,而不是从模板生成类。例如,为
char*
类型创建
Array
模板的特化版本:
template <>
class Array<char*>
{
// Definition of a class to suit type char*...
};
此特化定义必须在原模板定义或声明之后。由于所有参数都已指定,这称为模板的完全特化,因此
template
关键字后的尖括号为空。
8. 部分模板特化
若模板有两个参数,可能只需为特化指定类型参数,而保留非类型参数开放。可以定义部分特化的
Array
模板:
template <int start>
class Array<char*, start>
{
// Definition to suit type char*...
};
此特化也是一个模板,
template
关键字后的参数列表必须包含为该模板特化实例指定的参数。
除了
char*
类型的特化,指针类型通常也需要特殊处理。可以定义另一个部分特化的模板:
template <typename T, long start>
class Array<T*, start>
{
// Definition to suit pointer types other than char*...
};
当有多个部分特化版本时,编译器会选择最匹配的特化版本。例如:
Array<Box*, -5> boxes {11};
Array<char*, 1> messages {100};
对于
Array<char*, 1>
,编译器会选择
char*
的部分特化版本,因为它更特化。
9. 类模板的友元
类模板也可以有友元,友元可以是类、函数或其他模板。若类是类模板的友元,则其所有函数成员都是模板每个实例的友元;若函数是模板的友元,则它是模板任何实例的友元。
模板作为友元时,模板类的参数列表通常包含定义友元模板所需的所有参数。只有在代码中使用友元函数模板时,才会实例化该模板。
普通类也可以将类模板或函数模板声明为友元,此时模板的所有实例都是该类的友元。
通过以上内容,我们对类模板的各个方面有了更深入的了解,包括非类型参数的使用、模板实例化、特殊情况处理以及类模板的特化和友元等。这些知识将帮助我们更好地设计和使用类模板,提高代码的灵活性和可维护性。
下面是一个简单的 mermaid 流程图,展示类模板特化的选择过程:
graph TD;
A[声明模板实例] --> B{是否有完全特化版本};
B -- 是 --> C[使用完全特化版本];
B -- 否 --> D{是否有部分特化版本};
D -- 是 --> E[选择最匹配的部分特化版本];
D -- 否 --> F[使用原始模板生成类];
深入探索类模板:从基础到高级应用
10. 选择部分模板特化的规则
当存在多个部分模板特化版本时,编译器会依据一定规则选择最合适的版本。判断一个特化版本是否比另一个更“特化”的标准是:如果匹配某个特化版本的所有参数也能匹配另一个特化版本,但反之不成立,那么前者就更特化。例如,
Array<char*, start>
比
Array<T*, start>
更特化,因为所有能匹配
Array<char*, start>
的参数(即
char*
)也能匹配
Array<T*, start>
,但能匹配
Array<T*, start>
的参数(如
Box*
)不一定能匹配
Array<char*, start>
。
可以将模板的特化版本看作是从最特化到最不特化的有序集合。当多个模板特化版本都能匹配某个声明时,编译器会选择并应用最特化的版本。以下是一个对比表格,展示不同特化版本的匹配情况:
| 声明 | 匹配的特化版本 | 原因 |
| — | — | — |
|
Array<Box*, -5> boxes {11};
|
Array<T*, start>
| 只有此特化版本能匹配
Box*
类型 |
|
Array<char*, 1> messages {100};
|
Array<char*, start>
| 该特化版本比
Array<T*, start>
更特化 |
11. 实际应用中的考虑
在实际使用类模板时,有几个重要的方面需要考虑:
-
非类型参数的必要性
:非类型参数是模板实例类型的一部分,不同的非类型参数组合会产生不同的类类型。这可能会限制模板的实用性,例如不同起始索引的数组不能相互赋值。因此,在使用非类型参数时要谨慎,确保其确实必要,很多时候可以采用其他更灵活的方法。
-
代码效率
:显式模板实例化会生成类类型定义和所有函数成员的实例,即使这些成员未被调用,可执行文件中也可能包含未使用的代码。在性能敏感的场景中,需要权衡显式实例化的使用。
-
类型兼容性
:使用
static_assert()
可以避免类模板的误用,但要确保选择合适的
type_traits
模板进行检查。对于一些特殊类型,如
char*
,可能需要定义特化版本来处理。
12. 类模板友元的详细示例
为了更清晰地理解类模板的友元,下面给出一个详细的示例代码:
#include <iostream>
// 类模板定义
template <typename T>
class Array {
private:
T* data;
int size;
public:
Array(int s) : size(s) {
data = new T[size];
}
~Array() {
delete[] data;
}
// 友元函数模板声明
template <typename U>
friend void printArray(const Array<U>& arr);
};
// 友元函数模板定义
template <typename U>
void printArray(const Array<U>& arr) {
for (int i = 0; i < arr.size; ++i) {
std::cout << arr.data[i] << " ";
}
std::cout << std::endl;
}
int main() {
Array<int> intArray(5);
for (int i = 0; i < 5; ++i) {
intArray.data[i] = i;
}
printArray(intArray);
return 0;
}
在上述代码中,
printArray
是一个函数模板,它被声明为
Array
类模板的友元。这意味着
printArray
可以访问
Array
类模板实例的私有成员。
13. 总结与回顾
通过对类模板的全面探索,我们了解了许多关键概念和技术:
- 非类型参数的使用规则,包括常量表达式要求、指针和数组作为参数的限制。
- 模板参数的默认值,使模板使用更加灵活。
- 显式和隐式模板实例化的区别和应用场景。
- 处理特殊类型参数的方法,如使用
static_assert()
和定义类模板特化。
- 类模板友元的定义和使用,增强了类模板与其他类或函数的交互能力。
下面是一个 mermaid 流程图,总结类模板的使用流程:
graph LR;
A[定义类模板] --> B[使用模板参数(类型或非类型)];
B --> C{是否需要默认值};
C -- 是 --> D[为参数指定默认值];
C -- 否 --> E[不使用默认值];
D --> F[实例化模板(隐式或显式)];
E --> F;
F --> G{是否有特殊类型参数};
G -- 是 --> H[使用 static_assert() 或定义特化版本];
G -- 否 --> I[使用原始模板生成类];
H --> J[使用类实例];
I --> J;
通过掌握这些知识,我们可以更好地设计和实现类模板,提高代码的复用性和可维护性,在不同的应用场景中灵活运用类模板来解决实际问题。不断实践和探索类模板的各种特性,将有助于我们成为更优秀的 C++ 开发者。
超级会员免费看
1010

被折叠的 条评论
为什么被折叠?



