【c++面向对象编程】第46篇:CRTP(奇异递归模板模式):静态多态的妙用

目录

[一、CRTP 是什么?](#一、CRTP 是什么?)

二、为什么叫"静态多态"?

[三、CRTP 的典型应用](#三、CRTP 的典型应用)

[1. 静态多态:避免虚函数开销](#1. 静态多态:避免虚函数开销)

[2. 对象计数(自动统计实例数量)](#2. 对象计数(自动统计实例数量))

[3. 混入类(Mixin)------ 给现有类添加功能](#3. 混入类(Mixin)—— 给现有类添加功能)

[四、CRTP 与 C++23 的推导 this(deducing this)](#四、CRTP 与 C++23 的推导 this(deducing this))

五、完整例子:多态容器(避免虚函数)

[六、CRTP 的局限与替代方案](#六、CRTP 的局限与替代方案)

[CRTP vs 虚函数 vs std::variant](#CRTP vs 虚函数 vs std::variant)

七、常见错误

[1. 类型转换错误(用 dynamic_cast 而不是 static_cast)](#1. 类型转换错误(用 dynamic_cast 而不是 static_cast))

[2. 忘记 const 正确性](#2. 忘记 const 正确性)

[3. 将 CRTP 用于需要运行时多态的场景](#3. 将 CRTP 用于需要运行时多态的场景)

八、这一篇的收获


一、CRTP 是什么?

cpp

复制代码
// 基类模板:接受派生类类型作为参数
template <typename Derived>
class Base {
public:
    void interface() {
        // 通过 static_cast 调用派生类的实现
        static_cast<Derived*>(this)->implementation();
    }
};

// 派生类:把自己传给基类
class Derived : public Base<Derived> {
public:
    void implementation() {
        cout << "Derived 实现" << endl;
    }
};

// 使用
Derived d;
d.interface();  // 输出 "Derived 实现"

"奇异递归":派生类继承自一个以自己为模板参数的基类------形成一个递归的、不寻常的模式。

cpp

复制代码
class Derived : public Base<Derived>  // Derived 出现在自己的基类列表中

二、为什么叫"静态多态"?

普通虚函数是动态多态(运行时决定):

cpp

复制代码
class Shape {
public:
    virtual void draw() = 0;
};
class Circle : public Shape {
    void draw() override { cout << "Circle" << endl; }
};
Shape* s = new Circle();
s->draw();  // 运行时查 vtable 调用 Circle::draw

CRTP 是静态多态(编译期决定):

cpp

复制代码
template <typename Derived>
class Shape {
public:
    void draw() {
        static_cast<Derived*>(this)->drawImpl();
    }
};

class Circle : public Shape<Circle> {
public:
    void drawImpl() { cout << "Circle" << endl; }
};

Circle c;
c.draw();  // 编译期确定调用 Circle::drawImpl
特性 动态多态(虚函数) 静态多态(CRTP)
绑定时间 运行时 编译期
调用开销 虚表查表(2-3 次内存访问) 普通函数调用(可内联)
代码体积 小(共享虚表) 大(每个派生类生成一份基类代码)
灵活性 高(运行时替换) 低(编译期固定)
适用场景 需要运行时多态 性能敏感、不需要运行时替换

三、CRTP 的典型应用

1. 静态多态:避免虚函数开销

cpp

复制代码
#include <iostream>
#include <chrono>
using namespace std;

// 动态多态版本
class DynamicShape {
public:
    virtual double area() const = 0;
    virtual ~DynamicShape() = default;
};

class DynamicCircle : public DynamicShape {
    double r;
public:
    DynamicCircle(double rad) : r(rad) {}
    double area() const override { return 3.14159 * r * r; }
};

// CRTP 静态多态版本
template <typename Derived>
class StaticShape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }
};

class StaticCircle : public StaticShape<StaticCircle> {
    double r;
public:
    StaticCircle(double rad) : r(rad) {}
    double areaImpl() const { return 3.14159 * r * r; }
};

// 使用:CRTP 版本不需要指针,可直接调用
StaticCircle c(5.0);
cout << c.area() << endl;  // 编译期绑定,可内联

2. 对象计数(自动统计实例数量)

cpp

复制代码
template <typename T>
class Counter {
private:
    static int count;
public:
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }
    static int getCount() { return count; }
};

template <typename T>
int Counter<T>::count = 0;

// 需要计数的类只需继承 Counter
class Dog : public Counter<Dog> {
    string name;
public:
    Dog(const string& n) : name(n) {}
};

class Cat : public Counter<Cat> {
    string name;
public:
    Cat(const string& n) : name(n) {}
};

int main() {
    Dog d1("旺财"), d2("小黑");
    Cat c1("咪咪");
    
    cout << "Dog 数量: " << Dog::getCount() << endl;   // 2
    cout << "Cat 数量: " << Cat::getCount() << endl;   // 1
    
    return 0;
}

3. 混入类(Mixin)------ 给现有类添加功能

cpp

复制代码
// 为类添加克隆能力
template <typename Derived>
class Cloneable {
public:
    Derived clone() const {
        return static_cast<const Derived&>(*this);
    }
};

// 为类添加可比较能力
template <typename Derived>
class Comparable {
public:
    bool operator==(const Derived& other) const {
        const Derived& self = static_cast<const Derived&>(*this);
        return self.equal(other);
    }
    
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

// 组合多个 Mixin
class Point : public Cloneable<Point>, public Comparable<Point> {
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) {}
    
    bool equal(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

int main() {
    Point p1(1, 2), p2(1, 2), p3(3, 4);
    
    Point p4 = p1.clone();  // 来自 Cloneable
    cout << (p1 == p2) << endl;  // 1,来自 Comparable
    cout << (p1 == p3) << endl;  // 0
    
    return 0;
}

四、CRTP 与 C++23 的推导 this(deducing this)

C++23 引入的"推导 this"可以简化 CRTP 的写法,不再需要显式传递派生类参数:

cpp

复制代码
// C++23 之前的 CRTP
template <typename Derived>
class OldBase {
    void f() {
        static_cast<Derived*>(this)->impl();
    }
};

// C++23:用显式对象参数(deducing this)
class NewBase {
public:
    template <typename Self>
    void f(this Self&& self) {
        self.impl();
    }
};

class Derived : public NewBase {
    void impl() { cout << "impl" << endl; }
};

但目前大部分代码仍使用传统 CRTP。


五、完整例子:多态容器(避免虚函数)

cpp

复制代码
#include <iostream>
#include <memory>
#include <vector>
using namespace std;

// ========== 动态多态版本 ==========
class IDynamicDrawable {
public:
    virtual void draw() const = 0;
    virtual ~IDynamicDrawable() = default;
};

class DynamicCircle : public IDynamicDrawable {
    double r;
public:
    DynamicCircle(double rad) : r(rad) {}
    void draw() const override {
        cout << "画圆,半径=" << r << endl;
    }
};

class DynamicSquare : public IDynamicDrawable {
    double side;
public:
    DynamicSquare(double s) : side(s) {}
    void draw() const override {
        cout << "画正方形,边长=" << side << endl;
    }
};

// ========== CRTP 静态多态版本 ==========
template <typename Derived>
class StaticDrawable {
public:
    void draw() const {
        static_cast<const Derived*>(this)->drawImpl();
    }
};

class StaticCircle : public StaticDrawable<StaticCircle> {
    double r;
public:
    StaticCircle(double rad) : r(rad) {}
    void drawImpl() const {
        cout << "[CRTP] 画圆,半径=" << r << endl;
    }
};

class StaticSquare : public StaticDrawable<StaticSquare> {
    double side;
public:
    StaticSquare(double s) : side(s) {}
    void drawImpl() const {
        cout << "[CRTP] 画正方形,边长=" << side << endl;
    }
};

// CRTP 版本的容器需要知道具体类型(不能用基类指针统一存储)
// 解决方案:类型擦除或改用 std::variant
using StaticShape = variant<StaticCircle, StaticSquare>;

void drawAllStatic(const vector<StaticShape>& shapes) {
    for (const auto& shape : shapes) {
        visit([](const auto& s) { s.draw(); }, shape);
    }
}

int main() {
    cout << "=== 动态多态 ===" << endl;
    vector<unique_ptr<IDynamicDrawable>> dynamicShapes;
    dynamicShapes.push_back(make_unique<DynamicCircle>(5.0));
    dynamicShapes.push_back(make_unique<DynamicSquare>(4.0));
    for (const auto& s : dynamicShapes) {
        s->draw();
    }
    
    cout << "\n=== CRTP 静态多态(使用 variant) ===" << endl;
    vector<StaticShape> staticShapes;
    staticShapes.push_back(StaticCircle(5.0));
    staticShapes.push_back(StaticSquare(4.0));
    drawAllStatic(staticShapes);
    
    return 0;
}

输出:

text

复制代码
=== 动态多态 ===
画圆,半径=5
画正方形,边长=4

=== CRTP 静态多态(使用 variant) ===
[CRTP] 画圆,半径=5
[CRTP] 画正方形,边长=4

六、CRTP 的局限与替代方案

局限 说明 替代方案
无法存储异质容器 不同派生类类型不同 std::variant + 访问者模式
代码膨胀 每个派生类生成一份基类代码 虚函数更节省空间
类型关系隐藏 无公共基类指针 文档说明或概念约束
编译时间 增加模板实例化 按需使用

CRTP vs 虚函数 vs std::variant

场景 推荐方案
需要运行时多态(同一容器存不同类型) 虚函数
性能关键且类型数量固定 CRTP + std::variant
类型数量固定且需要多种操作 std::variant + 访问者
需要添加通用功能给多个类 CRTP Mixin

七、常见错误

1. 类型转换错误(用 dynamic_cast 而不是 static_cast)

cpp

复制代码
// ❌ CRTP 中不应使用 dynamic_cast(基类不知道派生类,但 static_cast 足够)
static_cast<Derived*>(this);  // ✅
dynamic_cast<Derived*>(this); // ❌ 多余且可能失败

2. 忘记 const 正确性

cpp

复制代码
template <typename D>
class Base {
    void f() const {
        // 如果 D::impl 不是 const,这里需要 const_cast 或调整
        static_cast<const D*>(this)->impl();
    }
};

3. 将 CRTP 用于需要运行时多态的场景

如果需要在运行时替换对象,CRTP 不适用------使用传统虚函数。


八、这一篇的收获

你现在应该理解:

  • CRTP 定义class Derived : public Base<Derived>,派生类把自己传给基类模板

  • 静态多态:编译期绑定,无虚函数开销,可内联

  • 典型应用

    • 静态多态(性能敏感场景)

    • 对象计数(自动统计实例)

    • Mixin 混入类(为类添加通用功能)

  • 与虚函数对比:CRTP 更快但缺乏运行时灵活性

  • 容器存储 :CRTP 对象类型不同,需要用 std::variant 或类型擦除

💡 小作业:实现一个 enable_if_streamable 的 CRTP 基类,为派生类自动添加 operator<< 支持。要求:基类提供 print 纯虚函数(静态多态),派生的 operator<< 调用 print。测试 PointLine 类。


下一篇预告 :第47篇《C++代码组织:头文件、预编译指令与不透明指针(Pimpl)》------头文件应该放什么?#pragma once 是什么原理?如何减少编译依赖?Pimpl 惯用法如何隐藏实现细节?下篇讲工程实践。

相关推荐
广州灵眸科技有限公司1 小时前
瑞芯微(EASY EAI)RV1126B 音频电路
开发语言·人工智能·深度学习·算法·yolo·音视频
科芯创展1 小时前
XZ4058B/C,20V,外置MOS,8.4V/8.7V开关充电芯片
c语言·开发语言
Ws_1 小时前
C# 学习 Day1
开发语言·学习·c#
小的~~1 小时前
算法题:只出现一次的数字
数据结构·算法
灵智实验室1 小时前
PX4状态估计技术EKF2详解(六):EKF2 磁力计融合——从航向修正到 3D 姿态约束
算法·无人机·px 4
hhcgchpspk1 小时前
easyx按键游戏
c++·stm32·单片机·游戏·easyx
JieE2121 小时前
手把手带你用虚拟头节点实现单链表,搞定所有边界问题
javascript·算法
郝学胜-神的一滴2 小时前
Qt 高级开发 011: 跨线程信号槽实战
开发语言·c++·qt·程序人生·开源软件·用户界面
轻刀快马2 小时前
讲透分布式系统的演进史与核心架构
开发语言·架构·php