当前位置: 首页 > 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.jsqmd.com/news/868270/

相关文章:

  • 别再乱买充电头了!一文看懂USB PD协议,教你选对笔记本和手机的‘能量搭档’
  • 从炼丹到炼蛋白:手把手拆解AlphaFold2的模型架构与训练技巧
  • 新高考答题卡模板全套PDF可打印(语文数学英语等)
  • 告别Ground Truth!用U2Fusion这个无监督网络,搞定多模态图像融合(附RoadScene数据集)
  • 2026年评价高的LED 薄膜开关/东莞定制薄膜开关厂家综合对比分析 - 行业平台推荐
  • Klogg实战:5分钟搞定海量日志中的Error排查(颜色标记+正则过滤技巧)
  • 告别编译限制!手把手教你用注册机破解Keil5 MDK(附2032年有效CID生成方法)
  • 手把手教你用C语言写一个简易的SMTP邮件内容解析器(基于libnids抓包库)
  • 别再只调样式了!深入理解鸿蒙ArkTS中Slider的四种交互状态(Begin/Moving/End/Click)
  • 2026年4月市面上评价好的建筑加固服务厂家推荐,建筑加固/建筑结构检测/建筑结构胶,建筑加固服务商推荐 - 品牌推荐师
  • 告别英文界面:RedHat Enterprise Linux 6.3 中文语言包配置与常见问题排查
  • ESP32 + SPH0645麦克风:用Python在电脑上实时播放音频的保姆级教程(附避坑指南)
  • 别再只会用PWM调速度了!STM32驱动直流有刷电机,H桥的三种模式(单极/双极/受限)到底怎么选?
  • 具身智能数据标注工具对比评测:6大平台横向测评
  • 保姆级教程:Proteus 8.6从下载到汉化,STM32仿真环境一步到位
  • 化妆品俄罗斯 Honest Sign诚实标签采集技术方案解析
  • 别再被‘一亿像素’忽悠了!聊聊手机CMOS尺寸、像素和Remosaic那些事儿
  • GD32F4系列驱动RGB888屏幕实战:TLI时序详解与IPA图层混合避坑指南
  • 三年级下册语文第四单元作文:中华传统节日
  • ops-math:昇腾 NPU 的数学算子库
  • 从CDDT模板到CDD数据库:手把手教你为车门ECU定制诊断描述文件
  • 2026年评价高的刀片/韩国LONGYI刀片长期合作厂家推荐 - 品牌宣传支持者
  • HA高可用架构:数字化转型的“隐性及格线”,你达标了吗?
  • 【信息系统项目管理师论文押题】论信息系统项目的度量绩效域
  • 炉石传说佣兵战记自动化脚本完整指南:5步轻松实现自动战斗
  • Applite完整指南:免费开源macOS软件管家,告别命令行复杂操作
  • pytorch-adapter:让 PyTorch 模型“无缝”跑在昇腾 NPU 上
  • 别再手动删了!用Notepad++正则表达式5分钟批量清理课程目录(附实战案例)
  • NotebookLM风格一致性密钥库(仅限首批200位AI架构师开放获取):含12个领域专属风格锚点模板与冲突检测CLI工具
  • 告别 GPU 独占时代:用 HAMi 实现训练推理一体化——博维智慧 GPU 虚拟化实战