C++享元模式及其实现(非常详细)
享元模式的主要目标是通过共享尽可能多的细粒度对象来最大限度地减少内存使用和提高性能,或方便在不同的模块之间分享数据。
享元模式的核心思想是将可共享的状态(称为内部状态)与不可共享的状态(称为外部状态)分离。内部状态是存储在享元对象内部的,不会随着外部状态的变化而变化,可以被多个对象共享,而外部状态是由客户端传递给享元对象的并会随着客户端的变化而变化。
享元模式的优势有以下几点:
但是由于共享对象会使系统对对象的状态变得更加透明,所以可能会导致系统的复杂性增加。对象的共享和重用可能会引入线程安全问题,需要额外的线程进行同步处理。

图 1 传统享元模式UML简图
第 1 种实现结构如下图所示:

图 2 C++11模板享元模式UML简图
类 dataItfc__ 是一个空的类,主要的功能是对数据继承进行约束性检查。模板类 dataItfc 用于继承 dataItfc__,并定义纯虚函数处理的数据的接口方法。实际的数据类型如 concreteDataA 和 concreteDataB从dataItfc 继承并实现数据接口方法。通过以上几点设计实现了 flyweight 类的通用的数据访问控制。
模板类flyweight依赖于dataItfc,实际的数据类通过组合关系实现了享元模式的骨架构造。flyweight通过数据ID检索数据,模板参数idType是数据ID的数据类型;模板参数Args是用来构造数据类的参数类型表,Args采用可变模板参数提供了灵活的初始化、指定数据的功能。
concreteFlyweight 是实际的享元类,通过特化或者继承模板类 flyweight 实现。Client 是实际使用 concreteFlyweight 的模块。
第 2 种实现方式如下图所示:

图 3 C++11模板万能数据类型享元模式UML简图
使用万能数据类型简化了模块的结构。在应用时也不需要对接口进行声明和实现,简化了开发过程,但同时因为 variant 类隐藏了具体类型,在运行时会增加少量的负担。
万能数据类型使用起来更加方便,由于将万能数据类型转换成具体数据类型有一定的消耗,所以相对来讲效率会比使用接口的方式低。两种模式通过宏控制,在预处理阶段明确。
在默认情况下模块使用万能数据类型实现,代码如下:
在使用万能数据类型的情况下,先包含 variant 模块,这个模块提供了任意变量类型的支持,代码如下:
在使用接口方式时先定义了一个空的类 dataItfc__,其主要目的是用来在使用的过程里检查接口来源是否正确,实际暴露出来供给业务代码使用的接口 dataItfc 则是继承于 dataItfc__,代码如下:
这个模板类用来对外暴露出访问数据的接口,约定具体保存的数据类型。使用纯虚函数约定访问接口,在实际使用时需要实现这两个接口。通过模板类和虚函数进行配合,可以大幅地增加通用性。最后形成在一个享元模块中可以存储不同类型的数据内容,代码如下:
使用 std::map 保存数据记录,key 部分是由模板参数 dataType 来确定的,具体值使用了两种方式进行存储,使用宏 FLYWEIGHT_USE_VARIANT 来控制存储方式。当这个宏的值是 1 时使用 variant 存储,否则使用接口对象指针 private__::dataItfc__* 的方式进行存储。使用 std::map 对数据进行整体管理,提供一个享元模式,以便可以存储多个不同的数据,代码如下:
引出迭代类型,方便在实际使用时对数据进行迭代访问。同样因为两种数据的存储方式不同,所以需要根据宏的值控制引出不同的迭代器,代码如下:
函数 has() 用来检查享元中是否存在给定 ID 的数据内容。对数据表进行查找前先对数据进行加锁,然后使用 map 容器的 find() 方法进行查找,最后判断结果是否是结束位置。has() 函数的返回值 true 表示存在数据,否则表示数据不存在,代码如下:
模板函数 set() 用来增加或者修改数据。模板参数T是接口类型。对应的实际数据类型会对所有的 PARAMS 对应的数据进行组织和保存。程序首先检查数据类型,然后查找数据。根据查找结果分成两个分支,如果数据存在,则修改数据,否则添加数据内容,代码如下:
get() 函数用于获取数据。使用 variant 方式时返回的 variant 对象在需要获取具体对象时,可以使用 int a=XXX.get<int>() 的方式获取,代码如下:
在使用 variant 的方式下重载 [] 符号,方便使用数组的方式读取数据内容,代码如下:
在接口方式下,get() 函数返回的是接口类型指针。函数要求显式地提供要求返回的接口数据类型。在编译过程中首先对数据类型进行检查,如果数据类型不符合要求,则停止编译,代码如下:
然后查找数据是否存在,如果存在,则对数据进行指针类型强制转换,然后返回对象指针,否则返回 nullptr。需要注意,使用接口方式实现的是没有 [] 操作符重载的实现,代码如下:
此模块还提供了多个方便管理数据的接口,例如删除数据、获取记录数量、获取迭代器等,代码如下:
第 1 种以 wheels::variant 万能数据类型实现,示例程序如下:
定义飞机类,这个演示使用的是 wheels::variant 模式,在默认情况下 flyweight 模块使用的是 variant 模式,代码如下:
飞机类自己的接口定义,当使用variant模式时可以不用考虑set和get函数的约束,代码如下:
创建一个 flyweight 对象来管理飞机对象。这里直接对 flyweight 模块进行特化,代码如下:
创建飞机工厂对象,使用 flyweight 类管理飞机对象实例化,也就是对 flyweight 模块进行实例化。接下来将飞机对象添加到工厂,使用模板参数明确数据类型。函数参数中第 1 个是数据索引,对应于 flyweight<std::string> 中的 std::string;第 2 个参数是容量,对应于 Airplan::capacity_,代码如下:
第 2 种针对接口方式实现,在包含 flyweight 模块前,必须定义宏 FLYWEIGHT_USE_VARIANT,并定义为 0,关闭 variant 方式,然后包含 flyweight.hpp,这是因为,在 flyweight 模块中默认使用了 variant 方式,通过将宏值定义为 FLYWEIGHT_USE_VARIANT 模块后编译时就会以接口的方式编译。
定义飞机类,这里就需要继承 flyweight 的数据接口。这里需要注意,应针对数据类型配置模板参数。继承接口模板类 dataItfc,模板参数是实际对象,代码如下:
创建一个 flyweight 对象来管理飞机对象,创建飞机工厂对象,使用 flyweight 类管理飞机对象,将飞机对象添加到工厂,代码如下:
获取飞机对象并打印其名称。这里获取的是 Airplane 的指针类型,这里和 variant 方式是不一样的,代码如下:
boeing747->get() 是调用虚函数的实现。使用接口模式,虽然可以在一定程度上提高数据的效率,但是由于预定义了接口,所以所有的新的数据类型都必须从这个接口继承。另外可以使用模板参数实现在一个享元模块内存储不同的数据类型,代码如下:
在这个示例程序中定义了一个 Airplane 类来表示飞机对象,然后使用 flyweight 类来管理飞机对象,创建飞机工厂并增加两个飞机对象(Boeing747 和 AirbusA380),然后通过工厂获取飞机对象,并打印出飞机容量,运行程序后结果如下:
享元模式的核心思想是将可共享的状态(称为内部状态)与不可共享的状态(称为外部状态)分离。内部状态是存储在享元对象内部的,不会随着外部状态的变化而变化,可以被多个对象共享,而外部状态是由客户端传递给享元对象的并会随着客户端的变化而变化。
享元模式的优势有以下几点:
- 可以大幅度地减少内存的使用,从而提高系统性能;
- 由于享元对象可以被共享,所以不需要每次都创建新的对象,可以降低创建对象的开销;
- 可以在一定程度上实现对象的复用,共享相同的对象可以在多个地方使用,提高系统的复用性;
- 通过将内部状态与外部状态分离,使系统更加灵活,可以在不同的环境下共享同一个对象,减少对象的数量。
但是由于共享对象会使系统对对象的状态变得更加透明,所以可能会导致系统的复杂性增加。对象的共享和重用可能会引入线程安全问题,需要额外的线程进行同步处理。
传统享元模式
传统享元模式的结构包括几个核心组件:享元接口(Flyweight)、具体享元(ConcreteFlyweight)和客户端(Client),如下图所示:
图 1 传统享元模式UML简图
- 享元接口定义了享元对象的共享方法,通过该接口可以设置外部状态;
- 具体享元实现了享元接口的具体享元对象。具体享元类中包含了内部状态,并在需要时进行共享;
- 享元工厂负责创建和管理享元对象。它会维护一个享元池,用于存储和管理创建的享元对象;
- 客户端通过享元工厂获取享元对象,并在需要时向享元对象传递外部状态。
C++11元编程下的结构设计
本文中享元模式采用了两种不同的实现方式:- 第 1 种使用了接口方式实现,实际数据需要实现接口以实现访问;
- 第 2 种使用了万能数据类型。
第 1 种实现结构如下图所示:

图 2 C++11模板享元模式UML简图
类 dataItfc__ 是一个空的类,主要的功能是对数据继承进行约束性检查。模板类 dataItfc 用于继承 dataItfc__,并定义纯虚函数处理的数据的接口方法。实际的数据类型如 concreteDataA 和 concreteDataB从dataItfc 继承并实现数据接口方法。通过以上几点设计实现了 flyweight 类的通用的数据访问控制。
模板类flyweight依赖于dataItfc,实际的数据类通过组合关系实现了享元模式的骨架构造。flyweight通过数据ID检索数据,模板参数idType是数据ID的数据类型;模板参数Args是用来构造数据类的参数类型表,Args采用可变模板参数提供了灵活的初始化、指定数据的功能。
concreteFlyweight 是实际的享元类,通过特化或者继承模板类 flyweight 实现。Client 是实际使用 concreteFlyweight 的模块。
第 2 种实现方式如下图所示:

图 3 C++11模板万能数据类型享元模式UML简图
使用万能数据类型简化了模块的结构。在应用时也不需要对接口进行声明和实现,简化了开发过程,但同时因为 variant 类隐藏了具体类型,在运行时会增加少量的负担。
C++享元模式的实现和解析
享元模式采用两种存储模式实现,一种是使用万能数据类型;另一种是使用接口。万能数据类型使用起来更加方便,由于将万能数据类型转换成具体数据类型有一定的消耗,所以相对来讲效率会比使用接口的方式低。两种模式通过宏控制,在预处理阶段明确。
在默认情况下模块使用万能数据类型实现,代码如下:
//designM/flyweight.hpp #include<mutex> #include<map> #if !defined(FLYWEIGHT_USE_VARIANT) #define FLYWEIGHT_USE_VARIANT (1) #endif
在使用万能数据类型的情况下,先包含 variant 模块,这个模块提供了任意变量类型的支持,代码如下:
#if FLYWEIGHT_USE_VARIANT == 1 #include"container/variant.hpp" #else
在使用接口方式时先定义了一个空的类 dataItfc__,其主要目的是用来在使用的过程里检查接口来源是否正确,实际暴露出来供给业务代码使用的接口 dataItfc 则是继承于 dataItfc__,代码如下:
//designM/flyweight.hpp namespace private__ { struct dataItfc__{ virtual~dataItfc__(){} }; } #endif #if !FLYWEIGHT_USE_VARIANT
这个模板类用来对外暴露出访问数据的接口,约定具体保存的数据类型。使用纯虚函数约定访问接口,在实际使用时需要实现这两个接口。通过模板类和虚函数进行配合,可以大幅地增加通用性。最后形成在一个享元模块中可以存储不同类型的数据内容,代码如下:
//designM/flyweight.hpp template< typename dataType > struct dataItfc:public private__::dataItfc__ { virtual void set(const dataType&d) = 0; virtual const dataType&get()const = 0; }; #endif //模板参量用来检索值的ID数据类型,通过数据ID访问数据内容 template< typename idType > class flyweight { protected:因为享元模式在很多情况下是多线程操作,所以可以使用 std::mutex 对数据进行加锁。对数据进行读写时先加锁再操作。
使用 std::map 保存数据记录,key 部分是由模板参数 dataType 来确定的,具体值使用了两种方式进行存储,使用宏 FLYWEIGHT_USE_VARIANT 来控制存储方式。当这个宏的值是 1 时使用 variant 存储,否则使用接口对象指针 private__::dataItfc__* 的方式进行存储。使用 std::map 对数据进行整体管理,提供一个享元模式,以便可以存储多个不同的数据,代码如下:
//designM/flyweight.hpp mutable std::mutex m_mutex__; #if FLYWEIGHT_USE_VARIANT == 1 std::map< idType,wheels::variant > m_itfcs__; #else std::map< idType,private__::dataItfc__ * > m_itfcs__; #endif public:
引出迭代类型,方便在实际使用时对数据进行迭代访问。同样因为两种数据的存储方式不同,所以需要根据宏的值控制引出不同的迭代器,代码如下:
//designM/flyweight.hpp #if FLYWEIGHT_USE_VARIANT == 1 using iterator = typename std::map< idType,wheels::variant >::iterator; #else using iterator = typename std::map< idType, private__::dataItfc__ * >:: iterator; #endif public: flyweight(){} virtual~ flyweight() { #if !FLYWEIGHT_USE_VARIANT for(auto item:m_itfcs__){ delete item.second; } #endif }
函数 has() 用来检查享元中是否存在给定 ID 的数据内容。对数据表进行查找前先对数据进行加锁,然后使用 map 容器的 find() 方法进行查找,最后判断结果是否是结束位置。has() 函数的返回值 true 表示存在数据,否则表示数据不存在,代码如下:
//designM/flyweight.hpp bool has(const idType&id) { std::lock_guard< std::mutex > lock(m_mutex__); auto it = m_itfcs__.find(id); return(it! = m_itfcs__.end()); }
模板函数 set() 用来增加或者修改数据。模板参数T是接口类型。对应的实际数据类型会对所有的 PARAMS 对应的数据进行组织和保存。程序首先检查数据类型,然后查找数据。根据查找结果分成两个分支,如果数据存在,则修改数据,否则添加数据内容,代码如下:
//designM/flyweight.hpp template< typename T, typename ...PARAMS > bool set(const idType& id, PARAMS&& ... args) { static_assert((std::is_base_of<private__::dataItfc__, T>::value, "")); static_assert((std::is_class< T >::value && std::is_default_constructible< T >::value) || std::is_arithmetic<T>::value, ""); std::lock_guard< std::mutex> lock(m_mutex__); auto it = m_itfcs__.find(id); if(it != m_itfcs__.end()) { //数据存在修改数据内容 auto p = dynamic_cast<dataItfc<PARAMS...> *>(it->second); p->set(std::forward<PARAMS>(args)...); }else{//数据不存在添加数据内容 try{ auto * t = new T; m_itfcs__.insert(std::make_pair(id, t)); t->set(std::forward<PARAMS>(args)...); }catch(std::bad_alloc&) { ret = false; } } return ret; }
get() 函数用于获取数据。使用 variant 方式时返回的 variant 对象在需要获取具体对象时,可以使用 int a=XXX.get<int>() 的方式获取,代码如下:
//designM/flyweight.hpp #if FLYWEIGHT_USE_VARIANT == 1 variant get(const idType&name) { std::lock_guard< std::mutex > lock(m_mutex__); auto it = m_itfcs__.find(name); if(it! = m_itfcs__.end()){ return it->second; } return{}; }
在使用 variant 的方式下重载 [] 符号,方便使用数组的方式读取数据内容,代码如下:
//designM/flyweight.hpp wheels::variant operator[](const idType&name) { auto it = m_itfcs__.find(name); if(it! = m_itfcs__.end()){ return it->second; } return{}; } #else
在接口方式下,get() 函数返回的是接口类型指针。函数要求显式地提供要求返回的接口数据类型。在编译过程中首先对数据类型进行检查,如果数据类型不符合要求,则停止编译,代码如下:
template< typename T > T * get(const idType&name) { static_assert(std::is_base_of< private__::dataItfc__, T >::value,""); std::lock_guard< std::mutex > lock(m_mutex__);
然后查找数据是否存在,如果存在,则对数据进行指针类型强制转换,然后返回对象指针,否则返回 nullptr。需要注意,使用接口方式实现的是没有 [] 操作符重载的实现,代码如下:
auto it = m_itfcs__.find(name); if(it! = m_itfcs__.end()){ return dynamic_cast<T * >(it->second); } return nullptr; } #endif
此模块还提供了多个方便管理数据的接口,例如删除数据、获取记录数量、获取迭代器等,代码如下:
size_t count(){return m_itfcs__.size();} void erase(iterator it){m_itfcs__.erase(it);} void erase(iterator b,iterator e){m_itfcs__.erase(b,e);} iterator begin(){return m_itfcs__.begin();} iterator end(){return m_itfcs__.end();} };
C++享元模式应用示例
在下面的示例程序中使用 flyweight 模板类来管理一组相似的对象,例如飞机的工厂。在享元模式的实现中采用两种方式,因此示例程序分别针对这两种不同的方式进行编写。第 1 种以 wheels::variant 万能数据类型实现,示例程序如下:
//flyweight.cpp #include<iostream > #include<string > //首先包含享元模块头文件,所有设计模式的模块都是以头文件的方式提供的 #include"designM/flyweight.hpp" //使用namespace暴露出来接口名字,当然也可以不使用 //在不暴露接口名字时就需要自己控制名字空间 using namespace wheels; using namespace dm;
定义飞机类,这个演示使用的是 wheels::variant 模式,在默认情况下 flyweight 模块使用的是 variant 模式,代码如下:
//flyweight.cpp class Airplane{ public: //构造函数 Airplane():capacity_(0){} Airplane(int c):capacity_(c){}
飞机类自己的接口定义,当使用variant模式时可以不用考虑set和get函数的约束,代码如下:
int getC(){return capacity_;} private: int capacity_; }; int main(){
创建一个 flyweight 对象来管理飞机对象。这里直接对 flyweight 模块进行特化,代码如下:
using AirplaneFlyweight = flyweight<std::string >;
创建飞机工厂对象,使用 flyweight 类管理飞机对象实例化,也就是对 flyweight 模块进行实例化。接下来将飞机对象添加到工厂,使用模板参数明确数据类型。函数参数中第 1 个是数据索引,对应于 flyweight<std::string> 中的 std::string;第 2 个参数是容量,对应于 Airplan::capacity_,代码如下:
//flyweight.cpp AirplaneFlyweight airplaneFactory; airplaneFactory.set<Airplane >("Boeing747",500); airplaneFactory.set<Airplane >("AirbusA380",600); //获取飞机对象并打印其名称 //这里对象boeing747和airbusA380是wheels::variant类型的 auto boeing747 = airplaneFactory.get("Boeing747"); auto airbusA380 = airplaneFactory.get("AirbusA380"); //boeing747.get<Airplane >用于将数据转换成Airplane类型 //然后调用成员函数getC() std::cout<< "Boeing747 capacity:" << boeing747.get<Airplane >().getC() << std::endl; std::cout<< "AirbusA380 capacity:" << airbusA380.get<Airplane >().getC() << std::endl; return 0; }
第 2 种针对接口方式实现,在包含 flyweight 模块前,必须定义宏 FLYWEIGHT_USE_VARIANT,并定义为 0,关闭 variant 方式,然后包含 flyweight.hpp,这是因为,在 flyweight 模块中默认使用了 variant 方式,通过将宏值定义为 FLYWEIGHT_USE_VARIANT 模块后编译时就会以接口的方式编译。
定义飞机类,这里就需要继承 flyweight 的数据接口。这里需要注意,应针对数据类型配置模板参数。继承接口模板类 dataItfc,模板参数是实际对象,代码如下:
//flyweight2.cpp #include<iostream > #include<string > #define FLYWEIGHT_USE_VARIANT(0) #include"designM/flyweight.hpp" using namespace wheels; using namespace dm; class Airplane:public dataItfc<int > { public: Airplane():capacity_(0){} //实现对应的虚函数 virtual const int&get()const override{return capacity_;} virtual void set(const int&d)override{capacity_ = d;} private: int capacity_; }; int main() {
创建一个 flyweight 对象来管理飞机对象,创建飞机工厂对象,使用 flyweight 类管理飞机对象,将飞机对象添加到工厂,代码如下:
using AirplaneFlyweight =flyweight<std::string >; AirplaneFlyweight airplaneFactory; airplaneFactory.set<Airplane >("Boeing747",500); airplaneFactory.set<Airplane >("AirbusA380",600);
获取飞机对象并打印其名称。这里获取的是 Airplane 的指针类型,这里和 variant 方式是不一样的,代码如下:
auto * boeing747 = airplaneFactory.get<Airplane >("Boeing747"); auto * airbusA380 = airplaneFactory.get<Airplane >("AirbusA380");
boeing747->get() 是调用虚函数的实现。使用接口模式,虽然可以在一定程度上提高数据的效率,但是由于预定义了接口,所以所有的新的数据类型都必须从这个接口继承。另外可以使用模板参数实现在一个享元模块内存储不同的数据类型,代码如下:
std::cout<< "Boeing747 capacity:"<< boeing747->get()<< std::endl; std::cout<< "AirbusA380 capacity:"<< airbusA380->get()<< std::endl; return 0; }
在这个示例程序中定义了一个 Airplane 类来表示飞机对象,然后使用 flyweight 类来管理飞机对象,创建飞机工厂并增加两个飞机对象(Boeing747 和 AirbusA380),然后通过工厂获取飞机对象,并打印出飞机容量,运行程序后结果如下:
Boeing747 capacity:500
AirbusA380 capacity:600