什么是泛型编程?
泛型编程是一种编程范式,核心思想是编写与具体数据类型无关的通用代码,从而提高代码的复用性和灵活性。允许开发者定义可适应多种数据类型的函数,类或接口,无需为每种类型重复编写相似的代码。
什么是C++模板?
实现泛型编程的机制,可以让开发者编写与数据类型无关的代码,通过编译器自动生成具体类型代码。
作用
- 代码复用
- 类型安全:编译时进行类型检查
- 性能优化:编译生成的代码具体类型化,无运行时类型判断开销
C++模板与泛型编程的关系
C++模板是泛型编程的子集,模板是C++实现泛型编程的核心机制;
泛型编程是一种编程范式,强调与类型无关的通用代码;
而C++模板只是用来实现泛型编程的一种工具,其他语言也可以通过不同形式去实现泛型编程。
语言 | 机制 | 核心特点 |
---|---|---|
C++ | 模板 | 编译时实例化代码,支持元编程,类型完全开放(可能会导致代码膨胀) |
Java | 类型擦除 | 泛型信息在运行时擦除,兼容旧版本,类型约束较弱(需要显示强制转化) |
C# | 运行时保留类型信息 | 泛型代码在运行时保留类型参数,性能更优 |
Rust | 零成本抽象泛型 | 编译时静态派发,通过trait约束类型 |
模板类型与使用
函数模板
以 通用交换函数 举例
#include <iostream>
#include <string>
// 传统方式:为每种类型单独写交换函数
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void swapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
// 模板方式:一个函数处理所有类型
template<typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
// 测试整型交换
int x = 10, y = 20;
mySwap(x, y); // 编译器自动推导类型为int
std::cout << "x=" << x << ", y=" << y << std::endl;
// 测试浮点型交换
double m = 3.14, n = 2.71;
mySwap<double>(m, n); // 显式指定类型
std::cout << "m=" << m << ", n=" << n << std::endl;
// 测试字符串交换
std::string s1 = "Hello", s2 = "World";
mySwap(s1, s2);
std::cout << "s1=" << s1 << ", s2=" << s2 << std::endl;
return 0;
}
类模板
以自定义数组为例
#include <iostream>
#include <stdexcept> // 用于异常处理
template<typename T>
class MyArray {
private:
T* data; // 存储数据的指针
size_t capacity; // 数组容量
size_t size; // 当前元素数量
public:
// 构造函数
MyArray() : data(new T[5]), capacity(5), size(0) {}
// 析构函数
~MyArray() {
delete[] data;
}
// 添加元素
void push_back(const T& value) {
if(size >= capacity) {
// 容量翻倍扩容
capacity *= 2;
T* newData = new T[capacity];
for(size_t i = 0; i < size; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
}
data[size++] = value;
}
// 获取元素
T& operator[](size_t index) {
if(index >= size) {
throw std::out_of_range("Index out of range");
}
return data[index];
}
// 获取数组大小
size_t getSize() const {
return size;
}
// 打印数组内容
void print() const {
std::cout << "[ ";
for(size_t i = 0; i < size; ++i) {
std::cout << data[i] << " ";
}
std::cout << "]\n";
}
};
int main() {
// 创建int类型的数组
MyArray<int> intArray;
intArray.push_back(10);
intArray.push_back(20);
intArray.push_back(30);
std::cout << "Int array: ";
intArray.print(); // 输出: [ 10 20 30 ]
// 创建string类型的数组
MyArray<std::string> strArray;
strArray.push_back("Hello");
strArray.push_back("Template");
strArray.push_back("!");
std::cout << "String array: ";
strArray.print(); // 输出: [ Hello Template ! ]
// 测试越界访问
try {
std::cout << "Third element: " << intArray[2] << std::endl; // 正常
std::cout << "Fifth element: " << intArray[4] << std::endl; // 抛异常
} catch(const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
可能会有小伙伴们问到为什么不使用继承来代替模板的工作,明明都是对代码的扩展,请继续往下看
继承与模板
对比
特性 | 模板 | 继承 |
---|---|---|
扩展维度 | 数据类型 (T) | 行为实现 |
多态类型 | 编译时(静态多态) | 运行时(动态多态) |
代码生成时机 | 编译时实例化 | 运行时通过虚函数派发 |
约束方式 | 隐式概念/显式断言(requires ) | 基类接口(抽象类、纯虚函数) |
典型应用 | 泛型算法、容器 | 多态对象、统一接口的多种实现 |
协同工作
两种技术并非互斥,如 CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)同时使用继承和模板。
CRTP
核心思想:基类将自己定义为模板类,并以派生类作为模板参数,从而在基类中通过静态类型转换(static_cast
)直接访问派生类的成员,实现编译时多态(静态多态)。
- 基类模板化:基类是一个模板类,其模板参数是派生类自身。
- 静态多态:基类通过
static_cast
将this
指针转换为派生类指针,直接调用派生类的成员方法,无需虚函数和运行时开销
基本结构:
template <typename Derived> // 基类是模板类,模板参数为派生类
class Base {
public:
void commonOperation() {
// 通过 static_cast 将基类指针转换为派生类指针,调用派生类方法
static_cast<Derived*>(this)->customStep();
}
};
// 派生类继承基类时,将自己作为模板参数传递给基类,这里已经隐式生成了一个Base<MyClass>
class MyClass : public Base<MyClass> {
public:
void customStep() {
// 派生类实现具体逻辑
std::cout << "MyClass customStep" << std::endl;
}
};
CRTP 的典型应用
运算符重载(Boost.Operators)
Boost 库使用 CRTP 简化运算符重载。例如,实现 operator+
时自动生成 operator+=
。
template <typename Derived>
class Addable {
public:
Derived operator+(const Derived& other) const {
Derived result = static_cast<const Derived&>(*this);
result += other;
return result;
}
};
class Vec3 : public Addable<Vec3> {
public:
Vec3(int x, int y, int z) : x(x), y(y), z(z) {}
Vec3& operator+=(const Vec3& other) {
x += other.x;
y += other.y;
z += other.z;
return *this;
}
int x, y, z;
};
// 使用
Vec3 a(1, 2, 3), b(4, 5, 6);
Vec3 c = a + b; // 调用 Addable<Vec3>::operator+,内部使用 Vec3::operator+=
- 单例模式(编译时单例)
通过 CRTP 实现通用的单例基类。
template <typename T>
class Singleton {
public:
static T& getInstance() {
static T instance;
return instance;
}
protected:
Singleton() = default;
~Singleton() = default;
};
class Logger : public Singleton<Logger> {
friend class Singleton<Logger>; // 允许基类构造 Logger
public:
void log(const std::string& message) { /* ... */ }
};
// 使用
Logger::getInstance().log("Hello CRTP");
CRTP 的优缺点
优点 | 缺点 |
---|---|
消除虚函数调用开销(静态多态) | 代码可读性降低(基类与派生类紧密耦合) |
编译时类型安全(无运行时错误) | 需手动管理类型转换(易出错) |
可复用通用逻辑(如计数器、运算符重载) | 不适用于需要运行时动态派发的场景 |
模板重载 & 模板特化
模板重载
// 函数模板重载示例:处理普通类型和指针类型
template <typename T>
void process(T value) { /* 通用实现 */ }
template <typename T>
void process(T* value) { /* 指针特化逻辑 */ }
模板特化:分为全部特化和部分特化
模板全特化
// 类模板全特化示例:针对 int 类型的特殊实现
template <typename T>
class Container { /* 通用实现 */ };
template <>
class Container<int> { /* int 类型的特化实现 */ }; [9](@ref)
// 函数模板全特化示例:针对 const char* 类型
template <>
const char* max<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b;
}
模板部分特化
// 通用模板
template <typename T1, typename T2, typename T3>
class TCP { /* 通用实现 */ };
// 部分特化:仅第一个参数为 int,第三个为 double
template <typename U>
class TCP<int, U, double> { /* 针对 int 和 double 的优化实现 */ };
特性 | 模板重载 | 模板特例化 |
---|---|---|
本质 | 多个独立模板的共存,通过参数差异区分 | 单一模板的特定类型扩展实现 |
适用范围 | 函数模板、类模板 | 函数全特化、类全特化/部分特化 |
参数匹配 | 基于参数类型和数量的最佳匹配 | 严格匹配特定类型或类型组合 |
实现灵活性 | 可定义完全不同的参数列表 | 必须保持原模板参数结构 |
优先级 | 普通函数 > 模板函数 | 特例化版本 > 通用模板 |
典型应用场景 | 处理不同类型或数量的参数(如指针、数组) | 优化特定类型的性能或功能(如字符串处理) |