Unreal是如何驾驭内存的 · 第8章 原生智能指针——TSharedPtr、TWeakPtr与TUniquePtr

张开发
2026/4/10 15:49:51 15 分钟阅读

分享文章

Unreal是如何驾驭内存的 · 第8章 原生智能指针——TSharedPtr、TWeakPtr与TUniquePtr
第8章 原生智能指针——TSharedPtr、TWeakPtr与TUniquePtr本章目标深入理解UE自研的智能指针体系——为什么不用std::shared_ptrUE的引用计数块如何设计线程安全模式的选择以及如何避免循环引用。8.1 为什么UE不用标准库智能指针UE在标准库提供std::shared_ptr之前就开发了自己的智能指针UE的智能指针系统从UE3时代就存在。即使C11标准化后UE仍然保留自己的实现原因对比维度std::shared_ptrUETSharedPtr引用计数位置独立控制块或make_shared合并分配独立FReferenceController或MakeShared合并分配线程安全始终原子操作无选择可选ESPMode::ThreadSafe|NotThreadSafe弱引用std::weak_ptrTWeakPtr非空引用不支持TSharedRef——编译期保证非空调试支持平台相关USING_SHARED_POINTER_DEBUGGING宏与UE工具链集成需适配原生支持关键UE的ESPMode::NotThreadSafe模式不使用原子操作在单线程使用的场景下比std::shared_ptr快。8.2 TSharedPtr——共享所有权8.2.1 内存布局templateclassObjectType,ESPMode ModeESPMode::ThreadSafeclassTSharedPtr{private:ObjectType*Object;// 8字节原始指针SharedPointerInternals::FSharedReferencerModeSharedReferenceCount;// SharedReferenceCount内部持有 FReferenceController* (8字节)// 总计 TSharedPtr 16字节};TSharedPtr16字节Object*指向对象ReferenceController*指向引用计数块你的对象堆上FReferenceControllerSharedCount: 2WeakCount: 1Destructor: fn(ptr)8.2.2 FReferenceControllerclassFReferenceControllerBase{public:int32 SharedReferenceCount;// 强引用计数int32 WeakReferenceCount;// 弱引用计数// 销毁被管理的对象virtualvoidDestroyObject()0;// 释放引用计数块本身// 弱引用计数归零时调用};// 具体实现——带有删除器templatetypenameObjectType,typenameDeleterTypeclassTReferenceControllerWithDeleter:publicFReferenceControllerBase{ObjectType*Object;DeleterType Deleter;virtualvoidDestroyObject()override{Deleter(Object);// 调用删除器}};8.2.3 引用计数的增减// 拷贝构造增加强引用计数TSharedPtr(constTSharedPtrOther):Object(Other.Object),SharedReferenceCount(Other.SharedReferenceCount){// SharedReferenceCount拷贝时内部会增加SharedCount}// 析构减少强引用计数~TSharedPtr(){// SharedReferenceCount析构时内部会减少SharedCount// 如果SharedCount归零调用DestroyObject()销毁对象// 如果WeakCount也归零释放FReferenceController本身}8.2.4 线程安全模式// 线程安全模式使用原子操作templateclassTSharedPtrT,ESPMode::ThreadSafe{// SharedCount的增减使用 FPlatformAtomics::InterlockedIncrement/Decrement// 开销每次拷贝/赋值/析构 ~5-10ns原子操作};// 非线程安全模式普通加减templateclassTSharedPtrT,ESPMode::NotThreadSafe{// SharedCount的增减使用普通 /--// 开销每次拷贝/赋值/析构 ~1-2ns};选择建议如果对象只在一个线程上使用 →ESPMode::NotThreadSafe如果对象可能跨线程共享 →ESPMode::ThreadSafe默认如果不确定 → 使用默认ThreadSafe8.3 TSharedRef——非空共享引用TSharedRef是TSharedPtr的变体编译期保证非空// TSharedRef不能为nullTSharedRefFMyClassRefMakeSharedFMyClass(Args...);// Ref永远有效——没有 IsValid() 或 nullptr 的检查// 不能默认构造// TSharedRefFMyClass Ref; ← 编译错误// 可以隐式转换为TSharedPtrTSharedPtrFMyClassPtrRef;// OK// 反过来需要显式检查TSharedRefFMyClassRefPtr.ToSharedRef();// 如果Ptr为null会断言内存布局与TSharedPtr完全相同16字节。区别纯粹是编译期的类型约束。8.4 TWeakPtr——弱引用8.4.1 解决的问题弱引用允许你观察一个对象而不阻止其销毁TSharedPtrFMyWidgetWidgetMakeSharedFMyWidget();TWeakPtrFMyWidgetWeakWidgetWidget;// Widget仍然只有1个强引用// WeakWidget不增加强引用计数Widget.Reset();// 强引用归零对象销毁// 弱引用检测对象是否仍然存在if(TSharedPtrFMyWidgetPinnedWeakWidget.Pin()){// 对象仍然存在Pinned是一个临时的强引用Pinned-DoSomething();}else{// 对象已被销毁}8.4.2 内存模型TWeakPtr16字节可能已析构Object*WeakReferencer*你的对象FReferenceControllerSharedCount: 0 ← 对象已销毁WeakCount: 1 ← WeakPtr持有关键当SharedCount归零时对象被销毁但FReferenceController还在因为WeakCount 0。TWeakPtr::Pin()通过检查SharedCount判断对象是否仍然存活。TSharedPtrTTWeakPtrT::Pin()const{// 尝试原子地将SharedCount从N增加到N1// 只有N 0时才成功对象仍存活if(ReferenceControllerReferenceController-SharedCount0){// 增加强引用计数returnTSharedPtrT(Object,ReferenceController);}returnnullptr;// 对象已销毁}8.5 TUniquePtr——独占所有权8.5.1 基本用法// 独占所有权——不可拷贝只可移动TUniquePtrFMyResourceResourceMakeUniqueFMyResource(Args...);// 不能拷贝// TUniquePtrFMyResource Copy Resource; ← 编译错误// 可以移动TUniquePtrFMyResourceMovedMoveTemp(Resource);// Resource现在为nullMoved持有对象// 超出作用域时自动销毁8.5.2 内存布局templatetypenameT,typenameDeleterTDefaultDeleteTclassTUniquePtr{private:T*Ptr;// 仅一个指针8字节// Deleter通常是空类Empty Base Optimization// 所以TUniquePtr总共只有8字节——与裸指针完全相同};TUniquePtr的内存开销为零相对于裸指针——相同的8字节。没有引用计数块。8.5.3 自定义删除器// 自定义删除器structFMyResourceDeleter{voidoperator()(FMyResource*Resource)const{Resource-ReleaseGPUResources();// 先释放GPU资源deleteResource;// 再释放内存}};TUniquePtrFMyResource,FMyResourceDeleterResource(newFMyResource());8.6 MakeShared vs MakeShareable8.6.1 MakeShared推荐TSharedRefFMyClassRefMakeSharedFMyClass(Arg1,Arg2);优势类似std::make_shared在一次分配中同时分配对象和引用计数块FReferenceController015FMyClass1631合并分配一次 FMemory::Malloc 调用。减少一次Malloc调用性能提升减少内存碎片连续内存更好的缓存局部性8.6.2 MakeShareableFMyClass*RawPtrnewFMyClass();TSharedRefFMyClassRefMakeShareable(RawPtr);用于包装已有的裸指针。需要两次分配一次new FMyClass一次为引用计数块。两次分配FReferenceController 和 FMyClass 位于两块不连续的内存中。8.7 循环引用问题8.7.1 问题classFA;classFB;classFA{TSharedPtrFBB;// A强引用B};classFB{TSharedPtrFAA;// B强引用A → 循环};// 创建循环TSharedPtrFAaMakeSharedFA();TSharedPtrFBbMakeSharedFB();a-Bb;b-Aa;// a和b超出作用域后// a的引用计数: 1b-A持有→ 不会销毁// b的引用计数: 1a-B持有→ 不会销毁// → 内存泄漏8.7.2 解决方案用TWeakPtr打破循环classFA{TSharedPtrFBB;// A强持有B → 控制B的生命周期};classFB{TWeakPtrFAA;// B弱引用A → 不阻止A销毁};// 现在// a超出作用域 → SharedCount归零 → FA销毁// FA析构 → a-B的TSharedPtr析构 → FB的SharedCount归零 → FB销毁// 无泄漏设计原则在双向引用中Owner强持有MemberMember弱引用Owner。8.8 智能指针的性能开销操作TUniquePtrTSharedPtr(NotThreadSafe)TSharedPtr(ThreadSafe)裸指针sizeof8B16B16B8B创建MakeUnique/Sharednewnew × 1-2new × 1-2new拷贝N/A不可拷贝count (~2ns)atomic (~8ns)赋值 (~0.5ns)解引用 裸指针 裸指针 裸指针基准销毁delete–count, 可能deleteatomic–, 可能delete手动delete结论在性能关键的热路径上如果所有权语义明确TUniquePtr是最佳选择——与裸指针零差异的性能但有RAII安全保证。8.9 智能指针与UObject的关系重要UE的智能指针用于非UObject的原生C对象。对象类型生命周期管理工具UObject及其子类GC UPROPERTY第6-7章非UObject的C对象TSharedPtr / TUniquePtr两者的桥接TStrongObjectPtr第9章// ✗ 错误不要用TSharedPtr管理UObjectTSharedPtrUMyObjectObj;// GC和SharedPtr会冲突// ✓ TSharedPtr用于非UObjectTSharedPtrFMyNativeClassNativeObjMakeSharedFMyNativeClass();8.10 实用模式8.10.1 工厂模式返回TSharedRefclassFWidgetFactory{public:staticTSharedRefSWidgetCreateButton(constFTextLabel){returnSNew(SButton).Text(Label);// SNew返回TSharedRef——保证非空}};8.10.2 TSharedFromThis让对象能从内部获取自己的TSharedPtr类似std::enable_shared_from_thisclassFMyClass:publicTSharedFromThisFMyClass{public:voidRegisterCallback(){// 获取自己的TSharedRefTSharedRefFMyClassSelfAsShared();SomeDelegate.BindSP(Self,FMyClass::OnCallback);}};8.11 小结TSharedPtr16字节共享所有权引用计数管理。ESPMode控制线程安全性。优选MakeShared合并分配。TSharedRef编译期非空保证与TSharedPtr相同内存开销。API设计中优先使用。TWeakPtr观察不拥有通过Pin()临时获取强引用。用于打破循环引用。TUniquePtr8字节独占所有权零开销同裸指针大小。性能关键路径首选。UE的智能指针仅用于非UObject对象——UObject由GC管理。

更多文章