C++装饰器模式通过继承抽象基类、持有被装饰对象智能指针实现运行时功能叠加,需避免裸指针、切片、循环引用,优先用unique_ptr,多线程或跨DLL时用shared_ptr配合weak_ptr。

装饰器模式在 C++ 里不能像 Python 那样直接写 @decorator
C++ 没有运行时函数装饰语法,所谓“动态添加功能”实际是编译期组合 + 运行时对象包装。核心思路是让装饰器类持有被装饰对象的指针(或引用),并在其接口调用前后插入逻辑。
常见错误是试图用模板特化或宏模拟 @ 语法,结果导致类型擦除失败、生命周期失控,或者误以为 std::function 包一层就是装饰器——那只是回调封装,不是装饰器模式。
- 装饰器必须继承自同一抽象基类(比如
Component),否则无法透明替换 - 装饰器构造函数必须接收
Component*或std::unique_ptr<component></component>,不能裸传值(会切片) - 避免在装饰器中持有
shared_ptr除非明确需要共享所有权;循环引用极易发生
如何写一个可叠加的装饰器链:以日志 + 缓存为例
典型场景是给一个 DataSource 接口动态加日志和缓存行为。关键不是“加”,而是“谁负责 new,谁负责 delete”。推荐用工厂函数 + 智能指针管理链式构造。
示例中 LoggingDecorator 和 CachingDecorator 都继承自 DataSource,构造时接收上层装饰器或原始对象:
立即学习“C++免费学习笔记(深入)”;
std::unique_ptr<DataSource> ds = std::make_unique<RealDataSource>(); ds = std::make_unique<LoggingDecorator>(std::move(ds)); ds = std::make_unique<CachingDecorator>(std::move(ds)); // 调用 ds->fetch() 时,先走缓存,命中则跳过日志;不命中则进日志,再进 RealDataSource
- 顺序很重要:
CachingDecorator包裹LoggingDecorator,意味着缓存逻辑在外层,日志在内层 - 每个装饰器的
fetch()必须显式调用m_component->fetch(),不能漏掉 - 不要在装饰器里做 heavy 初始化(如开线程、连数据库),那是被装饰对象该干的事
std::shared_ptr 和 std::unique_ptr 在装饰器链里的取舍
用 std::unique_ptr 是默认选择:语义清晰、无引用计数开销、强制单所有权。但遇到需要多处访问同一装饰链(比如主线程创建,工作线程调用),就得换 std::shared_ptr。
坑在于:一旦某个装饰器内部也持有了链中上游的 shared_ptr(比如缓存 decorator 把原始 DataSource 存进了自己的 map),就可能形成循环引用——导致整个链 never destruct。
- 用
std::weak_ptr替代装饰器内部对上游的强引用,尤其在缓存、观察者等场景 -
unique_ptr链无法在运行时拆解某一层(比如临时关掉日志),shared_ptr配合动态 cast 才能支持 - 跨 DLL 边界传递装饰器对象时,
shared_ptr更安全(分配器一致性问题少)
为什么不用多重继承 + CRTP 模拟装饰?
有人尝试用 template <typename t> class Loggable : public T</typename> 等方式“混入”功能,这看起来更轻量,但破坏了装饰器的核心价值:运行时决定组合方式。
CRTP 是编译期静态增强,无法做到 if (enable_cache) ds = make_cached(ds); 这种条件包装。而且一旦涉及虚函数重写,多重继承容易引发钻石继承、vtable 冲突、override 漏写等问题。
- CRTP 适合通用工具能力(比如序列化、反射),不适合业务逻辑装饰
- 如果真要用,确保所有装饰 trait 都不定义同名虚函数,且基类析构函数为
virtual - 调试时堆栈会变深,gdb 显示的类型名冗长,不利于快速定位哪层出了问题
真正难的不是写几个装饰器类,而是控制好对象生命周期边界和调用链透明性——这两点一错,问题会藏得很深。


















