UE5 智能指针剖析

发布于 2023-05-02  139 次阅读


UE5 智能指针

笔者版本:5.1.1

UE 中提供了诸多的智能指针,其中包括类似于 C++ 的一些智能指针,以及专用于 U 类的 Object 相关的智能指针,这些智能指针,在平时的开发中会经常遇到,了解其实现,才能更好的去使用,做好资源、内存及对象的管理(当然也防止崩溃==v==),因此考虑研究下其内部实现,总结学习过程于此。

UE 里的智能指针较多,慢慢总结至此。

TSharedPtr

ObjectType* Object;
SharedPointerInternals::FSharedReferencer< Mode > SharedReferenceCount;

存在两个成员变量,Object 为原始指针,WeakReferenceCount 即为管理引用计数的对象,

我们先来看下这个 SharedReferenceCount 对象,其内持有了一 TReferenceControllerBase* ReferenceController 类型的变量指针,

因此 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* ReferenceController 类型的指针。
上文中提到,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 ObjSharedPtr = MakeShareable(ObjPtr); 此处重载的本质上时移动构造,使用 InRawPtrProxy 提供的指针及 ReferenceController,就地构造。

再看下 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];
    }

参考文章:
知乎 quabqi:UE4的智能指针 TSharedPtr
博客园 UE4 LAYOUT_FIELD 分析

如堕五里雾中
最后更新于 2023-08-21