c++如何实现装饰模式_c++动态给对象添加功能【实战】

3次阅读

装饰模式的核心是组合而非继承,所有装饰器与被装饰对象实现同一抽象接口(如IDrawable),通过持有接口指针转发调用并增强逻辑;必须使用std::unique_ptr管理所有权、保持const正确性、避免虚调用开销。

c++如何实现装饰模式_c++动态给对象添加功能【实战】

装饰模式的核心不是继承,而是组合与接口一致

直接用继承扩展功能在 c++ 里看似简单,但会导致类爆炸、无法运行时选择行为、违反开闭原则。装饰模式的关键在于:所有装饰器和被装饰对象都实现同一抽象接口(比如 IDrawable),而装饰器内部持有一个指向该接口的指针(或引用),把调用“转发”过去——再在前后加自己的逻辑。

常见错误是让装饰器继承具体类(如 redShape : public Circle),这会锁死类型,失去装饰任意 IDrawable 的能力。

  • 必须定义纯虚基类(如 IDrawable),所有实体类和装饰器都 public virtual 继承它
  • 装饰器构造函数接收 IDrawable*std::unique_ptr,避免裸指针生命周期失控
  • 不要在装饰器里重写所有函数——只重写需要增强的那几个,其余直接委托

用 unique_ptr 管理装饰链,避免内存泄漏和悬挂指针

手动 new / delete 构建装饰链(比如 new BorderDecorator(new ColorDecorator(new Circle())))极易出错:谁负责释放?异常发生时怎么办?C++11 后标准做法是用 std::unique_ptr 自动管理所有权。

示例:构建一个带边框+红色填充的圆形

立即学习C++免费学习笔记(深入)”;

auto shape = std::make_unique(); shape = std::make_unique(std::move(shape), "red"); shape = std::make_unique(std::move(shape), 2); shape->draw(); // 输出:Drawing red Circle with border width 2
  • std::move 是关键:每次包装都移交所有权,避免拷贝和重复释放
  • 装饰器的成员变量应为 std::unique_ptr,而非裸指针
  • 如果需要共享底层对象(比如多个装饰器引用同一原始对象),才考虑 std::shared_ptr,但要警惕循环引用

装饰器不能改变被装饰对象的 const 正确性

如果原始对象是 const IDrawable&,而你的装饰器 draw() 函数没加 const 修饰,编译就会失败——因为委托调用需要匹配 const 限定符。

典型错误写法:void draw() override { /* ... */ } → 无法接受 const 对象;正确写法:void draw() const override { component_->draw(); }

  • 所有接口函数声明必须带 const(如果它们不修改逻辑状态)
  • 装饰器内部的 component_ 成员也应是 std::unique_ptr 或通过 const 引用传递
  • 若装饰器自身需维护可变状态(如计数器),用 mutable 修饰该成员,保持接口 const

避免过度装饰带来的虚函数调用开销和调试困难

每层装饰器都是一次虚函数调用跳转。5 层嵌套意味着 draw() 调用要经过 5 次 vtable 查找——对高频调用路径(如渲染循环)可能成为瓶颈。更隐蔽的问题是:跟踪里全是 ColorDecorator::draw → BorderDecorator::draw → ...,掩盖了真正业务逻辑的位置。

  • 优先用编译期方案(如模板策略、CRTP)替代运行期装饰,当行为组合固定且数量有限时
  • [[likely]] / [[unlikely]] 标注分支预测,但无法消除虚调用本身
  • 加日志时别只打 “decorator called”,要输出 typeid(*component_).name() 和当前装饰类型,否则查链式调用像盲人摸象

最常被忽略的一点:装饰器的析构顺序和构造顺序相反,但如果你在某层装饰器里做了资源申请(比如 OpenGL texture 绑定),必须确保它在对应 draw() 之后才释放——而不是依赖析构时机。这要求把“后置清理”逻辑显式塞进 draw() 尾部,而不是放在 destructor 里。

text=ZqhQzanResources