62、深入探索类模板:从基础到高级应用

深入探索类模板:从基础到高级应用

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++ 开发者。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值