模板与继承:类模板的继承,模板类作为基类,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(奇异递归模板) 静态多态(模拟虚函数) 对象计数 运算符重载自动生成 核心思想:编译期确定类型关系 避免运行时开销,提升性能 ⚠ 注意事项 依赖型基类需用 this-> 访问成员 | CRTP可能导致代码膨胀 | 模板错误信息难读

五、避坑指南与实战建议

讲完了理论,分享几个我实际踩过的坑:

  • CRTP与虚函数混用要小心:我曾经在一个CRTP基类里加了虚函数,结果派生类的static_cast和虚函数调度产生了奇怪的交互。建议要么全用CRTP,要么全用虚函数,别混着来。
  • 模板继承链不要太深:超过3层的模板继承,编译错误信息会变得极其恐怖。我见过一个5层模板继承的项目,改一行代码编译要3分钟,排查错误全靠猜。
  • CRTP的代码膨胀问题:每个派生类都会实例化一份基类代码。如果你有100个派生类,基类的代码就会被复制100份。对于嵌入式或对体积敏感的场景,要权衡一下。

我的选择标准:

  • 需要运行时多态(比如通过基类指针/引用操作不同派生类)→ 用虚函数
  • 编译期就能确定类型,且对性能有极致要求 → 用CRTP
  • 只是复用一些代码,不涉及多态 → 用普通模板继承或组合

模板与继承的结合,是C++泛型编程里最强大的武器之一。CRTP尤其值得你花时间掌握——它出现在很多现代C++库中,比如std::enable_shared_from_this、Boost的很多组件,以及各种高性能计算框架。理解它,你就能写出更高效、更优雅的代码。

好了,这一章的内容就到这里。记住,模板继承不是银弹,但用对了地方,效果绝对惊艳。


公众号:蓝海资料掘金营,微信deep3321