模版基础
需要预先学习:
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
}


