模板与继承:类模板的继承,模板类作为基类,CRTP(奇异递归模板模式)
模板和继承,这两个C++里最强大的特性,一旦结合起来,能玩出的花样远超你想象。今天我们就来聊聊这个组合拳——类模板的继承、模板类作为基类,以及那个名字听起来很唬人的CRTP。
说实话,我刚接触模板继承那会儿,也觉得不就是把模板套在继承上嘛,能有多复杂?直到我在一个网络库项目里,因为基类模板实例化搞错了,编译报错长达几百行,排查了整整一个下午……嗯,从那以后我再也不敢小看这个主题了。
一、类模板的继承——基础中的基础
先看最直观的场景:一个普通类继承自模板类,或者模板类继承自另一个模板类。
template<typename T>
class Base {
public:
T value;
void show() { std::cout << value << std::endl; }
};
// 普通类继承模板类(需要指定具体类型)
class DerivedInt : public Base<int> {
public:
void set(int v) { value = v; }
};
// 模板类继承模板类
template<typename T>
class DerivedT : public Base<T> {
public:
void printTwice() {
// 注意:这里不能直接写 value,需要加 this->
std::cout << this->value << " " << this->value << std::endl;
}
};
这里有个坑,我当年踩过。在DerivedT里访问基类的value成员,直接写value会报错。为什么?因为编译器在解析模板时,基类Base<T>依赖于模板参数T,属于依赖型基类。编译器不会在依赖型基类中查找名字,你得用this->或者Base<T>::来显式告诉它。
注意:依赖型基类的成员访问,必须使用 this-> 或 Base<T>:: 限定。这是C++模板两阶段查找的规则,别问我为什么,记住就行。
二、模板类作为基类——设计模式的好帮手
模板类作为基类,最常见的用途是实现代码复用和策略模式。我参与过一个日志库的设计,就是用模板基类来统一不同日志级别的处理逻辑。
// 模板基类:定义日志处理的骨架
template<typename Policy>
class LoggerBase : public Policy {
public:
void log(const std::string& msg) {
if (this->shouldLog(msg)) { // 调用策略类的接口
this->write(msg); // 调用策略类的实现
}
}
};
// 策略类1:写入文件
class FilePolicy {
protected:
bool shouldLog(const std::string&) { return true; }
void write(const std::string& msg) {
// 写入文件的具体实现
}
};
// 策略类2:写入网络
class NetworkPolicy {
protected:
bool shouldLog(const std::string&) { return true; }
void write(const std::string& msg) {
// 发送到网络的具体实现
}
};
// 使用
using FileLogger = LoggerBase<FilePolicy>;
using NetworkLogger = LoggerBase<NetworkPolicy>;
你看,通过模板参数传入不同的策略类,LoggerBase就能表现出不同的行为。这比运行时多态更高效——没有虚函数开销,所有调用在编译期就确定了。
个人建议:如果你的策略在编译期就能确定,优先用模板基类+策略模式。运行时多态虽然灵活,但虚函数调用的开销在某些场景下(比如高频日志)是不可忽视的。
三、CRTP——奇异递归模板模式
终于到了重头戏。CRTP,全称Curiously Recurring Template Pattern,中文叫奇异递归模板模式。名字很怪,但说白了就是:派生类把自己作为模板参数传给基类。
template<typename Derived>
class Base {
public:
void interface() {
// 调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
static void staticInterface() {
Derived::staticImplementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived implementation" << std::endl;
}
static void staticImplementation() {
std::cout << "Static impl" << std::endl;
}
};
我第一次看到这个模式时,心里想的是:这玩意儿能编译通过?基类还没定义完,派生类就传进去了?实际上,模板只有在实例化时才会完全展开,而Base<Derived>的实例化发生在Derived定义完成之后,所以没问题。
CRTP的典型应用场景
1. 静态多态(模拟虚函数)
你想想看,虚函数虽然好用,但每个对象都要多一个虚表指针,而且虚函数调用不能内联。CRTP可以实现编译期多态,没有这些开销。
template<typename T>
class Shape {
public:
double area() const {
return static_cast<const T*>(this)->areaImpl();
}
};
class Circle : public Shape<Circle> {
double radius;
public:
Circle(double r) : radius(r) {}
double areaImpl() const { return 3.14159 * radius * radius; }
};
class Square : public Shape<Square> {
double side;
public:
Square(double s) : side(s) {}
double areaImpl() const { return side * side; }
};
// 使用模板函数实现多态
template<typename T>
void printArea(const Shape<T>& s) {
std::cout << s.area() << std::endl;
}
2. 对象计数
我曾经需要统计某个类的实例数量,用CRTP实现起来非常优雅:
template<typename T>
class ObjectCounter {
static inline size_t count = 0;
protected:
ObjectCounter() { ++count; }
ObjectCounter(const ObjectCounter&) { ++count; }
~ObjectCounter() { --count; }
public:
static size_t liveCount() { return count; }
};
class MyClass : public ObjectCounter<MyClass> {
// 自己的实现
};
每个MyClass的实例都会自动被计数,而且不同派生类有自己独立的计数器。这比在MyClass里手动加静态变量省事多了。
3. 运算符重载的自动生成
标准库里的std::enable_shared_from_this就是CRTP的经典例子。我自己也常用CRTP来减少重复的运算符重载代码:
template<typename T>
class Comparable {
public:
bool operator!=(const T& other) const {
return !(static_cast<const T*>(this)->operator==(other));
}
};
class Point : public Comparable<Point> {
int x, y;
public:
Point(int a, int b) : x(a), y(b) {}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
你只需要实现==,!=就自动有了。类似的技巧还可以用于<、>、<=、>=等比较运算符。
核心要点:CRTP的本质是编译期多态。它把虚函数调用的运行时开销降为零,同时保留了多态的设计灵活性。代价是代码可读性稍差,而且每个派生类都会生成一份独立的基类代码(代码膨胀)。
四、知识体系总览
下面这张图总结了模板与继承的核心关系,我建议你保存下来,写代码时对照着看:
五、避坑指南与实战建议
讲完了理论,分享几个我实际踩过的坑:
- CRTP与虚函数混用要小心:我曾经在一个CRTP基类里加了虚函数,结果派生类的
static_cast和虚函数调度产生了奇怪的交互。建议要么全用CRTP,要么全用虚函数,别混着来。 - 模板继承链不要太深:超过3层的模板继承,编译错误信息会变得极其恐怖。我见过一个5层模板继承的项目,改一行代码编译要3分钟,排查错误全靠猜。
- CRTP的代码膨胀问题:每个派生类都会实例化一份基类代码。如果你有100个派生类,基类的代码就会被复制100份。对于嵌入式或对体积敏感的场景,要权衡一下。
我的选择标准:
- 需要运行时多态(比如通过基类指针/引用操作不同派生类)→ 用虚函数
- 编译期就能确定类型,且对性能有极致要求 → 用CRTP
- 只是复用一些代码,不涉及多态 → 用普通模板继承或组合
模板与继承的结合,是C++泛型编程里最强大的武器之一。CRTP尤其值得你花时间掌握——它出现在很多现代C++库中,比如std::enable_shared_from_this、Boost的很多组件,以及各种高性能计算框架。理解它,你就能写出更高效、更优雅的代码。
好了,这一章的内容就到这里。记住,模板继承不是银弹,但用对了地方,效果绝对惊艳。
公众号:蓝海资料掘金营,微信deep3321