当前位置: 首页 > news >正文

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

目录

一、CRTP 是什么?

二、为什么叫“静态多态”?

三、CRTP 的典型应用

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

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

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

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

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

六、CRTP 的局限与替代方案

CRTP vs 虚函数 vs std::variant

七、常见错误

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

2. 忘记 const 正确性

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 惯用法如何隐藏实现细节?下篇讲工程实践。

http://www.zskr.cn/news/1351945.html

相关文章:

  • 国产多模态大模型 vs DALL-E:本土化突围与全球竞技
  • 别再只调样式了!深入理解鸿蒙ArkTS中Slider的四种交互状态(Begin/Moving/End/Click)
  • 手把手教你用C语言写一个简易的SMTP邮件内容解析器(基于libnids抓包库)
  • 【c++面向对象编程】第44篇:typename与class的区别,依赖类型名与template消除歧义
  • 告别开发依赖!SAP顾问必学的SQ01/SQ02/SQ03实战:5步搞定自定义报表
  • DocKit v1.0 发布 — AI 原生 NoSQL 桌面客户端,支持 Elasticsearch、OpenSearch 和 DynamoDB,本地优先,Apache 2.0 开源
  • 21.jdbc 学习笔记:从原理到实践的全流程梳理
  • 20.MySQL事务隔离级别示例详解(脏读、不可重复读、幻读)
  • 化妆品俄罗斯 Honest Sign诚实标签采集技术方案解析
  • Klogg实战:5分钟搞定海量日志中的Error排查(颜色标记+正则过滤技巧)
  • 炉石传说佣兵战记自动化脚本完整指南:5步轻松实现自动战斗
  • RK3588/3568嵌入式视觉开发:为什么我选择OpenCV 3.4.3 + FFmpeg 4.2.9这个“经典组合”?
  • 避开RK3566以太网PHY调试的那些‘坑’:从硬件C15到DTS配置的完整避坑指南
  • 众汇量化以多策略融合与智能投研打造高质量投资体系
  • 告别 GPU 独占时代:用 HAMi 实现训练推理一体化——博维智慧 GPU 虚拟化实战
  • 复合AI系统基准测试与优化实践指南
  • BE-ToF技术:突破传统飞行时间成像的深度感知新方案
  • Vue3 + TypeScript实战:封装一个带实时预览的企业级图片裁剪组件(附完整源码)
  • 在树莓派上玩转framebuffer:手把手教你用C语言点亮第一块屏幕(附完整代码)
  • 麒麟KYLINOS权限设置避坑指南:从图形界面到命令行的完整流程与常见错误排查
  • 为什么你的 Agent 总是跑着跑着就废了?聊聊 Loop 设计里那些坑(文末赠书)
  • 终极RPG Maker游戏资源解密工具:无需安装的浏览器解决方案
  • 告别Python版本冲突!用Anaconda的conda命令5分钟搞定Python 3.8专属虚拟环境
  • MCB900评估板电容选型与电源滤波设计解析
  • 别再复制粘贴了!手把手教你用LaTeX的algorithmicx宏包写出漂亮的算法伪代码
  • 如何用AI快速生成专业音乐封面:AICoverGen完整指南
  • League Akari:英雄联盟玩家的智能游戏管家,3大核心功能深度解析
  • 5个技巧让你的Windows任务栏焕然一新:TranslucentTB深度定制指南
  • 麒麟系统(桌面版)安装 NVIDIA 显卡驱动
  • 告别数据混乱!用腾讯TBDS的数据血缘与数据地图,5分钟理清你的数据资产