C++类模板的参数设置
C++类模板参数的基本概念
在C++中,类模板允许我们定义一种通用的类,该类的某些类型或值在实例化时才确定。类模板的参数设置是实现这种通用性的关键。类模板参数主要分为两类:类型参数(type parameters)和非类型参数(non - type parameters)。
类型参数
类型参数是最常见的类模板参数类型。它允许我们在实例化模板类时指定不同的数据类型。例如,考虑一个简单的Stack
类模板,用于实现一个栈数据结构:
template <typename T>
class Stack {
private:
T* data;
int topIndex;
int capacity;
public:
Stack(int size) : topIndex(-1), capacity(size) {
data = new T[capacity];
}
~Stack() {
delete[] data;
}
void push(T value) {
if (topIndex < capacity - 1) {
data[++topIndex] = value;
}
}
T pop() {
if (topIndex >= 0) {
return data[topIndex--];
}
// 这里可以抛出异常,为简单起见暂未实现
return T();
}
};
在上述代码中,typename T
声明了一个类型参数T
。当我们实例化Stack
类时,就可以指定T
的具体类型,比如:
Stack<int> intStack(10);
Stack<double> doubleStack(20);
这里分别创建了一个存储int
类型数据的栈intStack
和一个存储double
类型数据的栈doubleStack
。
非类型参数
非类型参数是在模板定义中使用常量表达式作为参数。这些参数必须是编译期可确定的值。例如,我们可以修改上述Stack
类模板,使其大小也作为模板参数:
template <typename T, int size>
class Stack {
private:
T data[size];
int topIndex;
public:
Stack() : topIndex(-1) {}
~Stack() {}
void push(T value) {
if (topIndex < size - 1) {
data[++topIndex] = value;
}
}
T pop() {
if (topIndex >= 0) {
return data[topIndex--];
}
// 这里可以抛出异常,为简单起见暂未实现
return T();
}
};
在这个版本中,int size
是一个非类型参数。实例化时,必须提供一个编译期常量值:
Stack<int, 10> intStack;
Stack<double, 20> doubleStack;
注意,非类型参数必须是编译期常量,例如不能使用运行时才确定的变量来实例化模板:
// 错误示例
int num = 10;
// Stack<int, num> errorStack; // 编译错误,num不是编译期常量
类模板的默认模板参数
C++ 允许为类模板参数提供默认值,这增加了模板使用的灵活性。
类型参数的默认值
对于类型参数,我们可以这样设置默认值:
template <typename T = int>
class Container {
private:
T value;
public:
Container(T v = T()) : value(v) {}
T getValue() const {
return value;
}
};
在上述代码中,typename T = int
表示T
的默认类型为int
。这样,在实例化Container
类时,如果不指定类型参数,就会使用默认的int
类型:
Container<> defaultContainer; // 等同于 Container<int> defaultContainer;
int val = defaultContainer.getValue();
Container<double> doubleContainer(3.14);
double dVal = doubleContainer.getValue();
非类型参数的默认值
非类型参数同样可以有默认值:
template <typename T, int size = 10>
class FixedSizeArray {
private:
T data[size];
public:
FixedSizeArray() {}
T& operator[](int index) {
if (index >= 0 && index < size) {
return data[index];
}
// 这里可以抛出异常,为简单起见暂未实现
static T dummy;
return dummy;
}
};
这里int size = 10
为非类型参数size
提供了默认值10
。实例化时,如果不指定size
,就会使用默认值:
FixedSizeArray<int> defaultArray; // 等同于 FixedSizeArray<int, 10> defaultArray;
FixedSizeArray<double, 20> customArray;
类模板的模板参数
C++ 还支持将模板作为类模板的参数,这使得模板的嵌套使用成为可能,极大地增强了模板的表达能力。
模板模板参数的定义
假设我们有一个简单的List
类模板:
template <typename T>
class List {
// 简单实现,这里省略具体数据结构和方法
};
现在我们可以定义一个Container
类模板,它以List
这样的模板作为参数:
template <template <typename> class TemplateType, typename T>
class Container {
private:
TemplateType<T> data;
public:
// 可以在这里添加操作 TemplateType<T> 的方法
};
在上述代码中,template <typename> class TemplateType
声明了一个模板模板参数TemplateType
,它要求传入的模板必须接受一个类型参数。
模板模板参数的使用
我们可以这样实例化Container
类:
Container<List, int> intContainer;
这里将List
模板作为参数传入Container
,并指定List
内部的数据类型为int
。
模板模板参数的约束
模板模板参数有一些约束条件。例如,传入的模板必须满足模板模板参数所要求的参数列表形式。假设我们有另一个模板:
template <typename T, int size>
class FixedSizeList {
// 简单实现,这里省略具体数据结构和方法
};
如果尝试将FixedSizeList
作为Container
的模板模板参数,会导致编译错误,因为FixedSizeList
接受两个参数,而Container
的模板模板参数TemplateType
要求接受一个参数:
// 错误示例
// Container<FixedSizeList, int> errorContainer; // 编译错误
类模板参数的作用域
模板参数的作用域从其声明处开始,到模板声明或定义的末尾结束。
模板参数在类内部的作用域
在类模板内部,模板参数可以直接使用。例如:
template <typename T>
class MyClass {
private:
T value;
public:
MyClass(T v) : value(v) {}
T getValue() const {
return value;
}
};
这里在MyClass
类的成员变量定义和成员函数中,都直接使用了模板参数T
。
模板参数在类外部成员定义中的作用域
当在类模板外部定义成员函数时,也需要在函数定义处指定模板参数:
template <typename T>
class OuterClass {
public:
void printValue(T value);
};
template <typename T>
void OuterClass<T>::printValue(T value) {
std::cout << "The value is: " << value << std::endl;
}
在上述代码中,在类外部定义printValue
函数时,再次指定了模板参数T
,以确保函数定义在正确的模板参数作用域内。
模板参数作用域的嵌套
如果存在嵌套的模板,内层模板参数的作用域也遵循相同的规则。例如:
template <typename T>
class OuterTemplate {
public:
template <typename U>
class InnerTemplate {
private:
T outerValue;
U innerValue;
public:
InnerTemplate(T outer, U inner) : outerValue(outer), innerValue(inner) {}
void printValues() {
std::cout << "Outer value: " << outerValue << ", Inner value: " << innerValue << std::endl;
}
};
};
这里InnerTemplate
的模板参数U
的作用域在InnerTemplate
类内部,而OuterTemplate
的模板参数T
的作用域包括InnerTemplate
类。
类模板参数的替换与实例化
当我们使用类模板创建对象时,编译器会进行模板参数的替换和实例化过程。
模板参数替换
模板参数替换是将模板定义中的参数替换为实际的类型或值的过程。例如,对于以下模板:
template <typename T, int size>
class Array {
private:
T data[size];
public:
Array() {}
T& operator[](int index) {
if (index >= 0 && index < size) {
return data[index];
}
// 这里可以抛出异常,为简单起见暂未实现
static T dummy;
return dummy;
}
};
当我们实例化Array<int, 5>
时,编译器会将T
替换为int
,size
替换为5
,生成一个针对int
类型、大小为5
的Array
类的具体实现。
模板实例化
模板实例化是根据模板参数替换后的结果生成实际类定义的过程。模板实例化分为隐式实例化和显式实例化。
- 隐式实例化:当我们第一次使用模板类创建对象时,编译器会隐式实例化该模板。例如:
Array<int, 5> intArray; // 隐式实例化 Array<int, 5>
编译器会在需要时,根据模板定义生成Array<int, 5>
类的具体代码。
- 显式实例化:我们也可以显式地要求编译器实例化模板。例如:
template class Array<int, 5>;
显式实例化通常用于在多个编译单元中共享模板实例,以避免重复实例化带来的开销。例如,在一个库中,我们可以在某个源文件中显式实例化常用的模板类,其他使用该库的代码就不需要再次实例化。
实例化过程中的错误处理
在模板参数替换和实例化过程中,如果出现类型不匹配或其他错误,编译器会给出错误信息。例如,如果我们尝试实例化Array<std::string, -1>
,由于size
必须是正整数,编译器会报错。错误信息可能会比较复杂,因为模板实例化过程涉及到多层替换和检查。但通过仔细分析错误信息,我们可以定位到问题所在。
类模板参数与继承和多态
类模板在继承和多态方面有一些特殊的行为和考虑。
类模板的继承
- 从模板类继承:一个普通类可以从模板类继承,例如:
template <typename T>
class BaseTemplate {
public:
T value;
BaseTemplate(T v) : value(v) {}
};
class DerivedClass : public BaseTemplate<int> {
public:
DerivedClass(int v) : BaseTemplate<int>(v) {}
void printValue() {
std::cout << "Derived value: " << value << std::endl;
}
};
这里DerivedClass
从BaseTemplate<int>
继承,继承了BaseTemplate
类中int
类型的value
成员变量和构造函数。
- 模板类从模板类继承:模板类也可以从其他模板类继承。例如:
template <typename T>
class Base {
public:
T baseValue;
Base(T v) : baseValue(v) {}
};
template <typename T>
class Derived : public Base<T> {
public:
T derivedValue;
Derived(T base, T derived) : Base<T>(base), derivedValue(derived) {}
void printValues() {
std::cout << "Base value: " << this->baseValue << ", Derived value: " << derivedValue << std::endl;
}
};
在上述代码中,Derived
模板类从Base
模板类继承,并且继承的模板参数与自身的模板参数相同。实例化时:
Derived<int> derivedObj(10, 20);
derivedObj.printValues();
类模板与多态
多态性在类模板中同样适用,但需要注意一些细节。由于模板是在编译期实例化,虚函数机制在模板中需要特别处理。例如:
template <typename T>
class Shape {
public:
virtual T area() const = 0;
};
template <typename T>
class Circle : public Shape<T> {
private:
T radius;
public:
Circle(T r) : radius(r) {}
T area() const override {
return 3.14 * radius * radius;
}
};
template <typename T>
class Rectangle : public Shape<T> {
private:
T length;
T width;
public:
Rectangle(T l, T w) : length(l), width(w) {}
T area() const override {
return length * width;
}
};
这里定义了Shape
模板类及其派生类Circle
和Rectangle
,通过虚函数area
实现多态。在使用时:
Shape<int>* shapes[2];
shapes[0] = new Circle<int>(5);
shapes[1] = new Rectangle<int>(4, 6);
for (int i = 0; i < 2; ++i) {
std::cout << "Area: " << shapes[i]->area() << std::endl;
delete shapes[i];
}
注意,由于模板的实例化特性,不同类型参数的模板实例(如Shape<int>
和Shape<double>
)是完全不同的类型,它们之间不存在多态关系。
类模板参数的高级应用
- 元编程(Meta - programming):类模板参数在元编程中起着核心作用。元编程是编写生成代码的代码,通常在编译期执行。例如,通过类模板参数和递归实例化,可以实现编译期计算。考虑以下计算阶乘的元编程示例:
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
这里Factorial
类模板通过递归实例化,在编译期计算阶乘。使用时:
int result = Factorial<5>::value; // result 为 120
- 类型萃取(Type Traits):类型萃取是一种在编译期获取类型信息的技术。C++标准库提供了许多类型萃取模板,如
std::is_integral
用于判断一个类型是否为整数类型。我们可以自己实现简单的类型萃取模板:
template <typename T>
struct IsPointer {
static const bool value = false;
};
template <typename T>
struct IsPointer<T*> {
static const bool value = true;
};
这里通过偏特化实现了判断一个类型是否为指针类型。使用时:
std::cout << "Is int a pointer? " << (IsPointer<int>::value? "Yes" : "No") << std::endl;
std::cout << "Is int* a pointer? " << (IsPointer<int*>::value? "Yes" : "No") << std::endl;
- 策略模式(Policy - based Design):类模板参数可以用于实现策略模式。策略模式允许在运行时选择算法的行为。通过模板参数,我们可以在编译期选择不同的策略。例如,考虑一个排序类模板,我们可以通过模板参数选择不同的排序算法:
template <typename T, typename SortPolicy>
class Sorter {
private:
T* data;
int size;
public:
Sorter(T* arr, int sz) : data(arr), size(sz) {}
void sort() {
SortPolicy::sort(data, size);
}
};
// 简单的冒泡排序策略
template <typename T>
struct BubbleSortPolicy {
static void sort(T* data, int size) {
for (int i = 0; i < size - 1; ++i) {
for (int j = 0; j < size - i - 1; ++j) {
if (data[j] > data[j + 1]) {
T temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
}
};
// 简单的插入排序策略
template <typename T>
struct InsertionSortPolicy {
static void sort(T* data, int size) {
for (int i = 1; i < size; ++i) {
T key = data[i];
int j = i - 1;
while (j >= 0 && data[j] > key) {
data[j + 1] = data[j];
--j;
}
data[j + 1] = key;
}
}
};
使用时:
int arr[5] = {3, 1, 4, 1, 5};
Sorter<int, BubbleSortPolicy<int>> bubbleSorter(arr, 5);
bubbleSorter.sort();
// 输出排序后的数组
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
Sorter<int, InsertionSortPolicy<int>> insertionSorter(arr, 5);
insertionSorter.sort();
// 输出排序后的数组
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
通过类模板参数,我们可以在编译期灵活选择不同的排序策略,提高了代码的可维护性和可扩展性。
通过深入理解和灵活运用C++类模板的参数设置,我们可以编写出更加通用、高效且灵活的代码,充分发挥C++模板编程的强大功能。无论是简单的数据结构实现,还是复杂的元编程和设计模式应用,类模板参数都为我们提供了丰富的可能性。在实际编程中,需要根据具体需求合理选择和设置模板参数,以达到最佳的编程效果。