商城首页欢迎来到中国正版软件门户

您的位置:首页 >C++实现简单的享元模式 _ 内部状态共享与工厂管理【源码】

C++实现简单的享元模式 _ 内部状态共享与工厂管理【源码】

  发布于2026-05-03 阅读(0)

扫一扫,手机访问

享元模式的核心:分离内部状态与外部状态

C++实现简单的享元模式 _ 内部状态共享与工厂管理【源码】

享元模式的核心精髓,其实可以归结为一句话:把能共享的抽出来,不能共享的传进来。 这听起来简单,但实践中却是个容易踩坑的地方。关键在于严格区分两种状态:内部状态(intrinsic state)必须是只读、不可变、与上下文无关的;而外部状态(extrinsic state)则需要在每次使用时,由客户端主动传入。一个常见的误区就是把positioncolor这类随场景变化的字段塞进享元类里——这直接导致共享失效,甚至可能引发棘手的并发写冲突。

正确的做法是什么呢?享元类应该只保留像glyph(字符)、font_family(字体族)、font_size(字号)这类真正可复用的属性;至于xy坐标、is_selected(是否被选中)等,必须由调用方在render()这类方法时作为参数传入。

享元对象必须分离内部状态和外部状态

让我们通过一个代码片段来具体感受一下:

class CharacterFlyweight {
private:
    char m_glyph;
    std::string m_font_family;
    int m_font_size; // 内部状态:构造时固定,不修改
public:
    CharacterFlyweight(char g, const std::string& f, int s)
        : m_glyph(g), m_font_family(f), m_font_size(s) {}
        
    void render(int x, int y, bool is_selected) const { // 外部状态走参数
        std::cout << "Draw '" << m_glyph << "' at (" << x << "," << y << ")"
                  << (is_selected ? " [selected]" : "") << "\n";
    }
};

看,render方法清晰地展示了外部状态是如何“流动”进来的,而享元对象自身则保持恒定不变。

享元工厂要用键值唯一标识享元实例

接下来是工厂。工厂的作用可不是简单地“缓存任意对象”,它的核心任务是:根据内部状态的组合生成一个唯一键,然后通过查表来实现复用。 这里的关键在于键的选择和生成逻辑。如果键类型选错或者拼接逻辑有歧义,就会导致该复用的没复用,或者不该复用的却复用了。举个例子,使用std::tuple作为键,通常就比直接拼接字符串要安全得多——这能有效避免像"11""1,1"这类潜在的歧义问题。

此外,工厂还必须考虑线程安全。当然,并非所有场景都需要复杂的同步机制。对于小型项目,使用static std::map配合std::call_once进行初始化通常就足够了;而在高并发场景下,则可能需要考虑std::shared_mutex甚至无锁哈希表。

这里有三个关键点需要牢记:

  • get_flyweight()方法只应接受内部状态字段作为参数,绝不能混入外部状态。
  • 键的生成逻辑必须是幂等的,且不能有副作用(比如,不要在构造键的过程中去创建新对象)。
  • 工厂应返回引用或智能指针,绝对禁止返回栈上对象的地址。

立即学习“C++免费学习笔记(深入)”;

客户端调用时必须显式传递外部状态

这是整个模式中最容易被忽略的一环。很多开发者精心设计了享元类和工厂,却在渲染循环里不小心把坐标之类的信息硬编码或设置到享元对象内部,这就让之前的努力前功尽弃了。请记住,享元对象的render()hit_test()layout()等方法,所有依赖上下文的参数都必须由外部传入。

来看一个典型的误用示例:

// ❌ 错误:把 x/y 存进享元,破坏共享性
flyweight->set_position(x, y); // 不该存在的方法
flyweight->render(); // 无法复用

而正确的方式应该是这样的:

// ✅ 正确:每次调用带上下文
auto& fw = factory.get_flyweight('A', "Consolas", 12);
fw.render(cursor_x, cursor_y, is_focused);
fw.render(line_x + 10, line_y + 20, false);

如果外部状态参数特别多(比如超过5个),可以考虑将它们封装成一个结构体传入,但要注意,这个结构体本身也不应该被享元对象持有或缓存。

C++ 中 shared_ptr 是享元工厂的合理返回类型

在C++实现中,使用std::shared_ptr作为工厂的返回类型,通常比使用裸指针或引用更为合理。这样做既能避免复杂的生命周期管理问题,又能很好地支持多线程环境下的安全共享。工厂内部可以使用std::map>来存储享元对象,新对象只构造一次,后续请求直接返回已存在的shared_ptr副本——开销极小,并且内存管理是自动的。

为什么不推荐返回引用呢?因为工厂内部容器(如map)的扩容或重组操作可能导致引用失效。同样,也不推荐直接返回值(即复制整个对象),因为这直接违背了享元模式减少对象数量的初衷。

这里有一个小陷阱需要注意:如果享元类涉及继承并有虚函数,务必记得将析构函数声明为virtual,否则通过shared_ptr来持有派生类对象时,派生类的资源可能无法被正确释放。

最后,还有一个更复杂的情况需要警惕:如果外部状态本身涉及资源(例如纹理句柄、GPU缓冲区ID等),这些资源仍然需要由客户端统一管理,享元对象绝对不应该持有它们。这一点在图形渲染或游戏开发等场景中尤其容易被忽略,结果往往是表面上共享了字符对象,背地里每个对象却还在绑定独立的纹理,共享的优势大打折扣。

本文转载于:https://www.php.cn/faq/2324254.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注