需要预先学习:


1. 函数模版

函数模版(function template)是一份"函数蓝图"——你只需要写一次逻辑,编译器就能为不同的类型自动生成对应的函数版本。


1.1 基础语法

函数模版的声明以 template <typename T> 开头,其中 T 是一个模版参数(template parameter),代表一个待定的类型。使用时,编译器会根据你传入的实参类型推导出 T 的具体类型。

#include <iostream>

template <typename T>
T max(T a, T b)
{
    return (a > b) ? a : b;
}

int main()
{
    std::cout << max(3, 7)       << '\n';  // T 推导为 int,输出 7
    std::cout << max(3.14, 2.71) << '\n';  // T 推导为 double,输出 3.14
    std::cout << max('a', 'z')   << '\n';  // T 推导为 char,输出 z
}

注:你也可以显式指定类型——max<int>(3.14, 2.71) 会强制以 int 实例化,结果是 3。


1.2 多模板参数函数模版

当函数参数涉及多种类型时,可以声明多个模版参数:

template <typename T1, typename T2>
void print_pair(T1 a, T2 b)
{
    std::cout << a << " 和 " << b << '\n';
}

返回类型也可以是一个模版参数——但此时编译器无法从参数推导返回类型,你必须显式指定:

template <typename R, typename T1, typename T2>
R sum(T1 a, T2 b)
{
    return a + b;
}

int main()
{
    auto r = sum<double>(3, 4.5);  // 显式指定 R=double,T1 和 T2 由参数推导
    std::cout << r << '\n';         // 输出 7.5
}

注:显式指定的模版实参按从左到右的顺序匹配——先指定 R,剩下的 T1、T2 由编译器推导。如果某个参数无法推导,你也需要一并显式给出。


1.3 模版特化

模版特化(template specialization)允许你为某个特定类型提供一套"定制版"的实现。对于函数模版,只能做全特化(full specialization):

#include <iostream>
#include <cstring>

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 (std::strcmp(a, b) > 0) ? a : b;
}

int main()
{
    std::cout << max(3, 7)             << '\n';     // 使用主模版
    std::cout << max("hello", "world") << '\n';     // 使用特化版本
}

注:函数模版不支持偏特化(partial specialization)——如果你需要"针对指针类型统一处理"这种逻辑,请使用重载而非特化。


1.4 模版函数重载

函数模版可以和普通函数一样被重载(overload)。重载和特化是两种不同的机制,它们的关键区别在于:

机制 行为
重载 新增一个独立的函数(或函数模版),与原名共享同一个重载决议
特化 为已有模版的某个具体类型替换实现,不引入新的函数

重载决议的优先级:普通函数 > 特化版本 > 主模版。

#include <iostream>
#include <cstring>

// 主模版
template <typename T>
T max(T a, T b)
{
    return (a > b) ? a : b;
}

// 重载:专门处理指针
template <typename T>
T* max(T* a, T* b)
{
    return (*a > *b) ? a : b;
}

// 普通函数重载:专门处理 C 风格字符串
const char* max(const char* a, const char* b)
{
    return (std::strcmp(a, b) > 0) ? a : b;
}

int main()
{
    int x = 3, y = 7;
    std::cout << *max(&x, &y) << '\n';       // 调用指针重载,输出 7

    std::cout << max("abc", "xyz") << '\n';   // 调用普通函数,输出 xyz
}

2. 类模版

类模版(class template)把"类型参数化"的思想延伸到类身上——同一个类定义可以适配多种元素类型,如 std::vector<int> 和 std::vector<double> 来自同一个类模版。


2.1 基础语法

#include <iostream>

template <typename T>
class Box
{
public:
    explicit Box(T val) : m_value(val) {}

    T get() const { return m_value; }
    void set(T val) { m_value = val; }

private:
    T m_value;
};

int main()
{
    Box<int>    intBox(42);
    Box<double> dblBox(3.14);

    std::cout << intBox.get() << '\n';   // 输出 42
    std::cout << dblBox.get() << '\n';   // 输出 3.14
}

与函数模版不同,类模版的模版参数必须显式指定(如 Box<int>),编译器不会为类模版做类型推导(C++17 引入了类模版实参推导 CTAD,但这是后话)。


2.2 多模版参数的类模版

template <typename K, typename V>
class Pair
{
public:
    Pair(K key, V val) : m_key(key), m_value(val) {}

    K key()   const { return m_key; }
    V value() const { return m_value; }

private:
    K m_key;
    V m_value;
};

int main()
{
    Pair<std::string, int> p("age", 25);
    std::cout << p.key() << ": " << p.value() << '\n';  // 输出 age: 25
}

2.3 类模版的全特化与偏特化

与函数模版不同,类模版同时支持全特化和偏特化。

全特化:为所有模版参数指定具体类型:

// 主模版
template <typename T>
class Storage
{
public:
    void info() { std::cout << "通用存储\n"; }
};

// 全特化:T = bool
template <>
class Storage<bool>
{
public:
    void info() { std::cout << "bool 专用存储(使用位压缩)\n"; }
};

偏特化:只固定部分模版参数,或为参数添加约束。这是类模版独有的能力:

// 偏特化:当模版参数是指针类型时
template <typename T>
class Storage<T*>
{
public:
    void info() { std::cout << "指针专用存储\n"; }
};

// 偏特化:两个参数类型相同时
template <typename T>
class Pair<T, T>
{
    // 特化版本的实现……
};

int main()
{
    Storage<int>  s1;   s1.info();  // 输出:通用存储
    Storage<bool> s2;   s2.info();  // 输出:bool 专用存储(使用位压缩)
    Storage<int*> s3;   s3.info();  // 输出:指针专用存储
}

3. 模版

3.1 底层原理

模版的关键在于:只有当模版被使用(实例化)时,编译器才会生成对应的代码。

template <typename T>
T add(T a, T b)
{
    return a + b;
}

int main()
{
    add(1, 2);       // 编译器生成 add<int>
    add(1.5, 2.5);   // 编译器生成 add<double>
    
    // 从未调用过 add<std::string>,所以不会产生相关代码
}

这个过程称为隐式实例化(implicit instantiation)。编译器为每种使用过的类型组合独立生成一份函数(或类)的二进制代码——所以 add<int> 和 add<double> 在底层是两个完全不同的函数。

这也解释了为什么模版代码通常必须写在头文件中:编译器在实例化时需要看到模版的完整定义,而不仅仅是声明。

注:如果你希望将实例化限制在某个翻译单元内,可以使用显式实例化(explicit instantiation)——template int add<int>(int, int);,让编译器提前生成好对应版本,其他翻译单元只需声明即可。


3.2 非类型参数模版(常量参数模版)

模版参数不仅可以是类型,还可以是编译期常量(整数、枚举、指针等)。这类参数称为非类型模版参数(non-type template parameter)。

#include <iostream>

template <typename T, int N>
class Array
{
public:
    int size() const { return N; }

    T& operator[](int i) { return m_data[i]; }

private:
    T m_data[N];
};

int main()
{
    Array<int, 5> arr;
    std::cout << "数组大小: " << arr.size() << '\n';  // 输出 5

    for (int i = 0; i < 5; ++i)
        arr[i] = i * 10;

    for (int i = 0; i < 5; ++i)
        std::cout << arr[i] << ' ';                   // 输出 0 10 20 30 40
}

N 的值在编译期就已确定,所以 Array<int, 5> 和 Array<int, 10> 是两个不同的类型。


3.3 默认模版参数

模版参数也可以有默认值,规则与函数默认实参类似——有默认值的参数必须出现在参数列表的右侧:

#include <iostream>
#include <vector>

// 默认使用 vector 作为容器
template <typename T, typename Container = std::vector<T>>
class Stack
{
public:
    void push(const T& val) { m_container.push_back(val); }
    void pop()              { m_container.pop_back(); }
    T&   top()              { return m_container.back(); }

private:
    Container m_container;
};

int main()
{
    Stack<int> s1;                          // 使用默认的 vector<int>
    s1.push(10);
    std::cout << s1.top() << '\n';          // 输出 10
}

函数模版同样支持默认模版参数:

template <typename T = int>
T multiply(T a, T b)
{
    return a * b;
}

int main()
{
    std::cout << multiply(3, 4)     << '\n';  // T 推导为 int,输出 12
    std::cout << multiply<>(3.0, 4) << '\n';  // T 使用默认值 int,输出 12
}