template:泛型的蓝图

template 关键字用来声明一个模板,你可以把它想象成一个“配方”或“蓝图”,它定义了一类函数或类,但其中某些部分(通常是数据类型)是“待定”的,需要在实际使用时才能确定。

模板 class typename
(图片来源网络,侵删)

a) 函数模板

当你想要编写一个函数,这个函数的逻辑对多种数据类型都适用时,就可以使用函数模板。

示例:一个通用的 swap 函数

// T 是一个模板参数,代表一个待定的类型
template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}
int main() {
    int x = 10, y = 20;
    std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
    swap(x, y); // 编译器在这里实例化一个 swap<int>(int&, int&)
    std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
    std::string s1 = "hello", s2 = "world";
    std::cout << "Before swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
    swap(s1, s2); // 编译器在这里实例化一个 swap<std::string>(std::string&, std::string&)
    std::cout << "After swap: s1 = " << s1 << ", s2 = " << s2 << std::endl;
    return 0;
}

工作原理:

  1. 当编译器遇到 swap(x, y) 时,它会看到 xyint 类型。
  2. 它根据 swap 的模板“蓝图”,自动生成一个专门处理 int 类型的具体函数,这个过程称为模板实例化
  3. 同理,当遇到 swap(s1, s2) 时,编译器又会生成一个专门处理 std::string 类型的具体函数。

T 被称为模板参数,它必须在尖括号 <...> 中声明。

模板 class typename
(图片来源网络,侵删)

b) 类模板

当你想要定义一个类,这个类的数据成员或成员函数的实现依赖于一个或多个数据类型时,就需要使用类模板。

示例:一个简单的栈容器

#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
    std::vector<T> elements; // 使用 std::vector 作为底层容器
public:
    void push(const T& element) {
        elements.push_back(element);
    }
    void pop() {
        if (elements.empty()) {
            throw std::out_of_range("Stack<>::pop(): empty stack");
        }
        elements.pop_back();
    }
    T top() const {
        if (elements.empty()) {
            throw std::out_of_range("Stack<>::top(): empty stack");
        }
        return elements.back();
    }
    bool empty() const {
        return elements.empty();
    }
};
int main() {
    Stack<int> intStack; // 实例化一个 Stack<int> 类
    intStack.push(10);
    intStack.push(20);
    std::cout << "Top of intStack: " << intStack.top() << std::endl; // 输出 20
    Stack<std::string> stringStack; // 实例化一个 Stack<std::string> 类
    stringStack.push("Hello");
    stringStack.push("Templates!");
    std::cout << "Top of stringStack: " << stringStack.top() << std::endl; // 输出 "Templates!"
    return 0;
}

工作原理:

  • Stack<int> 告诉编译器,请基于 Stack 模板创建一个类,其中所有的 T 都被替换为 int
  • Stack<std::string> 则创建另一个类,其中所有的 T 都被替换为 std::string
  • Stack<int>Stack<std::string> 是两个完全不同的类型,它们之间不能相互赋值或操作。

typename:模板参数的声明符

在模板的声明中,typenameclass 几乎可以互换使用,都用来声明一个类型参数。

模板 class typename
(图片来源网络,侵删)
// 以下两种写法是等价的
template <typename T> // 使用 typename
class MyClass { ... };
template <class T>  // 使用 class
class MyClass { ... };

为什么会有两个关键字?

  • class 是 C++ 早期版本引入的,因为它更符合“类模板”的概念。
  • typename 是后来为了更清晰、更准确地表达“这是一个类型”而引入的,在模板参数列表中,推荐使用 typename,因为它在更复杂的上下文中具有唯一的意义。

typename 的核心用途:在模板内部指定“这是一个类型”

这是 typename 最重要、也是最容易被忽略的用法,在模板定义的内部,编译器有时无法确定一个名字(MyClass<T>::value_type)是一个类型还是一个静态成员变量。

示例:一个迭代器适配器

假设我们想写一个模板,它能接受一个迭代器,并获取该迭代器指向的值的类型。

#include <vector>
#include <list>
#include <iostream>
// 假设我们有一个自定义迭代器
template <typename T>
struct MyIterator {
    using value_type = T; // C++11 推荐的内部类型别名写法
    // ... 其他迭代器成员 ...
};
// 模板函数,打印容器中元素的类型
template <typename Iterator>
void print_element_type(Iterator it) {
    // 错误写法:
    // Iterator::value_type x; 
    // 在模板内部,编译器看到 Iterator::value_type,它不知道 value_type 是一个类型还是一个静态成员变量。
    // C++ 规定,当编译器无法确定时,默认它是一个对象(非类型)。
    // 必须用 typename 明确告诉编译器:“嘿,这里的 Iterator::value_type 是一个类型!”
    // 正确写法:
    typename Iterator::value_type x;
    std::cout << "Element type is: " << typeid(x).name() << std::endl;
}
int main() {
    std::vector<int> v;
    print_element_type(v.begin()); // std::vector<int>::iterator 的 value_type 是 int
    std::list<double> l;
    print_element_type(l.begin()); // std::list<double>::iterator 的 value_type 是 double
    MyIterator<std::string> my_it;
    print_element_type(my_it); // MyIterator<std::string> 的 value_type 是 std::string
    return 0;
}

规则: 在模板定义中,如果依赖于模板参数的名称是一个类型,并且它被用作一个类型限定符()的前缀,那么这个类型名称前必须加上 typename

例外情况:

  • 在基类列表中或成员初始化列表中,typename 是可选的。
    template <typename T>
    class Derived : public Base<T> { // 这里的 Base<T> 不需要 typename
    public:
        Derived() : Base<T>() {} // 这里的 Base<T> 也不需要 typename
    };
  • 在构造函数的成员初始化列表中,typename 是可选的。
  • 如果类型名是 templatetypename 的组合,情况会更复杂,但核心思想相同。

templatetypename 的高级用法

a) 非类型模板参数

模板参数不仅可以是类型,还可以是常量表达式(整数、指针、引用等)。

// N 是一个非类型模板参数,必须在编译时确定
template <typename T, int N>
class Array {
private:
    T data[N]; // 使用 N 来声明数组大小
public:
    T& operator[](int index) {
        if (index < 0 || index >= N) {
            throw std::out_of_range("Index out of bounds");
        }
        return data[index];
    }
    int size() const { return N; }
};
int main() {
    Array<int, 100> intArray; // 一个可以存储100个int的数组
    intArray[0] = 42;
    std::cout << "Array size: " << intArray.size() << std::endl; // 输出 100
    // Array<double, 10.5> doubleArray; // 错误!10.5 不是整数常量表达式
    // Array<int, dynamic_size> dynamic; // 错误!dynamic_size 不是编译时常量
    return 0;
}

b) 默认模板参数

可以为模板参数提供默认值。

template <typename T = int, int N = 100>
class DefaultArray {
    // ...
};
int main() {
    DefaultArray<> arr1; // 使用默认参数 T=int, N=100
    DefaultArray<double> arr2; // T=double, N=100 (N 使用默认值)
    DefaultArray<double, 50> arr3; // T=double, N=50
    return 0;
}

关键字 主要用途 示例 核心思想
template 声明一个泛型函数或类,定义一个“蓝图”。 template <typename T> void foo(T t); 编写与数据类型无关的通用代码。
typename 在模板参数列表中声明一个类型参数(与 class 等价)。
在模板定义内部,明确指出一个依赖于模板参数的名字是一个类型
template <typename T>
typename T::iterator it;
定义模板的“类型占位符”。
消除编译器在模板内部对“类型”和“变量”的歧义。

简单记忆:

  • template <...> 是写在函数或类定义的最前面,告诉编译器“我要开始写一个泛型了”。
  • typename 是写在模板函数或类的内部,用来解决“这个依赖模板参数的名字到底是不是个类型?”的问题,在模板参数列表里,它只是用来声明一个类型参数。