C++ CRTP

C++ CRTP(奇异递归模板模式)


CRTP 是什么?

一句话总结:CRTP 就是让子类把自己作为模板参数传递给父类

听起来有点绕,直接上代码就明白了:

template <typename Derived>
class Base {
    // ...
};

class Derived : public Base<Derived> {
    // ...
};

Derived 继承自 Base<Derived>,也就是说,子类把自己“递归”地传给了父类。这就是“奇异递归”的由来。


CRTP 有啥用?

其实 CRTP 最常用的场景有三个:

  1. 静态多态:不用虚函数也能实现类似多态的效果,而且没有虚表,效率高。
  2. 代码复用:基类写通用逻辑,子类只需要实现自己的部分。
  3. 每个子类独立的静态成员:比如计数器,每个子类都有自己的静态变量。

CRTP 的原理

CRTP 的核心原理其实很简单,就是利用了 C++ 模板的“编译期展开”特性,让基类在编译时就能知道派生类的类型。

1. static_cast 的作用

在 CRTP 里,基类通常会用 static_cast<Derived*>(this) 把自己转换成派生类指针,然后调用派生类的方法。这样,虽然代码写在基类里,但实际调用的是派生类的实现。

比如:

void interface() {
    static_cast<Derived*>(this)->implementation();
}

这行代码在编译期就能确定 Derived 的类型,所以没有虚表,也没有运行时开销。

2. 编译期多态的本质

CRTP 实现的是“静态多态”,也就是多态的分发发生在编译期,而不是运行时。模板展开时,基类里的 static_cast<Derived*> 会被替换成具体的派生类类型,所有调用都在编译时就确定了。

3. 代码复用和静态接口约束

  • 代码复用:基类可以写通用的逻辑,比如日志、计数、接口包装等,具体实现交给派生类。这样不同的派生类可以复用同一套基类逻辑。
  • 静态接口约束:如果派生类没有实现基类里要调用的方法(比如 implementation()),编译时就会报错。这其实是一种“编译期接口检查”,比传统的虚函数更早发现问题。

一个简单的例子

假如我有一堆不同的动物,每种动物都能“说话”,但我又不想用虚函数(比如对性能有要求),CRTP 就能派上用场:

#include <iostream>

template <typename Derived>
class Animal {
public:
    void speak() {
        static_cast<Derived*>(this)->speak_impl();
    }
};

class Dog : public Animal<Dog> {
public:
    void speak_impl() {
        std::cout << "汪汪!" << std::endl;
    }
};

class Cat : public Animal<Cat> {
public:
    void speak_impl() {
        std::cout << "喵喵!" << std::endl;
    }
};

int main() {
    Dog d;
    Cat c;
    d.speak(); // 汪汪!
    c.speak(); // 喵喵!
    return 0;
}

这里的 Animal 基类里有个 speak(),但真正的实现是在子类里。通过 static_cast<Derived*>(this),基类可以“静态”地调用子类的方法。这样既有多态的效果,又没有虚函数的开销。


CRTP 实例

1. 每个子类独立计数

有时候我想统计每种类型各自创建了多少对象,CRTP 也能轻松搞定:

template <typename Derived>
class Counter {
public:
    static int count;
    Counter() { ++count; }
};
template <typename Derived>
int Counter<Derived>::count = 0;

class Apple : public Counter<Apple> {};
class Banana : public Counter<Banana> {};

int main() {
    Apple a1, a2;
    Banana b1;
    std::cout << Apple::count << std::endl;  // 输出2
    std::cout << Banana::count << std::endl; // 输出1
}

每个子类都有自己的静态成员变量,互不影响。

2 . 日志

#include <iostream>
#include <string>

// CRTP 日志基类
template <typename Derived>
class LoggerBase {
public:
    void runWithLog(const std::string& opName) {
        std::cout << "[LOG] 开始操作: " << opName << std::endl;
        static_cast<Derived*>(this)->run();  // 调用派生类的 run()
        std::cout << "[LOG] 结束操作: " << opName << std::endl;
    }
};

// 业务类A
class MyAlgorithm : public LoggerBase<MyAlgorithm> {
public:
    void run() {
        std::cout << "算法A正在运行..." << std::endl;
    }
};

// 业务类B
class MyService : public LoggerBase<MyService> {
public:
    void run() {
        std::cout << "服务B正在处理..." << std::endl;
    }
};

int main() {
    MyAlgorithm algo;
    MyService svc;
    algo.runWithLog("算法A任务");
    svc.runWithLog("服务B任务");
    return 0;
}

CRTP 和虚函数的区别

  • 虚函数:运行时多态,有虚表指针,灵活但有点性能损耗。
  • CRTP:编译期多态,没有虚表,效率高,但只能在编译期确定类型。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

石耳朵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值