Skip to content

14 模板

泛型编程

泛型编程(Generic Programming)是一种编程范式,核心思想是编写与具体数据类型无关的代码,让算法和数据结构能够自动适配多种类型,而不需要为每种类型重复写同样的逻辑。

C++ 中的实现:模板(Template)

泛型编程是一种编译时多态性(静态多态)

实例化(Instantiation):由编译器将通用模板代码转换为不同的实例代码的过程。

函数模板

语法格式:

template <typename 模板形参1, typename 模板形参2>  // 声明模板
返回类型 函数名(参数列表) {
    // 函数体
}

函数模板的模板形参不一定要出现在形参列表里,但如果某个模板参数在调用时不能通过函数参数推导出来,就必须由调用者显式指定或给出默认参数。

  • 如果模板形参出现在函数形参类型中,编译器会尝试从实参中推导它们(常见情况)。
  • 如果某形参只出现在返回类型或函数体中,则不能从调用处推导,调用时须显式指定或该形参必须有默认值。
  • 非类型模板参数同样需要能被推导,否则须显式提供。

示例:

  • 不能被推导(调用时需显式指定):template<typename T, typename U> void f(T); // U 未出现在参数列表,不能推导 → f<int,double>(42);
  • 有默认值:template<typename T, typename U = int> void f(T); // 可以写 f(42);U 默认为 int
  • 只靠返回值无法推导:template<typename T> T g(); // 必须 g<int>()

经典示例(交换函数):

template <typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

调用方式

  1. 自动类型推导:编译器根据传入的实参自动推断出 T 的类型。
    int a = 10, b = 20;
    mySwap(a, b); // 编译器自动推导出 T 为 int
    
  2. 显式指定类型:在函数名后的尖括号中明确指定类型。
    double x = 1.1, y = 2.2;
    mySwap<double>(x, y); // 明确指定 T 为 double
    

注意:此过程中不进行常规的隐式类型转换。 (不能把一个int和一个double传给mySwap

模板(全)特化

模板特化(Template Specialization)是指为特定的类型参数组合,提供一份定制的模板实现,替代编译器自动生成的通用版本。

全特化:为所有模板参数提供具体类型的定制实现:

// 通用模板
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

// 全特化:针对 const char* 的定制版本
template<> // 空参数列表
const char* max(const char* a, const char* b) {
    return strcmp(a, b) > 0 ? a : b;  // 比较字符串内容
}

模板重载

对函数模板进行重载:

  • 定义名字相同而函数形参表不同的函数模板
  • 或者定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为

函数调用的静态绑定规则(重载协议):

  1. 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
  2. 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步
    > 在步骤2中:首先匹配函数模板的特化,再匹配非指定特殊的函数模板
  3. 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹配的函数则调用该函数。否则,进入第4步
  4. 提示编译错误

类模板

语法格式

template <模板形参表>
class 类模板名
{
    类成员声明
    ...
}

其中模板形参表的形式为:template <typename 类型参数1, typename 类型参数2, ...> (注:模板形参每项是非类型形参、类型形参、模板形参之一。)

示例

// 1. 定义类模板
template <typename T>
class Stack {
private:
    T* arr;
    int topIndex;
public:
    void push(T value) { /* 入栈逻辑 */ }
    T pop() { /* 出栈逻辑 */ }
};

// 2. 显式实例化并创建对象
Stack<int> intStack;         
Stack<double> doubleStack;   

在类模板外定义成员函数

语法格式

template <模板形参表>
返回值类型 类模板名<模板形参名列表>::函数名(函数形参表)
{
    函数实现
    ...
}

示例

template <typename T>
void MyClass<T>::show(T val) { }

类模板的文件组织形式

类模板(以及函数模板)在实例化时,编译器必须能看到完整的源代码(头文件中的模板定义)。

普通类在编译时,编译器只需要看到类的声明(通常放在头文件中)即可,而成员函数的具体实现可以放在源文件(.cpp)中。但类模板不同,编译器只有在遇到具体的类型(如 MyClass<int>)进行实例化时,才会生成真正的机器代码。

解决方案

一、包含编译模式

  1. 核心要求 在模板(函数模板/类模板成员函数)被调用实例化的位置,编译器必须能看到该模板的完整实现代码,才能根据传入的类型生成对应的实体代码。
  2. 两种实现手段
    • 方案1:把类模板的声明、成员函数实现全部直接写在头文件(.h/.hpp里,包含头文件就能看到全部代码;
    • 方案2:头文件只写类模板声明,在头文件末尾用#include "模板实现文件.tpp"引入存放成员实现的文件,调用处包含头文件时就会连带引入实现代码。
  3. 特点:所有主流C++编译器都支持,工程里最普遍使用。

.tpp 文件通常用于将 C++ 模板类或模板函数的实现与声明分离,同时避免链接错误。

二、分离编译模式

  1. 核心要求 像普通类一样把声明放头文件、实现放单独.cpp文件,需要在实现文件用export关键字修饰模板,让编译器跨编译单元保留模板定义信息,后续实例化时能找到实现。
  2. 局限 C++标准虽定义了该模式,但绝大多数主流编译器(如GCC、Clang早期版本、MSVC)都不完整支持export关键字,实际开发几乎不会采用这种方式。

非类型模板形参

  • 相当于模板内部的常量,形式上类似于普通的函数形参
  • 对模板进行实例化时,非类型形参由相应模板实参的值代替
  • 对应的模板实参必须是编译时常量表达式
  • C++20以前,非类型模板形参只允许是整型、枚举、指针、引用或指向成员类型的指针。不能是 float、double、string 等。
template<typename T, int N>    // T 是类型参数,N 是非类型参数
class Array {
    T data[N];                 // N 是编译期确定的常量
};

Array<int, 100> arr;           // N = 100,编译期分配 100 个 int

数组的引用

数组的引用就是给整个数组起别名,而不是指向数组首元素的指针。

int arr[5] = {1, 2, 3, 4, 5};

int (&ref)[5] = arr; 
//   ↑ 括号不能省!int& ref[5] 会变成"引用数组"(非法)
数组指针 int* 数组引用 int (&)[5]
类型 指向 int 的指针 绑定到"5 个 int 的数组"
sizeof sizeof(int*)(4/8 字节) sizeof(int[5])(20 字节)
能否重新绑定 可以指向其他地址 不能,引用一旦绑定不可改
退化行为 数组名会退化为指针 保持数组身份,不退化
元素个数信息 丢失 保留

数组引用是模板推导数组大小的经典技巧:

template<typename T, size_t N>
size_t array_size(T (&arr)[N]) {   // 推导 N
    return N;
}

int a[10];
array_size(a);   // 编译器推导 T=int, N=10,不需要传入N

这比 sizeof(arr)/sizeof(arr[0]) 更安全,因为传入指针会编译错误(无法匹配数组引用)。

数组引用 T (&)[N] 是整个数组的别名,它阻止了数组到指针的退化,保留了元素个数信息。 它是模板元编程中推导数组大小的利器,也是函数参数传递数组时避免信息丢失的精确方式。