UE5 智能指针
笔者版本:5.1.1
UE 中提供了诸多的智能指针,其中包括类似于 C++ 的一些智能指针,以及专用于 U 类的 Object 相关的智能指针,这些智能指针,在平时的开发中会经常遇到,了解其实现,才能更好的去使用,做好资源、内存及对象的管理(当然也防止崩溃==v==),因此考虑研究下其内部实现,总结学习过程于此。
UE 里的智能指针较多,慢慢总结至此。
TSharedPtr
ObjectType* Object;
SharedPointerInternals::FSharedReferencer< Mode > SharedReferenceCount;
存在两个成员变量,Object 为原始指针,WeakReferenceCount 即为管理引用计数的对象,
我们先来看下这个 SharedReferenceCount 对象,其内持有了一 TReferenceControllerBase
因此 TSharedPtr 在 64 位操作系统下应当 占 16 字节(一个指针,一个内部只有一个指针的对象),实测确实如此:
TReferenceControllerBase 中存在真正的两个引用计数
RefCountType SharedReferenceCount{1};
RefCountType WeakReferenceCount{1};
两值默认的计数均为 1,内部提供了引用计数的一些操作方法,也有一些 Weak 相关的方法,后续再探讨
FORCEINLINE void AddSharedReference()
{
if constexpr (Mode == ESPMode::ThreadSafe)
{
// Incrementing a reference count with relaxed ordering is always safe because no other action is taken
// in response to the increment, so there's nothing to order with.
#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
// We do a regular SC increment here because it maps to an _InterlockedIncrement (lock inc).
// The codegen for a relaxed fetch_add is actually much worse under MSVC (lock xadd).
++SharedReferenceCount;
#else
SharedReferenceCount.fetch_add(1, std::memory_order_relaxed);
#endif
}
else
{
++SharedReferenceCount;
}
}
可以通过 AddSharedReference 来增加引用计数,
FORCEINLINE void ReleaseSharedReference()
{
if constexpr (Mode == ESPMode::ThreadSafe)
{
// std::memory_order_acq_rel is used here so that, if we do end up executing the destructor, it's not possible
// for side effects from executing the destructor end up being visible before we've determined that the shared
// reference count is actually zero.
int32 OldSharedCount = SharedReferenceCount.fetch_sub(1, std::memory_order_acq_rel);
checkSlow(OldSharedCount > 0);
if (OldSharedCount == 1)
{
// Last shared reference was released! Destroy the referenced object.
DestroyObject();
// No more shared referencers, so decrement the weak reference count by one. When the weak
// reference count reaches zero, this object will be deleted.
ReleaseWeakReference();
}
}
else
{
checkSlow( SharedReferenceCount > 0 );
if( --SharedReferenceCount == 0 )
{
// Last shared reference was released! Destroy the referenced object.
DestroyObject();
// No more shared referencers, so decrement the weak reference count by one. When the weak
// reference count reaches zero, this object will be deleted.
ReleaseWeakReference();
}
}
}
ReleaseSharedReference 这里可以用于引用计数自减,当不存在引用时,再清理对象并释放 WeakPtr。
引用计数的增加的使用共有两处,均在 FSharedReferencer 类中,
一是拷贝构造:
FORCEINLINE FSharedReferencer( FSharedReferencer const& InSharedReference )
: ReferenceController( InSharedReference.ReferenceController )
{
// If the incoming reference had an object associated with it, then go ahead and increment the
// shared reference count
if( ReferenceController != nullptr )
{
ReferenceController->AddSharedReference();
}
}
二是赋值:
inline FSharedReferencer& operator=( FSharedReferencer const& InSharedReference )
{
// Make sure we're not be reassigned to ourself!
auto NewReferenceController = InSharedReference.ReferenceController;
if( NewReferenceController != ReferenceController )
{
// First, add a shared reference to the new object
if( NewReferenceController != nullptr )
{
NewReferenceController->AddSharedReference();
}
// Release shared reference to the old object
if( ReferenceController != nullptr )
{
ReferenceController->ReleaseSharedReference();
}
// Assume ownership of the assigned reference counter
ReferenceController = NewReferenceController;
}
return *this;
}
通过这两处在我们构建 TSharedPtr 初始化成员变量 SharedReferenceCount 时即可控制引用计数自增。
引用计数的自减则存在三处,
一是析构时:
FORCEINLINE ~FSharedReferencer()
{
if( ReferenceController != nullptr )
{
// Tell the reference counter object that we're no longer referencing the object with
// this shared pointer
ReferenceController->ReleaseSharedReference();
}
}
这里的 ReleaseSharedReference 会先自减,在引用计数归零时触发 Release 操作,销毁 Object 并释放 WeakReference ,因此如此命名。
第二种计数自减是在赋值时:
inline FSharedReferencer& operator=( FSharedReferencer const& InSharedReference )
{
// Make sure we're not be reassigned to ourself!
auto NewReferenceController = InSharedReference.ReferenceController;
if( NewReferenceController != ReferenceController )
{
// First, add a shared reference to the new object
if( NewReferenceController != nullptr )
{
NewReferenceController->AddSharedReference();
}
// Release shared reference to the old object
if( ReferenceController != nullptr )
{
ReferenceController->ReleaseSharedReference();
}
// Assume ownership of the assigned reference counter
ReferenceController = NewReferenceController;
}
return *this;
}
这里处理了一种情况,当被赋值指针指向的对象不为空,且未指向待指向对象时,应当自减计数。
当左右一致时,这里不会进行任何操作,保证了计数的正确性。
第三种计数自减是在移动赋值时:
inline FSharedReferencer& operator=( FSharedReferencer&& InSharedReference )
{
// Make sure we're not be reassigned to ourself!
auto NewReferenceController = InSharedReference.ReferenceController;
auto OldReferenceController = ReferenceController;
if( NewReferenceController != OldReferenceController )
{
// Assume ownership of the assigned reference counter
InSharedReference.ReferenceController = nullptr;
ReferenceController = NewReferenceController;
// Release shared reference to the old object
if( OldReferenceController != nullptr )
{
OldReferenceController->ReleaseSharedReference();
}
}
return *this;
}
同赋值,当被赋值指针指向的对象不为空,且未指向待指向对象时,应当自减计数,因为是移动赋值,所以入参的计数是不变的,这里不需要 Add。
通过这几处 FSharedReferencer 的逻辑,外部的 TSharedPtr 功能的实现就较为统一了,在合适的时机,维护好此成员变量即可,析构时自然减一,赋值时自然加一等等,重载较多,不再一一剖析,这里抽两个简单看一下:
// 大多都是如此逻辑,赋值即可,构造时自然 new 即可
FORCEINLINE TSharedPtr( TSharedPtr const& InSharedPtr )
: Object( InSharedPtr.Object )
, SharedReferenceCount( InSharedPtr.SharedReferenceCount )
{
}
// 移动赋值时也一样,因为 FSharedReferencer 提供了移动赋值相关的逻辑,这里将入参的右值引用转换为右值直接赋值即可
FORCEINLINE TSharedPtr( TSharedPtr&& InSharedPtr )
: Object( InSharedPtr.Object )
, SharedReferenceCount( MoveTemp(InSharedPtr.SharedReferenceCount) )
{
InSharedPtr.Object = nullptr;
}
// 逻辑大多与此一致,此处不再一一剖析
UE 的 TSharedPtr 默认是非线程安全的,若要安全指定 TSharedPtr 的第二个模板参数类型 ESPMode Mode 为 ThreadSafe 即可,会以原子操作管理引用计数:
FORCEINLINE void ReleaseSharedReference()
{
if constexpr (Mode == ESPMode::ThreadSafe)
{
// std::memory_order_acq_rel is used here so that, if we do end up executing the destructor, it's not possible
// for side effects from executing the destructor end up being visible before we've determined that the shared
// reference count is actually zero.
int32 OldSharedCount = SharedReferenceCount.fetch_sub(1, std::memory_order_acq_rel);
checkSlow(OldSharedCount > 0);
if (OldSharedCount == 1)
{
// Last shared reference was released! Destroy the referenced object.
DestroyObject();
// No more shared referencers, so decrement the weak reference count by one. When the weak
// reference count reaches zero, this object will be deleted.
ReleaseWeakReference();
}
}
...
}
return const_cast<_Atomic_integral_facade*>(this)->_Base::fetch_add(_Operand);
因此其实这个线程安全与 shared_ptr 差不多,引用计数是安全无锁的,但 Object 指向的对象不是。
todo Deleter(后续补充这里,用于自动调用 Deleter 完成需要主动释放的一些操作)
TSharedRef
TSharedRef 和 TSharedPtr 基本一致,只是 TSharedRef 在初始化的时候不能为空,与 C++ 引用和指针的区别类似。逻辑同样也是用如下两个成员变量来完成的,这里不再赘述。
ObjectType* Object;
SharedPointerInternals::FSharedReferencer< Mode > SharedReferenceCount;
TWeakPtr
ObjectType* Object;
SharedPointerInternals::FWeakReferencer< Mode > WeakReferenceCount;
与 TSharedPtr 差不多,也是存在两个成员变量,Object 为原始指针,WeakReferenceCount 即为管理引用计数的对象,这里对象的类型变成了 FWeakReferencer,因此我们来看下这个类的功能。
结果与 TSharedPtr 相同,同样是存储了一个 TReferenceControllerBase
上文中提到,TReferenceControllerBase 中存储了两个计数:
RefCountType SharedReferenceCount{1};
RefCountType WeakReferenceCount{1};
这里我们再来看下这个 WeakReferenceCount 相关的操作:
FORCEINLINE void AddWeakReference()
{
if constexpr (Mode == ESPMode::ThreadSafe)
{
// See AddSharedReference for the same reasons that std::memory_order_relaxed is used in this function.
#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
// We do a regular SC increment here because it maps to an _InterlockedIncrement (lock inc).
// The codegen for a relaxed fetch_add is actually much worse under MSVC (lock xadd).
++WeakReferenceCount;
#else
WeakReferenceCount.fetch_add(1, std::memory_order_relaxed);
#endif
}
else
{
++WeakReferenceCount;
}
}
增加引用计数。
void ReleaseWeakReference()
{
if constexpr (Mode == ESPMode::ThreadSafe)
{
// See ReleaseSharedReference for the same reasons that std::memory_order_acq_rel is used in this function.
int32 OldWeakCount = WeakReferenceCount.fetch_sub(1, std::memory_order_acq_rel);
checkSlow(OldWeakCount > 0);
if (OldWeakCount == 1)
{
// Disable this if running clang's static analyzer. Passing shared pointers
// and references to functions it cannot reason about, produces false
// positives about use-after-free in the TSharedPtr/TSharedRef destructors.
#if !defined(__clang_analyzer__)
// No more references to this reference count. Destroy it!
delete this;
#endif
}
}
else
{
checkSlow( WeakReferenceCount > 0 );
if( --WeakReferenceCount == 0 )
{
// No more references to this reference count. Destroy it!
#if !defined(__clang_analyzer__)
delete this;
#endif
}
}
}
此处提供了这两个方法来维护计数,WeakPtr 不需要负责对象指针维护,管理好计数即可,这里计数为 0 时为 delete this 指针,这里 delete 的实际上是 FWeakReferencer 中的 TReferenceControllerBase
Weak 引用计数增加的调用与 Share 一致,也是存在两种:
第一种是构造时,存在两个重载,使用 FWeakReferencer 的构造:
FORCEINLINE FWeakReferencer( FWeakReferencer const& InWeakRefCountPointer )
: ReferenceController( InWeakRefCountPointer.ReferenceController )
{
// If the weak referencer has a valid controller, then go ahead and add a weak reference to it!
if( ReferenceController != nullptr )
{
ReferenceController->AddWeakReference();
}
}
使用 FSharedReferencer 的构造:
FORCEINLINE FWeakReferencer( FSharedReferencer< Mode > const& InSharedRefCountPointer )
: ReferenceController( InSharedRefCountPointer.ReferenceController )
{
// If the shared referencer had a valid controller, then go ahead and add a weak reference to it!
if( ReferenceController != nullptr )
{
ReferenceController->AddWeakReference();
}
}
第二种是赋值时,通过 AssignReferenceController 来实现
FORCEINLINE FWeakReferencer& operator=( FWeakReferencer const& InWeakReference )
{
AssignReferenceController( InWeakReference.ReferenceController );
return *this;
}
FORCEINLINE FWeakReferencer& operator=( FSharedReferencer< Mode > const& InSharedReference )
{
AssignReferenceController( InSharedReference.ReferenceController );
return *this;
}
inline void AssignReferenceController( TReferenceControllerBase<Mode>* NewReferenceController )
{
// Only proceed if the new reference counter is different than our current
if( NewReferenceController != ReferenceController )
{
// First, add a weak reference to the new object
if( NewReferenceController != nullptr )
{
NewReferenceController->AddWeakReference();
}
// Release weak reference to the old object
if( ReferenceController != nullptr )
{
ReferenceController->ReleaseWeakReference();
}
// Assume ownership of the assigned reference counter
ReferenceController = NewReferenceController;
}
}
这里重载了以 FSharedReferencer 为入参的方法,与 TWeakPtr 入参 TSharedPtr 的重载提供了 TSharedPtr 通过赋值运算符转换为 SharedPtr 的能力。
FORCEINLINE TWeakPtr& operator=( TSharedPtr< OtherType, Mode > const& InSharedPtr )
{
Object = InSharedPtr.Object;
WeakReferenceCount = InSharedPtr.SharedReferenceCount;
return *this;
}
Weak 引用计数减少的调用则不太相同,除了析构、赋值、移动赋值外,在 FSharedReferencer 的构造时也会触发。
析构时:
FORCEINLINE ~FWeakReferencer()
{
if( ReferenceController != nullptr )
{
// Tell the reference counter object that we're no longer referencing the object with
// this weak pointer
ReferenceController->ReleaseWeakReference();
}
}
赋值时:
FORCEINLINE FWeakReferencer& operator=( FWeakReferencer const& InWeakReference )
{
AssignReferenceController( InWeakReference.ReferenceController );
return *this;
}
FORCEINLINE FWeakReferencer& operator=( FSharedReferencer< Mode > const& InSharedReference )
{
AssignReferenceController( InSharedReference.ReferenceController );
return *this;
}
移动赋值时:
FORCEINLINE FWeakReferencer& operator=( FWeakReferencer&& InWeakReference )
{
auto OldReferenceController = ReferenceController;
ReferenceController = InWeakReference.ReferenceController;
InWeakReference.ReferenceController = nullptr;
if( OldReferenceController != nullptr )
{
OldReferenceController->ReleaseWeakReference();
}
return *this;
}
这里将原成员变量赋给了一个临时变量,原值若合法,则释放引用计数减一。
上面 TSharedPtr 过滤了二者相同时的情况,不进行处理,这里则没有额外逻辑,看起来也会触发 ReleaseWeakReference,但其实外部做了处理,包括 TSharedPtr 也是,当相同时直接不会进入相关赋值语句:
FORCEINLINE TWeakPtr& operator=( TWeakPtr&& InWeakPtr )
{
if (this != &InWeakPtr)
{
Object = InWeakPtr.Object;
InWeakPtr.Object = nullptr;
WeakReferenceCount = MoveTemp(InWeakPtr.WeakReferenceCount);
}
return *this;
}
这里其实搞得还挺奇怪的,TSharedPtr 里的 FTSharedReferencer 还多做了一次容错,FWeakReferencer 则没有做,可能有一些其他场景,笔者没有考虑到吧。
通过这几处 FSharedReferencer 的逻辑,外部的 TWeakPtr 功能的实现就较为统一了,在合适的时机,维护好此成员变量即可,析构时自然减一,赋值时自然加一等等,重载较多,不再一一剖析,这里还是抽两个简单看一下:
// 直接赋值即可
FORCEINLINE TWeakPtr( TWeakPtr const& InWeakPtr )
: Object( InWeakPtr.Object )
, WeakReferenceCount( InWeakPtr.WeakReferenceCount )
{
}
// 移动赋值时也一样,因为 FWeakReferencer 提供了移动赋值相关的逻辑,这里将入参的右值引用转换为右值直接赋值即可
FORCEINLINE TWeakPtr( TWeakPtr&& InWeakPtr )
: Object( InWeakPtr.Object )
, WeakReferenceCount( MoveTemp(InWeakPtr.WeakReferenceCount) )
{
InWeakPtr.Object = nullptr;
}
TSharedFromThis
TSharedFromThis 与 STL std::enable_shared_from_this 对应,主要为了解决在一个类内获取自己 shared_ptr 的问题,这里写个例子简单看下:
先定义一个自定义结构,这里我们随便加两个成员变量,赋两个固定初值,以值来判断是否已经释放
struct my_struct
{
my_struct() : Number(654321), Time(0.1234567)
{
FMyTestThread* SimpleRunnable;
SimpleRunnable = new FMyTestThread(static_cast<TSharedPtr<my_struct>>(this));
}
int Number;
float Time;
};
在构造时,起一个异步线程,这里肯定是要使用 TSharedPtr 的,这里可以理解场景中有一个监控变量的需求,或是设备池之类的功能,需要在设备构造的时候起异步线程,执行一些监控、管理之类的操作:
class FMyTestThread : public FRunnable
{
public:
FMyTestThread(TSharedPtr<my_struct> InData) : TestData(InData)
{
Thread = FRunnableThread::Create(this, TEXT("MyTestThread"));
};
~FMyTestThread()
{
if (Thread)
{
delete Thread;
Thread = nullptr;
}
};
virtual bool Init() override { return true; };
virtual uint32 Run() override
{
while (true)
{
if (TestData.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("TestData Value is : %d , %f"), TestData->Number, TestData->Time);
}
else
{
UE_LOG(LogTemp, Error, TEXT("TestData is invalid"));
return 0;
}
FPlatformProcess::Sleep(0.1);
}
};
virtual void Stop()override { };
virtual void Exit() override { };
private:
FRunnableThread* Thread;
TSharedPtr<my_struct> TestData;
};
异步线程中这里只打印一些日志。
这样的逻辑完成后,在创建我们定义的对象时,便会将自动起异步线程执行一些额外操作(这里是打印日志)。
然后我们创建一个我们类的对象,同时创建一个指向其的共享指针。
这里还定义了一个 Button 控件,并且在按下时,会触发这里共享指针的 Reset 操作,也就是将指针浮空并计数减一。
// .h
TSharedPtr<my_struct> ButtonTestData;
// .cpp
Window->SetContent(SNew(SButton).OnClicked_Raw(this, &FDirectoryTreeViewEditorModule::OnButtonClicked));
ButtonTestData = static_cast<TSharedPtr<my_struct>>(new my_struct);
FReply FDirectoryTreeViewEditorModule::OnButtonClicked()
{
this->ButtonTestData.Reset();
UE_LOG(LogTemp, Error, TEXT("Button clicked and the ButtonTestData reset."));
return FReply::Handled();
}
那么,这里指针计数减一后,异步线程里的指针指向的数据还正常吗?
答案是否定的,截图中打印的日志很明显,Button 按下后,数据已经释放了。
其实不难理解,上文中我们已经分析了 TSharedPtr,其内维护的 FWeakReferencer 类型的变量内部的类型 TReferenceControllerBase* 的指针,只有通过同一 Ptr 构造出来的其他指针才能够指向同一个 Controller 对象,这里我们两个 Ptr 均是使用 this 指针构造出来的,因此二者其实各自维护了一个引用计数,而非同一个,因此我们 Reset Button 这里的指针后,引用计数归零,因而也就导致了数据的释放,因此异步线程里的访问也就是非法的了。
TSharedFromThis 便是为了解决这一问题,本文本不该梳理用法,但笔者之前一直未明白 enable_shared_from_this 的使用场景,工作的这一年里,用了一些多线程,看了一些系统源码也明白了一些,因此在这里总结一下。
接下来回归正题,看下 TSharedFromThis 是如何完成的其功能。
mutable TWeakPtr< ObjectType, Mode > WeakThis;
只有一个 TWeakPtr 类型的成员变量,这里看一下赋值的时机(链路方法均有多个重载,取一个 TSharedRef 示例):
template< class SharedRefType, class OtherType >
FORCEINLINE void UpdateWeakReferenceInternal( TSharedRef< SharedRefType, Mode > const* InSharedRef, OtherType* InObject ) const
{
if( !WeakThis.IsValid() )
{
WeakThis = TSharedRef< ObjectType, Mode >( *InSharedRef, InObject );
}
}
存在两处,只是入参是共享引用还是指针的区别,再看下 UpdateWeakReferenceInternal 具体的时机:
template< class SharedRefType, class ObjectType, class OtherType, ESPMode Mode >
FORCEINLINE void EnableSharedFromThis( TSharedRef< SharedRefType, Mode > const* InSharedRef, ObjectType const* InObject, TSharedFromThis< OtherType, Mode > const* InShareable )
{
if( InShareable != nullptr )
{
InShareable->UpdateWeakReferenceInternal( InSharedRef, const_cast< ObjectType* >( InObject ) );
}
}
FORCEINLINE void EnableSharedFromThis( ... ) { }
在名为 EnableSharedFromThis 的方法中调用,而此方法会在 TSharedRef 的几处构造时调用。
这里提供了几个重载,最后提供了一个可变参数模板,注意这里的方法段为空。
template <
typename OtherType,
typename DeleterType,
typename = decltype(ImplicitConv<ObjectType*>((OtherType*)nullptr))
>
FORCEINLINE TSharedRef( SharedPointerInternals::TRawPtrProxyWithDeleter< OtherType, DeleterType >&& InRawPtrProxy )
: Object( InRawPtrProxy.Object )
, SharedReferenceCount( SharedPointerInternals::NewCustomReferenceController< Mode >( InRawPtrProxy.Object, MoveTemp( InRawPtrProxy.Deleter ) ) )
{
UE_TSHAREDPTR_STATIC_ASSERT_VALID_MODE(ObjectType, Mode)
// If the following assert goes off, it means a TSharedRef was initialized from a nullptr object pointer.
// Shared references must never be nullptr, so either pass a valid object or consider using TSharedPtr instead.
check( InRawPtrProxy.Object != nullptr );
// If the object happens to be derived from TSharedFromThis, the following method
// will prime the object with a weak pointer to itself.
SharedPointerInternals::EnableSharedFromThis( this, InRawPtrProxy.Object, InRawPtrProxy.Object );
}
这里的 InRawPtrProxy 暂时忽略,在调用时,入参这传入了两个 Object,只有继承了 TSharedFromThis 指针的 Object,作为入参的调用才能匹配到提供了功能的 EnableSharedFromThis 模板方法,因而构建 TSharedFromThis 的 WeakPtr,未继承者则匹配到最后的可变参数模板,执行空函数。
接下来再看一下如何执行到这些构造的。
先看下 MakeShareable:
template< class ObjectType >
[[nodiscard]] FORCEINLINE SharedPointerInternals::TRawPtrProxy< ObjectType > MakeShareable( ObjectType* InObject )
{
return SharedPointerInternals::TRawPtrProxy< ObjectType >( InObject );
}
这里使用裸指针构建了一个临时对象 TRawPtrProxy 并返回,其实这里的 Proxy 本质上就是一个抽象的指针结构,其包含了上文提到的 Object 和 Controller,也就是说可以提供共享指针或引用的能力,在实现智能指针的时候,重载右值为 TRawPtrProxy 的赋值便统一了外部的调用,而不必关心左值是什么在接收。
此时我们再看下刚刚构造方法中忽略的 InRawPtrProxy,不就是此处 MakeShareable 的返回值吗?因为正常如此 TSharedPtr
再看下 MakeShared:
template <typename InObjectType, ESPMode InMode = ESPMode::ThreadSafe, typename... InArgTypes>
[[nodiscard]] FORCEINLINE TSharedRef<InObjectType, InMode> MakeShared(InArgTypes&&... Args)
{
SharedPointerInternals::TIntrusiveReferenceController<InObjectType, InMode>* Controller = SharedPointerInternals::NewIntrusiveReferenceController<InMode, InObjectType>(Forward<InArgTypes>(Args)...);
return UE::Core::Private::MakeSharedRef<InObjectType, InMode>(Controller->GetObjectPtr(), (SharedPointerInternals::TReferenceControllerBase<InMode>*)Controller);
}
注释中提到与 std::make_shared 一致,分配 ObjectType 和 controller 到一块内存上,连续内存分配,应该有较好的效率。这里对入参的右值通过 Forward 完美转发到了 NewIntrusiveReferenceController 方法,
template <typename... ArgTypes>
explicit TIntrusiveReferenceController(ArgTypes&&... Args)
{
new ((void*)&ObjectStorage) ObjectType(Forward<ArgTypes>(Args)...);
}
(这个地方的内存分配搞不太懂,大致看起来是通过不定参数模板,在编译器计算出了固定的内存大小就地构造 = = ,再研究研究)
引用一下知乎 quabqi 这一块的解释:
再看第二个函数MakeShared,他接收的参数是一堆可变的参数,看注释也说了,等价于std::make_shared,直接在一块内存上构造智能指针和对象本身,好处是对内存就非常友好,减少了一个内存碎片。可以想象一下,如果直接使用TSharedPtr(new T())的形式构造智能指针,其中new T()会先分配一次内存,然后TSharedPtr内部构造ReferenceController又分配了一次内存,这样两块内存不是连续的,耗时也会更高一些,在大量使用智能指针时,性能肯定就不那么好了。这里内部实现就是前面提到的NewIntrusiveReferenceController,这种特殊的构造方式。可以看到内部其实就是直接在Controller自己的内存上,通过placement_new来构造出实际对象,ObjectStorage大小和外部对象一样,但通过模板抹去了对象本身类型,在编译期就计算出大小的一个变量,而整个智能指针的大小就是Controller基类+ObjectStorage的大小,一次分配就完成了构造,这是一个很出色的设计。因此在实践中一定要优先使用这个函数。
然后使用 TSharedRef 提供的友元函数 MakeSharedRef 转调了 TSharedRef 的这个构造:
FORCEINLINE explicit TSharedRef(ObjectType* InObject, SharedPointerInternals::TReferenceControllerBase<Mode>* InSharedReferenceCount)
: Object(InObject)
, SharedReferenceCount(InSharedReferenceCount)
{
UE_TSHAREDPTR_STATIC_ASSERT_VALID_MODE(ObjectType, Mode)
Init(InObject);
}
template<class OtherType>
void Init(OtherType* InObject)
{
check(InObject != nullptr);
SharedPointerInternals::EnableSharedFromThis(this, InObject, InObject);
}
这里的 Init 方法中,调用了 EnableSharedFromThis 完成 TSharedFromThis Weak 指针的初始化方法。
至此,TSharedFromThis 指针的功能就完成了,其提供的 AsShared 相关方法:
[[nodiscard]] TSharedRef< ObjectType, Mode > AsShared()
{
TSharedPtr< ObjectType, Mode > SharedThis( WeakThis.Pin() );
check( SharedThis.Get() == this );
return MoveTemp( SharedThis ).ToSharedRef();
}
通过 WeakThis 指针完成转换即可,这里为了效率返回的是一个右值引用,因为各个构造及赋值均重载了右值引用的方法,所以外部用起来没有区别,其他 AsWeak 等大抵相同不再赘述。
TUniquePtr
TUniquePtr 与 std::unique_ptr 相同,实现了独享所有权的语义,即一个 TUniquePtr 对象同一时间只能绑定一个动态分配的对象,且对该对象有唯一的所有权。
UE 这里与 stl 的也类似,针对数组类型提供了特化版本的实现和对应的 deleter,这里我们先看下普通版本的逻辑。(之前写过一篇new 和 delete 搭配使用的文章,读者有兴趣可以读下)
private:
using PtrType = T*;
LAYOUT_FIELD(PtrType, Ptr);
这里先对模板参数 T* 起了一个别名 PtrType,用以指代模板类型指针,然后使用了一个 LAYOUT_FIELD 宏。
这里展开这个 LAYOUT_FIELD 宏看一下:
PtrType Ptr;
__pragma (warning(push))
__pragma (warning(disable: 4995))
__pragma (warning(disable: 4996))
template <>
struct InternalLinkType<874056343 - CounterBase>
{
;
static void Initialize(FTypeLayoutDesc& TypeDesc)
{
InternalLinkType<874056343 - CounterBase + 1>::Initialize(TypeDesc);
alignas(FFieldLayoutDesc) static uint8 FieldBuffer[sizeof(FFieldLayoutDesc)] = {0};
FFieldLayoutDesc& FieldDesc = *(FFieldLayoutDesc*)FieldBuffer;
FieldDesc.Name = L"Ptr";
FieldDesc.UFieldNameLength = Freeze::FindFieldNameLength(FieldDesc.Name);
FieldDesc.Type = &StaticGetTypeLayoutDesc<PtrType>();
FieldDesc.WriteFrozenMemoryImageFunc = TGetFreezeImageFieldHelper<PtrType>::Do();
FieldDesc.Offset = ((::size_t)&reinterpret_cast<char const volatile&>((((DerivedType*)0)->Ptr)));
FieldDesc.NumArray = 1u;
FieldDesc.Flags = EFieldLayoutFlags::MakeFlags();
FieldDesc.BitFieldSize = 0u;
FieldDesc.Next = TypeDesc.Fields;
TypeDesc.Fields = &FieldDesc;
}
};
__pragma (warning(pop));
可以看到,LAYOUT_FIELD(PtrType, Ptr) 展开成了 PtrType Ptr; 和下面的一段代码,下面的代码可以看出是一个 InternalLinkType 的特化,它提供了一个 static 的方法,完成了该字段的类型反射能力。
在 静态方法 Initialize 里,首先不断递归调用上一个 InternalLinkType 特化的 Initialize,形成了链表结构,然后指定内存对齐方式,分配结构 FieldBuffer,再取出字段描述符 FieldDesc,并填入名称、类型、偏移等信息。
这一块没找到太多文章,自己的理解不一定正确,凑活看看,但大体能明白这里是在编译期完成了字段偏移、大小的计算,提供了高效的内存布局访问方案。
对于我们分析 TUniquePtr 而言,暂时也不太需要关心这些,理解为定义了一个指针类型变量 Ptr 即可。
TUniquePtr 最核心的逻辑是删除了拷贝构造和赋值,以此来保证独占性(🤣):
TUniquePtr(const TUniquePtr&) = delete;
TUniquePtr& operator=(const TUniquePtr&) = delete;
除了针对 nullptr 和 原始指针的构造外,提供了移动构造和移动赋值,以提供转移所用权能力:
// 移动构造示例
FORCEINLINE TUniquePtr(TUniquePtr&& Other)
: Deleter(MoveTemp(Other.GetDeleter()))
, Ptr (Other.Ptr)
{
Other.Ptr = nullptr;
}
// 移动赋值示例
FORCEINLINE TUniquePtr& operator=(TUniquePtr&& Other)
{
if (this != &Other)
{
// We delete last, because we don't want odd side effects if the destructor of T relies on the state of this or Other
T* OldPtr = Ptr;
Ptr = Other.Ptr;
Other.Ptr = nullptr;
GetDeleter()(OldPtr);
}
GetDeleter() = MoveTemp(Other.GetDeleter());
return *this;
}
提供指针语义 * 和 -> 操作:
FORCEINLINE T* operator->() const
{
return Ptr;
}
FORCEINLINE T& operator*() const
{
return *Ptr;
}
Reset、Release、Get 等
// 赋值新对象,销毁旧对象,默认 nullptr 即 销毁
FORCEINLINE void Reset(T* InPtr = nullptr)
{
if (Ptr != InPtr)
{
T* OldPtr = Ptr;
Ptr = InPtr;
GetDeleter()(OldPtr);
}
}
// 释放控制权并返回,当前指针置空
FORCEINLINE T* Release()
{
T* Result = Ptr;
Ptr = nullptr;
return Result;
}
// 返回指向所属对象的指针
FORCEINLINE T* Get() const
{
return Ptr;
}
上述方法很多的参数可以指定 Deleter,默认的 Deleter 没有额外的逻辑,只是 delete 掉 Ptr:
template <typename T>
struct TDefaultDelete
{
...
void operator()(T* Ptr) const
{
delete Ptr;
}
};
如果对于对象的销毁需要有一些额外逻辑,执行特定销毁方法时可以指定。
再来看一下数组特化版本的模板,直接一个简单的模板参数指针:
T* Ptr;
默认的 Deleter 改为了 delete [] 释放数组空间。
void operator()(U* Ptr) const
{
delete [] Ptr;
}
没有重载 -> 和 * 运算符,重载了 [] 运算符,因此数组版本可以通过 Get() 方法来获取原始指针或 [] 形式访问。
FORCEINLINE T* Get() const
{
return Ptr;
}
FORCEINLINE T& operator[](SIZE_T Index) const
{
return Ptr[Index];
}
Comments NOTHING