前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UE4的智能指针 TSharedPtr

UE4的智能指针 TSharedPtr

作者头像
quabqi
发布2021-11-04 10:55:19
2.1K0
发布2021-11-04 10:55:19
举报
文章被收录于专栏:Dissecting UnrealDissecting Unreal

在UE4中有很多种智能指针,除了类似于C++的shared_ptr,unique_ptr等智能指针对应实现外,也有很多种和UObject相关的智能指针实现。这些智能指针的存在,可以让游戏的开发者方便得做好资源、内存以及对象的管理。引擎内部也在大规模的使用着这些智能指针,如果在不了解内部的原理和实现的情况下,而且在网上介绍关于UE4智能指针的用法文章也非常多。在不了解内部实现的情况下,只是照着网上示例或者直接调用UE4的API去用智能指针,就很可能写出BUG或性能糟糕的代码。本文就不过多的去介绍智能指针怎么用了,而是主要来分享一下智能指针的内部实现,在了解实现之后再去使用就会非常的容易,遇到了问题也可以轻松的解决。另外UE4的智能指针也有部分代码设计得非常巧妙,下面会一起分享出来。

那么,UE4到底有哪些智能指针?这里列出我知道的,不保证是UE4中所有的,可能有遗漏,但大部分都是很常用的

1 持有非UObject对象的智能指针

TSharedPtr,TSharedRef,TWeakPtr,TSharedFromThis,TUniquePtr,TUniqueObj,TPimplPtr,TNonNullPtr,TOptional

2 持有UObject的智能指针

TStrongObjectPtr,TWeakObjectPtr,FSoftObjectPtr,TSoftObjectPtr,TSoftClassPtr,FSoftObjectPath,FLazyObjectPtr,TPersistentObjectPtr,FGCObject,FGCObjectScopeGuard,TGCObjectsScopeGuard,TWeakInterfacePtr

由于智能指针覆盖面非常广,我又很想把内部的实现细节都写清楚,所以很难用一篇文章一口气把这些智能指针全部写完。 所以关于智能指针这部分内容我打算分成几篇来写,这篇文章主要介绍非UObject对象的共享智能指针家族这4个类的实现:TSharedPtr,TSharedRef,TWeakPtr,TSharedFromThis。

先分享一下UE4共享指针的内部结构图,也就是标题的配图,点击可以放大,可以作为看源码的参考图

TSharedPtr

这个类对应std::shared_ptr,但是实现上要稍微简单一些,因为本身没有STL那么多的历史和版本负担,先看前面大段注释

最前面有特别长的注释,一张图都截不下硬是来了两张,基本上把这个智能指针的前世今生恩怨纠葛说清楚了。按照像表达的意义简单翻译一下,就是说这个智能指针是抄shared_ptr或boost的智能指针的,好处是让语法干净 ,明确对象的所有者,防止内存泄露。但为什么不直接用STL又要仿照着造轮子呢?因为std的做不到全平台可用,UE4的智能指针可以无缝兼容UE4的容器,可以不要求保证线程安全,这样能带来更好的性能,允许赋值空指针,提供了一些UE4自己的辅助函数,而且UE4的性能更好(包括将函数inline,内存管理,虚函数的使用等),就只占2倍(16字节)普通指针内存,更符合UE4的命名规范,内部实现是不抛异常的,不依赖任何第三方库,更容易调试等原因,那么下面就来看怎么实现的。

先看成员变量:

只有两个变量,其中Object就是原始指针,而后面的WeakReferenceCount就是管理这个指针引用计数的对象。

这个对象里也就只有一个指针,指向的类型是FReferenceControllerBase,所以这就证明了确实一个TSharedPtr确实只占16字节(64位机器一个指针8字节)。

如果写过苹果老版的objc,肯定也知道要主动AddRef,Release,新版支持arc倒是能自动做引用计数的增加和减少了但还是要求自己心里清楚,如果以前用过C++的shared_ptr,肯定清楚引用计数是在拷贝构造和赋值运算符时增加的,在析构函数里减少的,如果最后引用计数为0就释放掉指针指向的对象,是要更方便一些的,UE4的实现跟他完全一致所以把这样的好处也保留了下来。除此外还可以主动调用Reset释放掉指针,留一个空壳智能指针对象。

先看赋值操作,可以看到传普通引用的赋值就是拷贝,传右值引用的赋值就是转移所有权

因为我们知道这里普通的指针Object拷贝没什么特别的,那么引用计数管理肯定是后面这个SharedReferenceCount做的,再看拷贝构造函数和移动构造函数,发现也是一样的情况。

那就来看这个SharedReferenceCount类型的源码,就真相大白了

那什么时候释放呢?再看他的析构函数

可以看到析构的时候,引用计数就减1,为0的时候会调用DestroyObject,同时释放掉所有的WeakReference。

上面这种是非线程安全的实现,因为前面也看到ReferenceController的类型有个Base,所以这里就可以在构造的时候就使用不同实现,还有一种线程安全的实现,引用计数用了原子变量管理,增加和减少都是CAS操作,这里就不贴了,相对来说在没多线程的情况下,肯定这种更快一些。使用不同实现其实就是通过模板的第二个参数决定的,其实就两种,什么都不填的时候默认Fast(其实一般就是NotThreadSafe,可以通过宏开关控制),看下图

你肯定会好奇,为什么引用计数为0,最后释放的时候,调用的是ReferenceController的DestroyObject而不是直接delete对象。为了解释这个原因,先看看DestroyObject里面做了什么:

可以看到,调用了模板传入参数的DeleterType,并把Object作为参数传了进去。这个Deleter到底是什么呢?可以通过搜索TReferenceControllerWithDeleter找到下面这两个函数。

可以看到,默认就是delete对象,但还额外提供了一个CustomDeleter,可以自己提供释放操作。这个CustomDeleter就是在智能指针其中一个构造函数上指定的。

这样,智能指针除了支持管理本身C++对象,用完自动delete这样的对象,还可以支持一些不是通过delete来释放的对象。比如可以临时创建一个RT放到TSharedPtr里,再单独提供一个Deleter函数对象,内部调用UKismetRenderingLibrary上的ReleaseRenderTarget2D函数(下图这个函数),就可以将这个类通过TSharedPtr传给外部其他业务到处用,就不用担心这个RT是不是会泄露了,等所有业务都不用的时候,就会自动调用到Deleter来释放RT。当然其他类似的对象需要主动销毁的,包括程序Spawn出来的Actor,程序创建出来的Component等或其他需要主动销毁的资源,都能这样做。

除了自定义的Deleter外,还有一个比较特殊构造ReferenceController的函数NewIntrusiveReferenceController,可以看到这里的参数很不一样,可以接受一系列的参数,通过Forward转发给了构造出来的IntrusiveReferenceController。这里先不解释具体做了什么,跟构造智能指针的MakeShared函数有关系,放在最后来说。如果你看过stl的源码或用过std::shared_ptr,肯定知道创建指针的时候要尽可能用make_shared而不是直接使用构造函数,他们在内存的分配上有本质的差别。

因为智能指针重载了->和*运算符,所以完全可以像普通指针一样来使用,当使用->就是访问指针本身指向的对象那个功能,使用*就是指向指针的对象,而使用.就是调用智能指针这个对象上的函数(虽然叫做指针但实际是一个对象)

最常用的函数肯定就是判空,清空(上面有说是Reset),查看引用计数

如果写过苹果的objc肯定知道AutoReleasePool,这里也可以通过上面查看计数的函数,简单的在UE4里来实现一个类似的AutoReleasePool。具体做法是,在构造智能指针时,也拷贝一份传给AutoReleasePool内部持有,这样只要这个Pool存活就能保证至少有一个引用,内部的对象不会被释放,当Pool需要释放的时候可以检测一下计数是否为1,如果为1说明除了自己这个Pool外没有任何引用,就可以主动清理掉这个智能指针。再扩展一下这个用法,如果计数为1的时候不清理,而是把这个指针拿着的对象放到另一个地方,让下次申请的时候从这里复用,这不就变成了一个已经实现好的对象池了。注释写的不一定快,尽量只用在调试,但其实不要紧张,如果使用NotThreadSafe的情况下,这个是很快的,可以放心的用,当然用在多线程的时候自己还是权衡一下是否需要。检测是否为1,UE4也单独包装了一个函数,如下图所示,可能也考虑到了有业务逻辑确实需要这么做。再搭配上前面说的Deleter,对象池就可以管理任意资源了。

TSharedRef

然后再来说一下TSharedRef,这个类和TSharedPtr唯一区别就是TSharedRef在初始化的时候不能为空,就像C++的指针和引用的区别一样,引用必须在构造的时候就必须有被引用的对象。当然因为这个类本质还是一个C++的类,这里还必须像指针一样使用->操作,不像引用在编译器下,把指针的->操作都换成了引用的.操作。可以说这个类是UE4特有的,STL中并没有对应实现。

TSharedPtr可以转换成TSharedRef,如下图所示,但要注意必须是有效的指针,否则会触发check报错崩溃。

TSharedRef其他所有的操作都完全和TSharedPtr一样,需要特别注意的一点是,TSharedRef虽然不能为空,也没有Reset函数,但却可以通过拷贝赋值,拷贝构造,移动赋值,移动构造等来换掉内部的指针,老的会被释放(引用计数-1),这些操作和TSharedPtr完全一致。

为什么要专门提这一点,是因为UE4里有些比较睿智的API,理论上可以接受空对象,但函数的参数却只接受TSharedRef而不要TSharedPtr,这时还是可以自己搞个空壳对象换掉TSharedRef里的老对象,丢给UE4来曲线救国,虽然不推荐这么做,但是自己清楚自己在做什么的时候,也没什么问题。只要项目会魔改引擎又不想动接口,这种事情一定会遇到。

再来说下TWeakPtr。

TWeakPtr

这个看名字就可以知道,就是弱指针,不能单独使用,和TSharedPtr的成员变量的结构一样,但唯一区别就是这里的第二个变量是WeakReferenceCount,会和SharedPtr的计数区分开,单独记。对应STL中的std::weak_ptr,实现也基本上是一致的。

前面说FReferenceControllerBase时也一起说了这里。

和SharedPtr一样,计数为0的时候,会删掉ReferenceController

在SharedPtr计数为0的时候,也会释放1次ReleaseWeakReference

你可能会好奇,这里是SharedPtr的减计数函数,并没有拿WeakPtr的引用,为什么还要释放一次呢?可以往前看,在构造ReferenceController的时候,默认就是1,所以要把这个1给减掉。

这里需要注意的是,TSharedPtr和TWeakPtr,如果这个指针是互相转换得到的,他们拿着的ReferenceController都是同一个。因为WeakPtr上并没有重载->和.这样的指针运算符,所以是不能直接使用的,需要从TWeakPtr转换成TSharedPtr来使用,当然使用前需要IsValid先判断对象是否还活着的

可以看到,Pin实际调用的是TSharedPtr的私有的构造函数,关键代码就是红框这里,通过WeakReferenceCount来构造SharedReferenceCount

进入到这个构造函数,可以看到里面调用的是ConditionallyAddSharedReference

这个是有条件的增加引用计数,至于是什么条件呢?注释也写了,Shared计数,至少为1的时候,才会构造,否则会直接把ReferenceController置为nullptr。

可以看到为0的时候直接return false,大于0才让引用计数+1,这样就保证了TWeakPtr在对象还活着的时候能够还原为TSharedPtr。

TSharedFromThis

对应STL中的std::enable_shared_from_this,用法就像注释所说的,需要自己的类继承这个类,就可以自动将当前的对象进行引用计数管理,之后通过AsShared()函数就可以得到this的TSharedPtr传给外部使用。

为什么要特意搞一个这样的类呢?可以想象一下,如果一个对象被外部的某个TSharedPtr管理,在自己的成员函数内,怎样获取外部的这个智能指针呢?如果直接把this传给TSharedPtr,这个TSharedPtr就会和外面那个TSharedPtr分别做引用计数,当第一个用完后就会销毁对象,第二个用完时就会去删一个野指针,这显然是有BUG的。你可能会说这是用法不对,确实如此,但是在成员函数内确实很难拿到外部这个TSharedPtr,提供了TSharedFromThis就可以有一个过渡,这样能保证获得的都是一起计数的TSharedPtr。

你可能也会想这么做的目的是什么?自己成员函数内直接用this不就好了,可以考虑这样一种情况,假如成员函数里调用了一个带异步回调或者Delegate的函数,等异步回来后需要再做点收尾的事情,比如异步加载这件事,先调用RequestAsyncLoad,后面有个回调,成功加载好了之后再对this做剩下的事情,这里肯定就会把this作为lambda的upvalue(我也不知道应该叫什么就延用lua的命名吧,ue4的委托内部叫payload),这时怎么保证在加载期间this一直存活呢?如果外面的智能指针在加载期间就释放了,当回调完成时,这个upvalue里的this就是野指针,这显然是有问题的。这种情况下如果把从TSharedFromThis获取到的智能指针作为lambda的upvalue而不是this本身,即使外部的TSharedPtr在加载期间释放了,lambda内部还留有一个TSharedPtr,这样引用计数至少为1,在回调回来时对象肯定依然存活,这样就能正确运行了。UE4委托提供了CreateSP静态函数,方便快速创建带智能指针的Delegate,可以直接把AsShared的结果作为参数传进去。

如果没用过TSharedFromThis,可能直接这么描述有点迷茫,还是直接看内部是怎样实现的吧。

成员变量,就只有一个,其实就是自己(this指针)包装的TWeakPtr,自己拿着自己的弱引用。因为只是一个变量,没赋值的时候并没有指向自己,所以需要找到赋值的地方:

可以看到只有在调用UpdateWeakReferenceInternal这个函数的时候,才会被参数传进来的SharedPtr初始化好。这个函数是被一组全局函数EnableSharedFromThis调用的,而这组全局函数EnableSharedFromThis,是在TSharedPtr构造函数里被调用的。

你可能会好奇,为什么要这么中转一遍,直接在构造函数里调用UpdateWeakReferenceInternal不就好了吗?这是因为并不是所有用智能指针的类都继承了TSharedFromThis,为了保证只有继承的类生效,通过中转一层可以用模板的多态区分开。这样,没继承的会匹配到下面这个函数,可以看到是个空函数,什么都没做。

那么,这就说明了自己写的类继承了TSharedFromThis,并不是马上就变成了智能指针,而是在第一次用的时候才生成,如果一直不用智能指针,就和普通的类完全一样。this在这里就是过度不同的TSharedPtr的桥梁,让都是指向this的TSharedPtr可以用同一套引用计数。

最后看AsShared是怎样实现的,其实就是Pin自己成员变量WeakThis,如果外部已经释放了,肯定Pin不到,说明this已经是野指针了。都野了却能调用到AsShared,说明代码一定有BUG,所以后面有个check,可以作为一个保护方便定位问题。还有一点要注意的是,AsShared不能在析构函数内使用。

辅助函数

先看这两个,MakeShareable和MakeShared

你可能会好奇,这两个函数名字这么像,都是创建智能指针,为什么要用两个不同的名字,到底有什么区别呢?

先看第一个函数,就是传入一个裸指针对象InObject,构造了一个FRawPtrProxy,因为正常写法都是TSharedPtr<T> xxx = MakeShareable(pObj);UE4不清楚到底是用什么智能指针接收,但是可以保证的是这些智能指针都会用到ReferenceController,所以就提前把这个构造好存在FRawPtrProxy,外部接收的每种智能指针,都有适配FRawPtrProxy的构造函数,内部不用再做一遍构造ReferenceController了,直接取这个裸指针和ReferenceController就好了。注释也说了,在赋值和函数需要返回SharedPtr时很有用。

再看第二个函数MakeShared,他接收的参数是一堆可变的参数,看注释也说了,等价于std::make_shared,直接在一块内存上构造智能指针和对象本身,好处是对内存就非常友好,减少了一个内存碎片。可以想象一下,如果直接使用TSharedPtr(new T())的形式构造智能指针,其中new T()会先分配一次内存,然后TSharedPtr内部构造ReferenceController又分配了一次内存,这样两块内存不是连续的,耗时也会更高一些,在大量使用智能指针时,性能肯定就不那么好了。这里内部实现就是前面提到的NewIntrusiveReferenceController,这种特殊的构造方式。可以看到内部其实就是直接在Controller自己的内存上,通过placement_new来构造出实际对象,ObjectStorage大小和外部对象一样,但通过模板抹去了对象本身类型,在编译期就计算出大小的一个变量,而整个智能指针的大小就是Controller基类+ObjectStorage的大小,一次分配就完成了构造,这是一个很出色的设计。因此在实践中一定要优先使用这个函数。

当然还有其他几个辅助函数,类型转换和清理数组,其中类型转换对应于STL中的std::static_pointer_cast和std::const_pointer_cast,不过STL在C++17才有,这里UE4还是比较有优势的。

整体上UE4的智能指针,代码比STL的要简单不少,但是易用性和性能也很高,该有的都有了,引擎的代码也到处都在使用。另外UE4还提供了一个测试代码,默认不参与编译但可以打开WITH_SHARED_POINTER_TESTS宏来参与编译,里面有不少智能指针的示例,基本上把共享指针的用法覆盖全了,也可以作为使用参考,如果有兴趣可以断点这里的函数。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TSharedPtr
  • TSharedRef
  • TWeakPtr
  • TSharedFromThis
  • 辅助函数
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档