C++实现简单的装饰器模式 _ 动态添加对象功能职责【源码】
C++装饰器模式:如何优雅地为对象“叠Buff”? C++装饰器模式通过继承同一抽象接口并组合被装饰对象实现行为叠加,不改变原接口签名;核心是装饰器持接口指针并在前后插入逻辑,需用智能指针管理嵌套生命周期,虚函数方案兼顾动态组合与维护性。 装饰器模式在 C++ 里为什么不能像 Python 那样写
C++装饰器模式:如何优雅地为对象“叠Buff”?
C++装饰器模式通过继承同一抽象接口并组合被装饰对象实现行为叠加,不改变原接口签名;核心是装饰器持接口指针并在前后插入逻辑,需用智能指针管理嵌套生命周期,虚函数方案兼顾动态组合与维护性。

装饰器模式在 C++ 里为什么不能像 Python 那样写 @log
很多从 Python 转过来的朋友,可能都怀念那个简洁的 @log 语法糖。但到了 C++ 这儿,情况就不同了。C++ 语言本身并没有提供运行时注解或者语法级别的装饰器支持。所以,我们常说的“C++ 装饰器模式”,其实是经典 GoF 结构型模式的一种手动实现——它依靠“继承”加“组合”这两大法宝,在编译期就构建好职责链。这种模式的核心优势在于,它绝不改变原有对象的类型签名,却能以一种近乎透明的方式,为对象叠加新的行为,比如添加日志记录、性能计时或者权限检查。关键在于,它追求的不是“看起来像装饰”,而是实打实的“调用接口保持不变,行为却能灵活叠加”。
如何用继承和组合写出可叠加的装饰器
想要实现这种可叠加的装饰效果,关键在于理清几个角色之间的关系。整个架构的核心,是让装饰器类和被装饰的类,都去实现同一个抽象接口。装饰器内部则持有一个指向该接口的指针(或者引用)。这样一来,所有对接口的调用,最终都会转发给那个被包裹的对象,而装饰器要做的,就是在转发前后,插入自己的那部分逻辑。
Component:这是整个体系的基石,一个纯虚基类,定义了统一的接口(比如一个operation()方法)。ConcreteComponent:这是原始功能的实现者,也就是我们需要被“装饰”的那个核心对象。Decorator:作为抽象装饰器,它同样继承自Component。它的特殊之处在于,内部持有一个Component*类型的成员,用来链接被装饰对象。- 具体装饰器(比如
LoggingDecorator、TimingDecorator):它们继承自Decorator,并重写接口方法。在方法内部,先执行自己的逻辑(如打印开始日志),然后调用component_->operation(),最后再执行后续逻辑(如打印结束日志)。
来看一个简化的代码片段,感受一下这个结构:
class Component {
public:
virtual ~Component() = default;
virtual void operation() = 0;
};
class ConcreteComponent : public Component {
public:
void operation() override { std::cout << "do work\n"; }
};
class Decorator : public Component {
protected:
Component* component_;
public:
explicit Decorator(Component* c) : component_(c) {}
void operation() override { component_->operation(); }
};
class LoggingDecorator : public Decorator {
public:
explicit LoggingDecorator(Component* c) : Decorator(c) {}
void operation() override {
std::cout << "[LOG] start\n";
component_->operation();
std::cout << "[LOG] end\n";
}
};
多个装饰器嵌套时的构造顺序和内存管理风险
当需要给一个对象叠加多层“Buff”时,装饰器就会形成一条链。这条链的构造顺序,必须严格遵循“从里到外”的原则:先创建最核心的 ConcreteComponent,然后用它来构造 LoggingDecorator,再用这个包含了日志功能的中间对象,去构造外层的 TimingDecorator。顺序一旦搞反,很容易导致悬空指针或者对象被提前释放的灾难性后果。
立即学习“C++免费学习笔记(深入)”;
- 强烈推荐使用智能指针:用
std::unique_ptr来管理对象生命周期,能有效避免裸指针带来的管理混乱。 - 警惕循环引用:不要让装饰器持有
std::shared_ptr并形成循环引用(比如,装饰器又把自身传给了被装饰对象)。 - 状态管理要独立:如果装饰器需要保存内部状态(比如调用计数器),务必确保每个装饰器实例的状态是独立的,千万别误用 static 变量,导致所有实例共享同一份数据。
- 析构职责要清晰:注意,
Decorator的析构函数通常不应该去 delete 它所持有的component_。这个生命周期的管理职责,应该交给最外层的装饰器,或者由对象的创建者来统一负责。当然,最省心的办法还是全程交给智能指针。
来看一个相对安全的写法示例:
auto comp = std::make_unique(); auto logged = std::make_unique (comp.get()); auto timed = std::make_unique (logged.get()); // 注意:此时 comp 和 logged 必须比 timed 活得久
不过,更稳妥、所有权更清晰的写法,是全部使用 std::unique_ptr 并通过移动语义来构造:
auto comp = std::make_unique(); auto logged = std::make_unique (std::move(comp)); auto timed = std::make_unique (std::move(logged));
为什么不用模板或 CRTP 实现更“零开销”的装饰器
看到虚函数调用,有些追求极致性能的开发者可能会想:能不能用模板或者 CRTP(奇异递归模板模式)来实现,彻底消除运行时开销?
想法很好,但代价不小。模板版本的装饰器(比如 template)确实能避免虚函数调用的开销,但它也彻底破坏了“统一接口”和“运行时动态叠加”这两个核心能力。你无法将 LoggingDecorator 和 TimingDecorator 当作同一种类型存进容器,也无法在程序运行的时候,再决定到底要给一个对象叠加几层装饰。
- 虚函数方案的取舍:它牺牲了一点微小的性能,换来的却是真正的“动态组合自由”。在大多数应用场景下,这点开销是完全可以接受的。
- CRTP 的局限性:CRTP 可以实现静态多态,但一旦需要叠加两层以上的装饰,类型就会爆炸(变成
Logging这种嵌套形式),而且装饰逻辑会与被装饰的具体类型紧密耦合,难以解耦和复用。> - 如何选择:只有当装饰逻辑完全固定、绝无运行时配置需求,并且对性能敏感到了锱铢必较的程度时,才值得考虑模板方案。否则,基于虚函数和堆分配的经典实现,才是更通用、更易于维护的选择。
最后,必须提醒一点:装饰器模式并非万能胶。它非常擅长增强“已有接口的行为”,但如果想给对象添加一个全新的接口(比如,突然想给 Component 加一个 sa ve_to_file() 方法),那装饰器就无能为力了,这时就得修改基类,或者考虑其他设计模式。另外,如果装饰层叠得太深,调试也会变得困难——错误调用栈里会塞满一连串的 Decorator::operation,想一眼看出问题到底出在哪一层,可不是件容易的事。

































