智能指针之设计模式4
前面的文章介绍了使用工厂模式来封装智能指针对象的创建过程,下面介绍一下工厂类
enable_shared_from_this的实现方案。
4、模板方法模式
在前面的文章分析过,enable_shared_from_this<T>类是一个工厂基类,提供的工厂方法是shared_from_this(),而shared_ptr<T>就是一个具体的产品类,每一个shared_ptr<T>对象都需要一个具体的工厂类来创建它,这个具体的工厂类就是T,它是enable_shared_from_this<T>的派生类,同时又是模板参数类型。那么,C++标准库是怎么通过this指针来创建shared_ptr<T>对象的呢?
按照常规的做法,可以继承工厂基类然后由派生类来实现基类定义的工厂方法接口,然而当在一个对象内部通过this指针创建一个shared_ptr对象时,却不是一件容易的事情:
首先,要判断资源对象是否是在堆中,如果资源对象不是在堆中,那么使用shared_ptr<T>(this)创建的智能指针对象,在析构时就会发生异常。
其次,要确定资源对象是否已经被shared_ptr管理了,如果管理了,也不能直接使用shared_ptr<T>(this)创建的智能指针对象,否则会导致由两个不同的shared_ptr来管理this,最后会发生重复释放内存的错误。
再次,如果在资源对象内部使用shared_ptr<T>(this)创建了智能指针对象,但是用户在外部是不知道的,可能又使用shared_ptr<T>(obj_ptr)创建了智能指针对象,也会发生重复释放内存的错误。
如果这个创建过程,完全交给程序员来实现,需要仔细地约束各方面的代码实现,还得需要相关人员认真地评审,防止出现上面描述的错误,带来了极大的风险和不便。C++标准库的思路是交由代码来解决,把通过this指针创建shared_ptr对象的代码封装起来形成固定的模板套路,只把需要定制的部分交给派生类去实现,也就是模板方法模式,即在模板基类中把通过this指针创建shared_ptr对象的过程封装成“模板方法”,它执行过程中要使用具体派生类提供的“钩子函数",这样哪个类要想提供shared_from_this()功能,就继承模板基类并实现”钩子函数“就可以了。下面是GOF模板方法模式的结构图:
从结构图中可以看出,模板方法模式实际上就是面向对象编程中的继承和动态多态机制,也就是在基类中定义virtual函数接口,也就是“钩子方法”,在派生类中实现virtual函数,从而让基类中的“模板方法”能够通过动态绑定的方式,来调用派生类提供的virtual函数。
不过enable_shared_from_this<T>作为模板基类与上面的模板方法模式在形式上不同,它没有采用面向对象编程的多态机制,而是利使用了一种特殊的继承方式实现的,即CRTP。CRTP采用了C++的模板技术,虽然也是继承方式,但却是在基类中通过把自己强制转为派生类类型的方式来调用派生类实现的函数,这是二者最大的区别。模板方法模式是一个共有的基类,然后有多个同一族类的派生类,它们复用了基类中的“模板方法”,并重写了virtual函数,属于动态绑定;而CRTP方式是一个基类仅有一个派生类,这样完全可以把基类强制转换成派生类,调用的是派生类的非virtual成员函数,属于静态绑定机制。同样,因为CRTP派生类继承了基类,它复用了基类的“模板方法”,因此,外界也可以调用派生类的“模板方法”来实现具体的功能。
shared_from_this()创建shared_ptr对象的套路是,定义了一个weak_ptr数据成员,在创建shared_ptr对象时,就使用这个weak_ptr成员来创建,派生类显然是无法设置这个weak_ptr成员的,也就是说无法设计成提供一个“钩子函数”的形式,而weak_ptr类型和shared_ptr类型息息相关,因此enable_shared_from_this<T>就让shared_ptr<T>作为它的友元类,要求shared_ptr在创建对象时,同时初始化这个weak_ptr成员。这样,如果派生类T没有使用shared_ptr对象来管理自己的生存期,也就初始化不了这个weak_ptr成员,进而也就无法通过this创建出shared_ptr对象,而通过weak_ptr对象创建的shared_ptr对象和原来的shared_ptr对象共享T资源对象的所有权,也不会发生直接使用this指针创建了shared_ptr对象之后,导致由两个不同的shared_ptr来管理this的错误。
可见,在这个模板方法模式中,“钩子方法”不是一个具体函数,它是一个抽象的概念,或者说是一个“钩子数据”,在这里就是它的weak_ptr类型的数据成员,模板方法在实现套路化的逻辑时,需要使用weak_ptr对象数据成员来创建shared_ptr对象,这个weak_ptr对象是在创建shared_ptr对象时初始化的,因此要求创建它的派生类对象时,必须要被一个shared_ptr对象托管,这样shared_ptr在创建对象时,就会同时初始化这个weak_ptr成员。也就是说,派生类不需要提供具体的“钩子函数”,只要它在创建时候,同时创建一个shared_ptr对象来管理它的生存期,它自然而然地就为基类的“模板方法”提供了“钩子数据”。
这里,相当于把通过this指针创建shared_ptr对象的过程给封装成模板方法,谁要是想通过this指针创建shared_ptr对象就可以直接继承enable_shared_from_this类,它的模板方法自己实现了这个功能,派生类无需专门编写代码,为程序员编程带来了便利。下面是结构图:
enable_shared_from_this<resource>是工厂基类也是模板方法类,它的成员函数shared_from_this()是"模板方法",职责是使用资源对象resource的this指针来创建一个shared_ptr对象;资源对象resource类是它的一个公共派生类,是工厂派生类也是模板方法派生类,具体产品对象shared_ptr<resource>就是从resource中创建出来的,也就是一个产品对象通过“模板方法”创建了一个管理自己生存期的shared_ptr对象。示例代码如下:
class resource : public enable_shared_from_this<resource> {data_obj obj;public:void process() {auto sp = shared_from_this();...}... // 其它成员函数
};
...
只要继承了enable_shared_from_this<T>类,它就是一个模板方法派生类,就可以使用模板方法类的shared_from_this(),然而这只是“模板方法”函数,还得需要派生类自己提供“钩子方法”函数,也就是保证派生类创建的对象被shared_ptr托管,这样就可以根据模板方法来通过this指针创建shared_ptr的副本了。因此,resoure资源对象可以通过:
shared_ptr<resource> res1(new resource);
shared_ptr<resource> res2 = make_shared<resource>();
shared_ptr<resource> res3 = weak_ptr_obj.lock();
shared_ptr<resource> res4(weak_ptr_obj());
shared_ptr<resource> res5(unique_ptr_obj());
等这些方式来创建shared_ptr<resource>对象,可以把这些创建方式等同于“钩子方法”,派生类resource只要能够创建出shared_ptr<resource>对象,当在resource对象内部使用this指针调用“模板方法”shared_shared_this()时,就会按照固定的套路创建出一个shared_ptr<resource>对象。显然这样就简化了根据this创建shared_ptr<resource>对象的过程,程序也不需要作太多的考虑,只要保证resource对象是使用shared_ptr智能指针管理的就可以了。
总之,工厂方法shared_from_this()是使用模板方法模式实现的,而模板模式又是使用CRTP惯用法来实现的。
参考:
https://cloud.tencent.com/developer/article/2362395