MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类模板的参数设置

2023-07-306.6k 阅读

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替换为intsize替换为5,生成一个针对int类型、大小为5Array类的具体实现。

模板实例化

模板实例化是根据模板参数替换后的结果生成实际类定义的过程。模板实例化分为隐式实例化和显式实例化。

  1. 隐式实例化:当我们第一次使用模板类创建对象时,编译器会隐式实例化该模板。例如:
Array<int, 5> intArray; // 隐式实例化 Array<int, 5>

编译器会在需要时,根据模板定义生成Array<int, 5>类的具体代码。

  1. 显式实例化:我们也可以显式地要求编译器实例化模板。例如:
template class Array<int, 5>;

显式实例化通常用于在多个编译单元中共享模板实例,以避免重复实例化带来的开销。例如,在一个库中,我们可以在某个源文件中显式实例化常用的模板类,其他使用该库的代码就不需要再次实例化。

实例化过程中的错误处理

在模板参数替换和实例化过程中,如果出现类型不匹配或其他错误,编译器会给出错误信息。例如,如果我们尝试实例化Array<std::string, -1>,由于size必须是正整数,编译器会报错。错误信息可能会比较复杂,因为模板实例化过程涉及到多层替换和检查。但通过仔细分析错误信息,我们可以定位到问题所在。

类模板参数与继承和多态

类模板在继承和多态方面有一些特殊的行为和考虑。

类模板的继承

  1. 从模板类继承:一个普通类可以从模板类继承,例如:
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;
    }
};

这里DerivedClassBaseTemplate<int>继承,继承了BaseTemplate类中int类型的value成员变量和构造函数。

  1. 模板类从模板类继承:模板类也可以从其他模板类继承。例如:
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模板类及其派生类CircleRectangle,通过虚函数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>)是完全不同的类型,它们之间不存在多态关系。

类模板参数的高级应用

  1. 元编程(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
  1. 类型萃取(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;
  1. 策略模式(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++模板编程的强大功能。无论是简单的数据结构实现,还是复杂的元编程和设计模式应用,类模板参数都为我们提供了丰富的可能性。在实际编程中,需要根据具体需求合理选择和设置模板参数,以达到最佳的编程效果。