享元模式的核心是分离内在状态与外在状态:内在状态(如字符样式、图元类型)不可变且共享,外在状态(如坐标、时间戳)由外部传入;需用const-correct享元类、std::shared_ptr工厂管理生命周期,并谨慎处理线程安全与资源析构。

享元模式的核心不是“写个类”,而是识别可共享的内在状态
享元模式在 C++ 里不靠框架,靠你主动把对象拆成两部分:一部分能被多个地方共用(intrinsic state),另一部分必须随上下文变化(extrinsic state)。如果没想清楚哪些字段属于哪一类,后面所有缓存、工厂、引用计数都白搭。
典型误用是把整个对象塞进 std::unordered_map 当享元,结果发现每次传参还得拷贝一堆临时数据——这反而更耗内存。
- 只把不变的、可复用的数据(比如字符样式、图元类型、协议头模板)放进享元对象
- 把坐标、ID、时间戳、用户上下文这些动态值,留在外部调用方手里,通过函数参数传入
- 享元类本身应为
const-correct,构造后不可变;否则共享就失去意义
用 std::shared_ptr + 工厂管理享元生命周期最稳妥
手写引用计数容易漏减、多线程下难保证原子性;裸指针又不敢释放。C++ 标准库的 std::shared_ptr 天然适配享元场景:谁用谁持有一份引用,最后一个用完自动析构。
工厂函数负责查表、创建、返回 std::shared_ptr<flyweight></flyweight>,避免重复构造。注意别直接返回 shared_ptr 的引用或指针——那会干扰引用计数逻辑。
立即学习“C++免费学习笔记(深入)”;
- 工厂内部用
static std::unordered_map<key std::shared_ptr>></key>缓存已创建实例 - 键(
Key)必须能唯一标识内在状态,推荐用std::tuple或自定义结构体 +operator==和std::hash - 不要在工厂里做 heavy 初始化(如加载纹理、解析 XML),那会让首次获取变慢且影响缓存命中率
class CharacterFlyweight {
public:
CharacterFlyweight(char c, int size, const std::string& font)
: m_char(c), m_size(size), m_font(font) {}
void render(int x, int y) const { /* 使用 m_char/m_size/m_font + 外部 x/y */ }
private:
const char m_char;
const int m_size;
const std::string m_font;
};
<p>using FlyweightPtr = std::shared_ptr<CharacterFlyweight>;</p><p>FlyweightPtr getCharacterFlyweight(char c, int size, const std::string& font) {
static std::unordered_map<std::tuple<char, int, std::string>, FlyweightPtr> cache;
auto key = std::make_tuple(c, size, font);
auto it = cache.find(key);
if (it != cache.end()) return it->second;
auto ptr = std::make_shared<CharacterFlyweight>(c, size, font);
cache[key] = ptr;
return ptr;
}</p>多线程下缓存查表必须加锁,但别锁整个工厂函数
std::unordered_map 非线程安全,多个线程同时调用工厂查表+插入,可能触发重哈希导致迭代器失效甚至崩溃。但给整个 getCharacterFlyweight 加 std::mutex 会严重串行化,抵消享元带来的性能收益。
- 只对 map 的读-改-写关键段加锁,比如用
std::shared_mutex区分读写路径 - 更推荐用
std::call_once+std::once_flag做单次初始化,或用absl::flat_hash_map(若引入第三方)提升并发查找效率 - 如果享元极少新增、几乎只读,可考虑初始化期构建好全部实例,运行时只做无锁查找
别忘了析构代价:共享对象里含大资源时要格外小心
享元被多个地方持有,意味着它的析构时机不可控。如果它内部持有 std::vector<uint8_t></uint8_t> 缓冲区、OpenGL 纹理 ID 或文件句柄,销毁瞬间的开销可能集中爆发,引发卡顿或资源泄漏。
- 大资源建议延迟释放:用
std::weak_ptr弱引用关联到资源管理器,由独立线程定期回收 - 避免在享元析构中做同步 I/O 或网络调用
- 调试时留意
shared_ptr的引用计数是否符合预期——用use_count()打印,别靠猜
真正难的不是写出享元结构,而是判断某个对象到底“值不值得”共享:它被复用的频次够不够高?构造/析构成本是不是真比查表高?这些得看 profile 数据,不是看设计模式名字。










