智能指针之设计模式6
本系列文章探讨了智能指针和设计模式的关系,前面五篇文章介绍的是使用设计模式实现了智能指针的相关特性,比如使用工厂模式控制了智能指针对象的创建,使用代理模式控制了资源对象的销毁。本文介绍一下使用智能指针来帮助我们实现相关的设计模式。
1、在创建型设计模式中,既然是创建对象,肯定要涉及到对象的生存期管理,那么用智能指针来实现创建型的设计模式就再正常不过了。
首先看工厂方法模式的例子:
使用工厂方法模式创建对象时,一般都是在堆上创建的,建议使用unique_ptr来返回创建的对象实例,因为使用unique_ptr更灵活,这样最终可以支持unique_ptr或者shared_ptr类型的智能指针来管理产品对象的生存期,如果要想使用shared_ptr类型的智能指针,用户可以根据需要把它转换成shared_ptr类型,但是如果反过来,让工厂方法返回shared_ptr类型的话,就无法满足用户使用unique_ptr指针的要求。比如:
// 产品基类
class product {
public:virtual ~product() = default;
};// 工厂基类
class factory {
public:virtual unique_ptr<product> create_product() const = 0;virtual ~factory() = default;
};// 具体产品类
class productA : public product {
};// 具体工厂类
class factoryA : public factory {
public:unique_ptr<product> create_product() const {return make_unique<productA>();}
};// 只使用unique_ptr的应用场景
void test1(const factory &fac) {unique_ptr<product> up = fac.create_product();//...
}// 使用shared_ptr的应用场景
void test2(const factory &fac) {unique_ptr<product> &&up = fac.create_product();// 如果要使用shared_ptr,可以把unique_ptr对象转化为shared_ptr对象shared_ptr<product> sp(move(up));//...
}
再看一个使用智能指针管理单例对象的例子:在程序退出后,释放单例资源。如果单例对象是在堆上创建的,因为单例对象的生存周期不需要程序管理,可以使用裸指针类型来访问单例。假设在程序退出后,希望能够销毁单例对象,因为没有合适的地方去delete这个单例指针,可以借助于智能指针来实现这个功能。比如下面的实现例子:
class singleton {static singleton *instance;singleton() = default;~singleton() = default;
public:static singleton *get_instance();static void destroy(singleton *ptr) {delete ptr;}
};singleton *singleton::instance = nullptr;
// unique_ptr对象来管理单例对象的生存期
unique_ptr<singleton, void(*)(singleton*)> singleton_helper(nullptr, singleton::destroy);singleton *singleton::get_instance() {if (instance == nullptr) {instance = new singleton();// 创建单例对象之后,放入unique_ptr对象中singleton_helper.reset(instance);}return instance;
}
2、在其它模式中,凡是涉及到要引用一个对象时,可以考虑使用shared_ptr管理对象,这样对象之间就不用考虑使用裸指针来引用对象时的生存期问题了,它的生存期完全交由shared_ptr来管理。
设计模式中涉及到多个对象之间的组合起来一起工作,而且大都是基于基类作为接口的,为了实现面向对象的多态机制,交互传递的都是指针引用类型。这样就涉及到对象的生存期管理了,更重要的是有时候有一个对象会被多个对象引用,如何管理这个对象的生存期呢,它归谁所有呢?既然是共同使用的,因此使用了shared_ptr,如果有互相引用的场景,就考虑一方使用weak_ptr。例如,在实现享元模式时,因为享元对象必须被共享,所以可以使用shared_ptr来引用享元对象。像其它的模式,比如桥梁模式、观察者模式,还有PIMPL惯用法,也一般也会用到shared_ptr。这也是shared_ptr典型的应用场景,比较简单,就不再举例说明了。
当然,如果一方在失去生存期时需要同时解除引用关系,也可以使用weak_ptr类型。例如,观察者模式中的观察者对象,在需要主题对象给它发送通知时,把观察者对象注册到主题中去,通常情况下,当一个观察者失去生存期时要从主题中注销掉,也可以不用去注销,那就在主题中使用weak_ptr来保存观察者,看下面的例子:
// 观察者基类
class observer {
public:virtual void update() = 0;// ...virtual ~observer() = default;
};// subject对象一般要比observer对象活得时间长
class subject {// Observer的生存期没有和Subject紧密绑定,使用weak_ptr存放vector<weak_ptr<observer>> list;
public:void regist(weak_ptr<observer> observ) {list.push_back(observ);}void notify() {for (auto weak : list) {auto shared = weak.lock();if (shared) {shared->update();} else {...从list中删除weak }}}
};class my_observer : public observer {
public:virtual void update() {...}
};
在这里,观察者在Subject对象中注册时,使用了weak_ptr类型,这样如果一个观察者失去了生存期被销毁后,可以不用向Subject对象进行注销操作。这样,当Subject在向所有注册的观察者发送通知时,先对观察者进行检查,如果发现它一个观察者已经销毁了,就把它从注册表中移除,这样就简化了观察者一方的流程,只注册就可以了。
结语
本系列文章介绍的只是智能指针在设计和实现时所体现出的设计模式的思想,如果纯粹从面向对象编程上来说,它们没有那么分明的类层次体系,有的甚至连类都没有,并不是严格意义上面向对象的设计模式,和GOF经典设计模式在外在形式上有所差别。
首先它们都使用了模板技术,模板技术的特点是鸭式辩型(duck type),只要提供的类型中有符合形式的接口(比如函数或者操作符)就可以了,并不像面向对象编程那样,接口函数需要使用基类来定义,派生类来继承并重写这些接口函数。其次,C++有操作符重载,在某些场合中可以无需像传统的面向对象那样叠床架屋般的设计一套接口基类,只要模板的实参类型类符合相关函数接口(如操作符)就可以了。比如,C++17中的variant类模板和visit()函数模板,就可以替换GOF的访问者模式,无需再严格地按照面向对象技术去编写那些element类和visitor类以及它们的派生类了。
总之,不管形式如何,重点是理解它们所体现出的设计模式的思想,如果仔细体会智能指针的实现方案及机制,它们的确符合这些模式背后的思想,熟悉设计模式的朋友不妨认真体会一下,相信会大有收获的:
工厂模式控制了资源对象的创建过程。工厂模式在控制智能指针对象创建过程的同时,也控制了资源对象使用new在堆上的创建,如make_unique()和make_shared(),也控制了shared_ptr对象从this指针创建的过程,如shared_from_this();
代理模式控制了资源对象的销毁过程。代理模式可以控制资源对象在智能对象失去生存期时也立即销毁,如unique_ptr,也可以控制它在最后一个引用它的智能指针对象失去生存期时再销毁,如shared_ptr。
策略模式实现了删除器的灵活性。为了让智能指针能够灵活地控制不同资源的销毁过程,还使用策略模式设计了删除器的角色,通过删除器可以让之智能指针在管理资源方面能够扮演更多的角色。
模板方法模式简化了用户通过this创建shared_ptr对象的实现。通过模板方法模式,在模板类enable_shared_from_this中封装了通过this指针创建shared_ptr对象的过程,即模板方法,只要派生类对象被一个shared_ptr对象托管就可以了,即等同于钩子方法,简化了用户的工作量和犯错的可能。
此外,为了让智能指针支持二维指针的访问和操作,还按照适配器模式为它们实现了访问二维指针类型的接口。