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

装饰模式的核心不是继承,而是组合与接口一致
直接用继承扩展功能在 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 里。











