享元模式的核心是共享内部状态、分离内外部状态,即flyweight只存可共享的intrinsic state,外部状态由客户端调用时传入;关键判断标准是字段是否在多个上下文中完全一致。

享元模式的核心是共享内部状态,分离内外部状态
享元模式不是靠继承或模板“实现”出来的,而是通过手动拆分对象职责来达成的。C++里没有语言级支持,必须靠设计者明确区分:flyweight(享元类)只存可共享的intrinsic state,而把变化的extrinsic state移出对象,由客户端在调用时传入。
典型误操作是把所有字段都塞进享元类,结果导致每次使用都要新建实例——这反而比不用还慢。关键判断标准只有一条:这个字段是否在多个上下文中完全一致?如果是,就放进flyweight;否则,必须剥离。
用std::unordered_map做享元工厂最稳妥
享元必须复用,不能每次new一个。手动管理生命周期容易出错,推荐用静态std::unordered_map缓存已创建的实例。键通常由内部状态组合而成(比如std::tuple<int std::string></int>或自定义struct加operator==和std::hash特化)。
- 避免用
std::map——除非需要有序遍历,否则红黑树开销不必要 - 键类型别用裸指针或
std::shared_ptr作key,哈希和比较易出错 - 工厂方法返回
const flyweight&而非std::shared_ptr<flyweight></flyweight>,减少引用计数开销
示例键构造:auto key = std::make_tuple(size, color, font_family),然后查表:cache.try_emplace(key, size, color, font_family)。
立即学习“C++免费学习笔记(深入)”;
客户端必须显式传入外部状态,不能藏在享元里
常见错误是让flyweight::render()自己去查坐标、透明度、父容器等——这些全是外部状态。正确做法是接口形如:void render(int x, int y, float alpha) const,所有变化参数都由调用方决定。
这也意味着享元类必须是const-correct的:所有成员变量const或mutable(仅限缓存),所有方法标const。否则编译器会阻止你把它当共享资源安全使用。
- 如果渲染逻辑依赖某个上下文对象(如
Canvas*),把它作为参数传入,而不是存为成员 - 不要为了“方便”在享元里加
set_position()之类方法——那已经不是享元了,是普通对象 - 外部状态若较重(如大数组),考虑传
const&或std::span,避免拷贝
注意std::string和小字符串优化带来的隐式复制
std::string常被当作内部状态使用,但要注意:即使启用了SSO(Small String Optimization),std::string赋值或传参时仍可能触发写时复制或堆分配。若享元高频创建且含字符串,建议:
- 用
std::string_view替代std::string作构造参数,并在享元内用const char*+size_t存储(前提是字符串生命周期长于享元) - 若字符串来自配置文件或常量池,直接用字面量地址:
text_data("Arial")比std::string("Arial")更轻量 - 对多语言支持场景,别把
std::locale或std::codecvt塞进享元——它们不可共享,且线程不安全
真正难处理的从来不是模式结构,而是外部状态的边界划分是否干净。一个flyweight类改了三次访问权限、重写了四次operator==,往往说明最初就没想清楚哪些该共享、哪些该外置。


















