C++的模板和继承都是实现代码复用和扩展的重要特性,将两者结合可以设计出更灵活、通用性更强的代码结构,派生类模板的设计模式也在很多大型项目和标准库中有广泛应用。

模板与继承结合的基础方式
模板和继承结合的核心思路是,将基类设计为模板类,派生类可以继承这个模板基类,并根据需要指定基类的模板参数,或者派生类本身也是模板类,继承非模板或模板基类。下面是一个最基础的结合示例:
// 模板基类
template <typename T>
class Base {
public:
T value;
Base(T v) : value(v) {}
virtual T getValue() const {
return value;
}
virtual ~Base() = default;
};
// 非模板派生类,指定基类模板参数为int
class DerivedInt : public Base<int> {
public:
DerivedInt(int v) : Base<int>(v) {}
int getValue() const override {
return value * 2;
}
};
// 模板派生类,继承模板基类,派生类模板参数传递给基类
template <typename T>
class DerivedGeneric : public Base<T> {
public:
DerivedGeneric(T v) : Base<T>(v) {}
T getValue() const override {
return this->value + 1; // 访问基类成员需要加this->,避免模板编译时的名称查找问题
}
};
典型的派生类模板设计模式
1. CRTP模式(奇异递归模板模式)
CRTP是模板与继承结合最经典的设计模式,它的特点是派生类作为模板参数传递给基类,基类可以在编译期调用派生类的方法,实现静态多态,避免虚函数的运行时开销。
// CRTP基类,模板参数为派生类类型
template <typename Derived>
class CRTPBase {
public:
void interface() {
// 编译期调用派生类的实现方法
static_cast<Derived*>(this)->implementation();
}
// 默认实现,派生类可以重写
void implementation() {
// 默认逻辑
}
};
// 派生类,继承时把自身作为模板参数传给基类
class DerivedA : public CRTPBase<DerivedA> {
public:
void implementation() {
// 派生类自定义实现
}
};
class DerivedB : public CRTPBase<DerivedB> {
// 不重写implementation,使用基类的默认实现
};
int main() {
DerivedA a;
a.interface(); // 调用DerivedA的implementation
DerivedB b;
b.interface(); // 调用CRTPBase的默认implementation
return 0;
}
2. 参数化派生类模式
这种模式中派生类本身是模板类,继承的基类可以是普通类或者模板类,通过派生类的模板参数控制基类的特性或者派生类自身的扩展行为,常用于需要灵活扩展功能的场景。
// 普通基类
class Logger {
public:
void log(const std::string& msg) {
// 日志输出逻辑
}
};
// 模板派生类,模板参数控制是否启用日志功能
template <bool EnableLog>
class DataProcessor : public Logger {
public:
void process(int data) {
if constexpr (EnableLog) {
log("开始处理数据");
}
// 数据处理逻辑
if constexpr (EnableLog) {
log("数据处理完成");
}
}
};
int main() {
DataProcessor<true> processorWithLog;
processorWithLog.process(10); // 会输出日志
DataProcessor<false> processorNoLog;
processorNoLog.process(20); // 不会输出日志
return 0;
}
3. 模板基类与多态结合模式
这种模式中模板基类包含虚函数,派生类继承模板基类并重写虚函数,既可以利用模板的泛型能力,又可以保留运行时的多态特性,适合需要同时支持泛型操作和动态绑定的场景。
#include <iostream>
#include <vector>
#include <memory>
// 模板基类,包含虚函数
template <typename T>
class Shape {
public:
virtual T area() const = 0;
virtual ~Shape() = default;
};
// 派生类1,继承模板基类,指定模板参数为double
class Circle : public Shape<double> {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14 * radius * radius;
}
};
// 派生类2,继承模板基类,指定模板参数为int
class Square : public Shape<int> {
private:
int side;
public:
Square(int s) : side(s) {}
int area() const override {
return side * side;
}
};
int main() {
// 注意:不同模板参数的基类是不同的类型,无法直接放到同一个多态容器中
// 如果需要统一容器,可以再抽象一层非模板基类
std::vector<std::unique_ptr<Shape<double>>> doubleShapes;
doubleShapes.push_back(std::make_unique<Circle>(5.0));
for (auto& shape : doubleShapes) {
std::cout << shape->area() << std::endl;
}
return 0;
}
结合使用的注意事项
- 模板基类的成员在派生类中访问时,如果依赖模板参数,需要加
this->或者使用using声明,避免编译时的名称查找错误。 - CRTP模式是编译期多态,没有虚函数的运行时开销,但无法实现运行时的动态绑定,需要根据场景选择。
- 模板和继承结合会增加代码的编译复杂度,可能导致编译错误信息较长,调试时需要注意。
- 不同模板参数实例化的基类是不同的类型,无法直接使用普通的多态容器存储,需要额外设计抽象层。