类模板:静态成员、非类型参数及实际应用
1. 异常处理与类模板实例化
当异常对象被创建时,异常信息可通过输出得知是由重载的下标运算符函数抛出,且索引值过大,推测该索引值可能源于对无符号零值的递减操作。当该运算符函数抛出异常时,控制权会立即转移到异常处理程序,非法元素引用不会被使用,也不会在非法索引指定的位置存储任何内容,同时循环也会立即结束。
接下来的
try
块定义了一个可存储
Box
对象的对象。此时,编译器会生成类模板
Array<Box>
的一个实例,该实例用于存储
Box
对象数组,因为此前该模板尚未针对
Box
对象进行实例化。此语句还会调用构造函数来创建
boxes
对象,从而生成构造函数的函数模板实例。
Array<Box>
类的构造函数在自由存储区创建
elements
成员时,会调用
Box
类的默认构造函数,因此
elements
数组中的所有
Box
对象都具有默认尺寸
1 × 1 × 1
。
在
for
循环中输出
boxes
中每个
Box
对象的体积。表达式
boxes[i]
会调用重载的下标运算符,编译器会再次使用模板实例来生成该函数的定义。当
i
的值为
nBoxes
时,下标运算符函数会抛出异常,因为
nBoxes
这个索引值超出了
elements
数组的末尾。
try
块后面的
catch
块会捕获该异常,由于退出了
try
块,所有局部声明的对象(包括
boxes
对象)都会被销毁,而
values
对象仍然存在,因为它是在第一个
try
块之前创建的,且仍在作用域内。
2. 类模板的静态成员
类模板和普通类一样可以有静态成员。模板类的静态函数成员比较直接,类模板的每个实例会根据需要实例化静态函数成员。静态函数成员没有
this
指针,因此不能引用类的非静态成员。定义类模板静态函数成员的规则与普通类相同,类模板的静态函数成员在模板的每个实例中的行为就如同在普通类中一样。
静态数据成员则更有趣一些,因为它需要在模板定义之外进行初始化。假设
Array<T>
模板包含一个类型为
T
的静态数据成员:
template <typename T>
class Array
{
private:
static T value; // Static data member
T* elements; // Array of type T
size_t size; // Number of array elements
public:
explicit Array(size_t arraySize); // Constructor
Array(const Array& array); // Copy Constructor
~Array(); // Destructor
T& operator[](size_t index); // Subscript operator
const T& operator[](size_t index) const; // Subscript operator-const arrays
Array& operator=(const Array& rhs); // Assignment operator
size_t getSize() { return size; } // Accessor for size
};
在同一个头文件中,可通过以下模板对
value
成员进行初始化:
template <typename T> T Array<T>::value; // Initialize static data member
这会将
value
初始化为等效于
0
的值,实际上是调用了类型
T
的默认构造函数。静态数据成员始终依赖于它所属模板的参数,无论其类型如何,因此必须将
value
作为带有参数
T
的模板进行初始化。静态变量名还必须使用类型名
Array<T>
进行限定,以便与生成的类模板实例相关联。这里不能单独使用
Array
,因为初始化
value
的这个模板在类模板体之外,模板ID是
Array<T>
。
假设要定义
Array<T>
的一个静态成员来表示元素的最小数量:
template <typename T>
class Array
{
private:
static size_t minSize; // Minimum number of elements
// Rest of the class as before...
};
即使
minSize
是基本类型(
size_t
是无符号整数类型的别名),仍必须在模板中对其进行初始化,并使用模板ID对成员名进行限定:
template<typename T> size_t Array<T>::minSize {5};
minSize
只能作为从
Array<T>
模板生成的类的成员存在,每个这样的类都有自己的
minSize
成员,因此不可避免地只能通过另一个模板对其进行初始化。需要注意的是,创建模板实例并不保证会定义静态数据成员,类模板的静态数据成员只有在被使用时才会被定义,因为编译器只有在使用该成员时才会处理初始化静态数据成员的模板。
3. 非类型类模板参数
非类型参数看起来像函数参数,即类型名后面跟着参数名。因此,非类型参数的实参是给定类型的值。但在类模板中,不能随意使用任何类型作为非类型参数。非类型参数旨在用于定义在指定容器时可能有用的值,例如数组维度或其他大小规格,或者可能作为索引值的上下限。
非类型参数只能是以下几种类型:
- 整数类型,如
size_t
或
long
;
- 枚举类型;
- 对象的指针或引用,如
string*
或
Box&
;
- 函数的指针或引用;
- 类成员的指针。
由此可知,非类型参数不能是浮点类型或任何类类型,因此
double
、
Box
和
std::string
等类型是不允许的,
std::string**
也不允许。需要记住,非类型参数的主要目的是允许指定容器的大小和范围限制。当然,只要参数类型是引用,与非类型参数对应的实参可以是类类型的对象。例如,对于类型为
Box&
的参数,可以使用任何
Box
类型的对象作为实参。
非类型参数的写法与函数参数相同,即类型名后面跟着参数名。例如:
template <typename T, size_t size>
class ClassName
{
// Definition using T and size...
};
这个模板有一个类型参数
T
和一个非类型参数
size
,其定义是根据这两个参数和模板名来表达的。如果需要,类型参数的类型名也可以作为非类型参数的类型:
template <typename T, // T is the name of the type parameter
size_t size,
T value> // T is also the type of this non-type parameter
class ClassName
{
// Definition using T, size, and value...
};
这个模板有一个类型为
T
的非类型参数
value
。参数
T
必须在参数列表中先出现,因此这里
value
不能在类型参数
T
之前。需要注意的是,在类型参数和非类型参数中使用相同的符号会隐式地将类型参数的可能实参限制为非类型实参允许的类型(即
T
只能是整数类型)。
为了说明如何使用非类型参数,假设按以下方式定义数组的类模板:
template <typename T, size_t arraySize, T value>
class Array
{
// Definition using T, size, and value...
};
可以使用非类型参数
value
在构造函数中初始化数组的每个元素:
template <typename T, int arraySize, T value>
Array<T, arraySize, value>::Array(size_t arraySize) : size {arraySize}, elements {new T[size]}
{
for(size_t i {} ; i < size ; ++i)
elements[i] = value;
}
这并不是一种非常明智的数组元素初始化方法,它对
T
的合法类型施加了严重限制。因为
T
被用作非类型参数的类型,所以它受到非类型参数类型的约束。非类型参数只能是整数类型、指针或引用,因此无法创建存储
double
值或
Box
对象的
Array
对象,该模板的实用性在一定程度上受到了限制。
为了提供一个更可靠的示例,在
Array
模板中添加一个非类型参数,以实现数组索引的灵活性:
template <typename T, int startIndex>
class Array
{
private:
T* elements; // Array of type T
size_t size; // Number of array elements
public:
explicit Array(size_t arraySize); // Constructor
Array(const Array& array); // Copy Constructor
~Array(); // Destructor
T& operator[](int index); // Subscript operator
const T& operator[](int index) const; // Subscript operator-const arrays
Array& operator=(const Array& rhs); // Assignment operator
size_t getSize() { return size; } // Accessor for size
};
这里添加了一个类型为
int
的非类型参数
startIndex
,其思路是可以指定希望使用在给定范围内变化的索引值。例如,要创建一个允许索引值从
-10
到
+10
的
Array<>
对象,需要将非类型参数值指定为
–10
,并将构造函数的参数指定为
21
,因为数组需要
21
个元素。现在索引值可以为负数,因此下标运算符函数的参数类型已更改为
int
。
由于类模板现在有两个参数,定义类模板成员函数的模板也必须具有相同的两个参数,即使某些函数不使用非类型参数,这也是必要的。参数是类模板标识的一部分,为了匹配模板,它们必须具有相同的参数列表。
添加
startIndex
模板参数会带来一些严重的缺点。不同的实参值会生成不同的模板实例,这意味着从
0
开始索引的
double
值数组与从
1
开始索引的
double
值数组是不同的类型。如果在程序中同时使用这两种类型,模板会创建两个独立的类定义,每个定义都包含所使用的成员函数。这至少会带来两个不良后果:一是程序中会生成比预期更多的编译代码(通常称为代码膨胀);二是更糟糕的是,无法在表达式中混合使用这两种类型的元素。更好的方法是通过向构造函数添加参数来提供索引值范围的灵活性,而不是使用非类型模板参数,示例如下:
template <typename T>
class Array
{
private:
T* elements; // Array of type T
size_t size; // Number of array elements
int start; // Starting index value
public:
explicit Array(size_t arraySize, int startIndex=0); // Constructor
T& operator[](int index); // Subscript operator
const T& operator[](int index) const; // Subscript operator-const arrays
Array& operator=(const Array& rhs); // Assignment operator
size_t getSize() { return size; } // Accessor for size
};
额外的成员
start
用于存储由第二个构造函数参数指定的数组起始索引,
startIndex
参数的默认值为
0
,因此默认情况下可获得正常的索引方式。
4. 带有非类型参数的函数成员模板
由于在类模板定义中添加了非类型参数,所有函数成员的模板代码都需要更改。构造函数的模板如下:
template <typename T, int startIndex>
inline Array<T, startIndex>::Array(size_t arraySize) :
size {arraySize}, elements {new T[arraySize]}
{}
现在模板ID是
Array<T, startIndex>
,用于限定构造函数名。除了在模板中添加新的模板参数并省略处理
bad_alloc
的
try/catch
块外,这是与原始定义唯一的区别。
复制构造函数的模板更改类似:
template <typename T, int startIndex>
inline Array<T, startIndex>::Array(const Array& array) :
size {array.size}, elements {new T[array.size]}
{
for (size_t i {} ; i < size ; ++i)
elements[i] = array.elements[i];
}
当然,数组的外部索引不会影响内部访问数组的方式,这里仍然从
0
开始索引。
析构函数只需要额外的模板参数:
template <typename T, int startIndex>
inline Array<T, startIndex>::~Array()
{
delete[] elements;
}
非
const
下标运算符函数的模板定义现在变为:
template <typename T, int startIndex>
T& Array<T, startIndex>::operator[](int index)
{
if (index > startIndex + static_cast<int>(size) - 1)
throw std::out_of_range {"Index too large: " + std::to_string(index)};
if(index < startIndex)
throw std::out_of_range {"Index too small: " + std::to_string(index)};
return elements[index - startIndex];
}
这里有显著的更改。索引参数的类型为
int
,以允许负值。对索引值的有效性检查现在会验证其是否在由非类型模板参数和数组元素数量确定的范围内,索引值只能从
startIndex
到
startIndex + size - 1
。由于
size_t
通常是无符号整数类型,必须显式地将其转换为
int
,否则表达式中的其他值会隐式转换为
size_t
,如果
startIndex
为负数,会产生错误的结果。异常消息的选择和选择该消息的表达式也发生了变化。
const
版本的下标运算符函数也有类似的更改:
template <typename T, int startIndex>
const T& Array<T, startIndex>::operator[](int index) const
{
if (index > startIndex + static_cast<int>(size) - 1)
throw std::out_of_range {"Index too large: " + std::to_string(index)};
if(index < startIndex)
throw std::out_of_range {"Index too small: " + std::to_string(index)};
return elements[index - startIndex];
}
最后,需要更改赋值运算符的模板,但只需修改模板参数列表和限定运算符名的模板ID:
template <typename T, int startIndex>
Array<T, startIndex>& Array<T, startIndex>::operator=(const Array& rhs)
{
if (&rhs != this) // If lhs != rhs...
{ // ...do the assignment...
if (elements) // If lhs array exists
delete[] elements; // release the free store memory
size = rhs.size;
elements = new T[rhs.size];
for (size_t i {}; i < size; ++i)
elements[i] = rhs.elements[i];
}
return *this; // ... return lhs
}
在模板中使用非类型参数有一些限制。特别是,不能在模板定义中修改参数的值,因此非类型参数不能用于赋值语句的左侧,也不能应用递增或递减运算符,即它被视为常量。类模板中的所有参数在创建实例时都必须指定,除非它们有默认值。
5. 非类型参数的实际应用示例
尽管带有非类型参数的
Array
模板存在一些缺点,但通过一个实际示例可以看到它的工作情况。只需将函数成员模板的定义与带有非类型参数的
Array
模板定义一起组装到一个头文件中。以下示例使用
Ex16_01
中的
Box.h
来测试新特性:
// Ex16_02.cpp
// Using a class template with a non-type parameter
#include "Box.h"
#include "Array.h"
#include <iostream>
#include <iomanip>
int main()
try
{
try
{
const size_t size {21}; // Number of array elements
const int start {-10}; // Index for first element
const int end {start + static_cast<int>(size) - 1}; // Index for last element
Array<double, start> values {size}; // Define array of double values
for (int i {start}; i <= end; ++i) // Initialize the elements
values[i] = i - start + 1;
std::cout << "Sums of pairs of elements: ";
size_t lines {};
for (int i {end} ; i >=start; --i)
std::cout << (lines++ % 5 == 0 ? "\n" : "")
<< std::setw(5) << values[i] + values[i - 1];
}
catch (const std::out_of_range& ex)
{
std::cerr << "\nout_of_range exception object caught! " << ex.what() << std::endl;
}
const int start {};
const size_t size {11};
Array<Box, start - 5> boxes {size}; // Create array of Box objects
for (int i {start - 5}; i <= start + static_cast<int>(size) - 5; ++i)
std::cout << "Box[" << i << "] volume is " << boxes[i].volume() << std::endl;
}
catch (const std::exception& ex)
{
std::cerr << typeid(ex).name() << " exception caught in main()! "
<< ex.what() << std::endl;
}
该程序的输出如下:
Sums of pairs of elements:
41 39 37 35 33
31 29 27 25 23
21 19 17 15 13
11 9 7 5 3
out_of_range exception object caught! Index too small: -11
Box[-5] volume is 1
Box[-4] volume is 1
Box[-3] volume is 1
Box[-2] volume is 1
Box[-1] volume is 1
Box[0] volume is 1
Box[1] volume is 1
Box[2] volume is 1
Box[3] volume is 1
Box[4] volume is 1
Box[5] volume is 1
class std::out_of_range exception caught in main()! Index too large: 6
main()
函数体是一个
try
块,用于捕获所有以
std::exception
为基类的未捕获异常,因此
std::bad_alloc
也会被捕获。嵌套的
try
块首先定义了指定索引值范围和数组大小的常量,
size
和
start
变量用于创建
Array
模板的一个实例,以存储
21
个
double
类型的值。第二个模板实参对应于非类型参数,指定了数组索引值的下限,数组的大小由构造函数参数指定。
接下来的
for
循环为
values
对象的元素赋值,循环索引
i
从下限
start
(即
–10
)开始,到上限
end
(即
+10
)结束,循环内数组元素的值从
1
到
21
。
然后从数组的最后一个元素开始递减输出相邻元素对的和,
lines
变量用于每行输出五个和。与前面的示例一样,索引值控制不当会导致表达式
values[i–1]
抛出
out_of_range
异常,嵌套
try
块的处理程序会捕获该异常并显示输出中的消息。
创建存储
Box
对象数组的语句位于外层
try
块(即
main()
函数体)中,
boxes
的类型是
Array<Box,start-5>
,这表明在模板实例化中,表达式可以作为非类型参数的实参值。这样的表达式必须计算为具有参数类型的值,或者必须可以通过隐式转换将结果转换为适当的类型。如果这样的表达式包含
>
字符,需要格外小心,例如
Array<Box, start > 5 ? start : 5> boxes;
是无法编译的。
综上所述,类模板的静态成员和非类型参数为C++编程提供了强大的功能和灵活性,但在使用时需要注意它们所带来的各种限制和潜在问题。
类模板:静态成员、非类型参数及实际应用
6. 总结与对比分析
为了更清晰地理解类模板的静态成员和非类型参数,下面对相关内容进行总结和对比分析。
6.1 静态成员总结
| 成员类型 | 特点 | 初始化 | 使用注意事项 |
|---|---|---|---|
| 静态函数成员 |
- 类模板的每个实例按需实例化
- 无
this
指针,不能引用非静态成员
- 定义规则与普通类相同 | 无需特殊初始化,随模板实例化自动处理 | 可直接通过类名调用,使用方式与普通类静态函数类似 |
| 静态数据成员 |
- 依赖于模板参数
- 不同模板实例有各自独立的静态数据成员 |
在模板定义之外初始化,使用模板语法,如
template <typename T> T Array<T>::value;
| 只有在被使用时才会定义,创建模板实例不保证其定义 |
6.2 非类型参数总结
| 相关内容 | 说明 |
|---|---|
| 允许的类型 |
整数类型(如
size_t
、
long
)、枚举类型、对象指针或引用、函数指针或引用、类成员指针
|
| 不允许的类型 |
浮点类型、类类型(如
double
、
Box
、
std::string
)
|
| 使用场景 | 用于指定容器的大小、范围限制等 |
| 限制 |
- 不能在模板定义中修改参数值
- 不能用于赋值语句左侧,不能应用递增或递减运算符 - 创建实例时必须指定,除非有默认值 |
6.3 非类型参数与构造函数参数对比
| 对比项 | 非类型参数 | 构造函数参数 |
|---|---|---|
| 生成实例情况 | 不同实参值生成不同模板实例,可能导致代码膨胀 | 同一模板,不同对象,不会因参数不同生成新的类定义 |
| 灵活性 | 相对固定,编译时确定 | 运行时可灵活指定 |
| 代码复杂度 | 增加模板定义和成员函数模板的复杂度 | 相对简单,不影响模板的本质结构 |
7. 深入理解与拓展思考
通过前面的内容,我们对类模板的静态成员和非类型参数有了较为全面的了解,但在实际应用中,还可以进行更深入的思考和拓展。
7.1 静态成员的高级应用
静态成员可以用于实现一些全局的统计或管理功能。例如,在
Array
模板中,可以添加一个静态数据成员来统计创建的
Array
对象的总数,或者记录所有
Array
对象占用的总内存大小。这样可以方便进行资源管理和性能监控。
template <typename T>
class Array
{
private:
static size_t objectCount; // 统计创建的Array对象总数
T* elements;
size_t size;
public:
explicit Array(size_t arraySize) : size {arraySize}, elements {new T[arraySize]}
{
++objectCount;
}
~Array()
{
delete[] elements;
--objectCount;
}
static size_t getObjectCount()
{
return objectCount;
}
};
template <typename T>
size_t Array<T>::objectCount = 0; // 初始化静态数据成员
7.2 非类型参数的优化使用
虽然非类型参数存在一些缺点,但在某些特定场景下,合理使用可以提高代码的性能和安全性。例如,在需要创建固定大小的数组时,使用非类型参数可以在编译时进行边界检查,避免运行时错误。
template <typename T, size_t N>
class FixedSizeArray
{
private:
T data[N];
public:
T& operator[](size_t index)
{
if (index >= N)
{
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
};
7.3 模板元编程的联系
类模板的静态成员和非类型参数与模板元编程密切相关。模板元编程是在编译时进行计算和代码生成的技术,静态成员和非类型参数可以作为编译时的常量参与计算,从而实现一些复杂的编译时逻辑。例如,通过非类型参数和静态成员的组合,可以实现编译时的递归计算。
template <int N>
struct Factorial
{
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0>
{
static const int value = 1;
};
// 使用示例
int result = Factorial<5>::value; // 编译时计算5的阶乘
8. 最佳实践建议
在使用类模板的静态成员和非类型参数时,为了避免潜在的问题并充分发挥其优势,可以遵循以下最佳实践建议。
8.1 静态成员使用建议
- 明确初始化 :确保静态数据成员在模板定义之外正确初始化,避免未定义行为。
- 合理使用 :仅在确实需要全局共享或统计信息时使用静态成员,避免滥用导致代码混乱。
- 线程安全 :如果在多线程环境中使用静态成员,需要考虑线程安全问题,添加适当的同步机制。
8.2 非类型参数使用建议
- 谨慎选择 :在决定使用非类型参数之前,仔细评估是否真的需要编译时确定的常量,是否可以通过构造函数参数实现相同的功能。
- 避免复杂表达式 :尽量避免在非类型参数中使用复杂的表达式,以免增加代码的复杂度和维护难度。
- 注意类型限制 :严格遵守非类型参数允许的类型范围,避免使用不允许的类型导致编译错误。
9. 未来发展趋势
随着C++语言的不断发展,类模板的静态成员和非类型参数可能会有更多的应用场景和改进。
9.1 语言特性的增强
未来的C++标准可能会进一步增强类模板的功能,例如提供更灵活的非类型参数类型支持,或者简化静态成员的初始化和使用方式。
9.2 与其他技术的融合
类模板的静态成员和非类型参数可能会与其他新兴技术(如人工智能、大数据等)相结合,为这些领域的开发提供更强大的工具和支持。
9.3 代码优化和性能提升
编译器可能会对类模板的静态成员和非类型参数进行更高效的优化,减少代码膨胀和运行时开销,提高程序的性能。
总之,类模板的静态成员和非类型参数是C++语言中非常重要的特性,它们为开发者提供了强大的功能和灵活性。通过深入理解和合理使用这些特性,可以编写出更加高效、安全和可维护的代码。同时,关注这些特性的未来发展趋势,也有助于开发者跟上技术的步伐,不断提升自己的编程能力。
超级会员免费看
3321

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



