0%

UE智能指针源码阅读

UE里的智能指针源码阅读

UE里的智能指针源码阅读

注释:

pZCACM4.png

还有很多,这里直接总结(抄一段):

  • 惯例的智能指针内存分配特性
  • 弱引用解决循环引用
  • 有线程安全的版本
  • 模板特性可支持几乎所有类型
  • 共享引用永远不会为空,永远可以解引用,实现运行时安全

辅助类和函数:

  • MakeShareable() - 将普通指针转化成共享指针
  • MakeShared(...) - 构造时同时分配控制块内存与目标对象内存
  • TSharedFromThis - 从本类中生成一个自己的类,或者获取TSharedRef*
  • StaticCastSharedRef() - 静态向下转换
  • ConstCastSharedPtr() - const引用转换为mutable引用
  • StaticCastSharedPtr() - 动态向下转换
  • ConstCastSharedRef() - const指针转换为mutable指针
  • StaticCastWeakPtr()
  • ConstCastWeakPtr()

几条建议:

  • Reset()释放对象引用并(可能)释放内存
  • 共享指针内部不要调用自己的delete
  • 不光C指针,UE的智能指针和UObject也不兼容,原因一致

和标准库以及其他智能指针的不同点:

  • 称呼不一样,更贴近Unreal语法
  • 不要直接用weak类型的指针做参数,然后使用shared的构造函数凭空构造,最好用pin,理由和上面的一样
  • TSharedFromThis返回的是共享引用

虚幻实现智能指针的理由:

  • std以及其他库的智能指针并不是全平台通用的
  • 与Unreal容器更好的协作
  • 线程安全可选,强制线程安全可能导致性能问题
  • 更多改进

以上,便是虚幻官方对U智能指针的总结,基本上涵盖了所有智能指针特性

SharedPtr:

template< class ObjectType, ESPMode InMode >
class TSharedPtr
{
public:
    static constexpr ESPMode Mode = InMode;
    ...
    //省去一大堆各自特化的构造函数、功能函数
    ...
private:
    ObjectType* Object;// 不能为空
    SharedPointerInternals::FSharedReferencer< Mode > SharedReferenceCount;
}
FSharedReferencer是一个包含一个指针的对象,它指向的是引用控制块,在SharedPointerInternals.h内:

template< ESPMode Mode >
class FSharedReferencer
{

private:
TReferenceControllerBase<Mode>* ReferenceController;
}

所以一个Shared指针对象的大小为16,(在64位的情况下),而控制块的内容是两个计数:

template <ESPMode Mode>
class TReferenceControllerBase
{
public:
    FORCEINLINE explicit TReferenceControllerBase() = default;// 顺手把默认构造函数删了
    RefCountType SharedReferenceCount{1};
    RefCountType WeakReferenceCount{1};
    ...
}

贴上原注释机翻:

// 对这个对象的共享引用数量。当这个计数达到零时,关联的对象

// 将被销毁(即使还有弱引用!),但引用控制器不会被销毁。

//

// 这个计数从1开始,因为我们通过构造TSharedPtr创建引用控制器,

// 而这就是第一个引用。没有必要从0开始然后再递增

// 对此对象的弱引用数量。如果有任何共享引用,这也算作一个

// 弱引用。当这个计数达到零时,引用控制器将被删除。

//

// 这个计数从1开始,因为它表示我们也在初始化

// SharedReferenceCount 的共享引用。

 

所以一个Shared指针的基本结构就很清晰了,本质就是一个指向对象的指针,和一个指向引用控制块的指针,在文件中,又通过Internals将引用控制块包了一层,各自处理逻辑来实现自动化引用技术功能。

SharedPtr做了什么,引用控制块做了什么

先看最底层的引用控制块,由上文的注释可得到以下信息:

控制块初始化的时候,强引用计数就附一 强引用计数也算做一个弱引用计数(但是从代码中看并不会对计数本身的值产生影响) 强引用计数归0时释放指向对象,弱引用计数归0时释放控制块 也就是说,虚幻的shared智能指针支持:哪怕对象本身已经销毁,也允许弱引用的安全存在。

这样做的目的之一,就是保证在对象释放掉之后也能被查询到,比如使用IsValid()安全检测对象是否有效,而且控制块本身肯定要比对象轻量,所以控制块是否销毁也无所谓。

 

看下控制块内的剩下内容:

virtual void DestroyObject() = 0; // 虚函数声明的,允许重写的自定义delete
FORCEINLINE int32 GetSharedReferenceCount() const{} // 获取计数数量,懒得看,里面有个线程安全的分支
FORCEINLINE bool IsUnique() const // 检查此控制块是否只被一个引用
{
    return 1 == GetSharedReferenceCount();
}
FORCEINLINE void AddSharedReference(){} // 增加引用计数,也有线程安全的版本,可以理解为SharedReferenceCount++
FORCEINLINE void ReleaseSharedReference() // 释放(减少)引用计数
{
    //如果为线程安全版本,则在此处多加,但减少计数的代码还是一样的
    checkSlow( SharedReferenceCount > 0 );
    if( --SharedReferenceCount == 0 )
    {
        DestroyObject(); // 如果变0了就释放对象
        ReleaseWeakReference();
    }
}
FORCEINLINE void AddWeakReference(){}; // 增加弱计数,和强计数几乎一样
void ReleaseWeakReference()
{
    if(--WeakReferenceCount == 0)
    {
        delete this;
    }
}

TReferenceControllerBase(const TReferenceControllerBase&) = delete;
TReferenceControllerBase& operator=(const TReferenceControllerBase&) = delete; //不准拷贝

也没啥,基本上都是计数的基本操作,提供api,分别设置强引用弱引用为0干嘛。

再看下SharedPointerInternals的内容,其构造和控制块是一致的,通过构造函数列表的方式,同步控制块和其指针的构造,例:

FORCEINLINE FSharedReferencer()
: ReferenceController( nullptr ){ }

总结下shared指针控制块构造规则:

  • 拷贝构造时,强引用计数+1
  • 移动构造时,原始控制块指针赋空
  • 通过弱指针拷贝构造时,会检查是否已经存在强引用计数,如果有,才会+1
  • 通过弱指针移动构造时,也会检查是否已经存在强引用计数,而且会释放掉原弱指针
  • 拷贝赋值运算符,在赋值的对象不同的情况下,增加原引用计数,减少强指针计数,最后将自己的控制块指针赋值过去
  • 移动赋值运算符,直接将指向挪过去就行

其实和字符串定义如出一辙。从上面的总结其实就可以分析出智能指针对引用计数的管理方式了,以下为盲写的shared指针定义,代码为伪代码,实际情况比这个负责得多。

template<class Type, ESPMode InMode> // InMode为线程安全的设置,先不管,这里写上属于是一个象征意义
class TSharedPtr
{
public:
    inline void explicit TSharedPtr() = default;
    inline void TSharedPtr(TSharedPtr const& ptr){
        if(ptr != nullptr){
            count++;
        }
    };
    inline void TSharedPtr(TSharedPtr&& ptr){
        ptr.ObjectPtr = nullptr;
    }
    inline TSharedPtr& operator=(TSharedPtr const& ptr){
        if(ptr.ObjectPtr != ObjectPtr)
        {
         ptr.count++;
        }
        return *this
    }      
   inline TSharedPtr& operator=(TSharedPtr&& ptr){
        if(ptr.ObjectPtr != ObjectPtr)
        {  
            count--;
            if(ptr != nullptr)
            {
                ptr.count++;
            }
        }
        return *this;
    }   
    virtual ~TSharedPtr(){}
    void Release(){
        count--;
        if(count<=0){
            Destory(ObjectPtr);
        }
    }
private:
    Type* ObjectPtr;
    int count;
}

由于控制块聚焦于计数,所以此伪代码省去了ObjectPtr指针本身的管理,那么指针本身在哪管理呢,那就是TSharedPtr本体

真实的SharedPtr的构造函数重载非常多,这里只举例几个:

FORCEINLINE explicit TSharedPtr( OtherType* InObject )
    : Object( InObject )
    , SharedReferenceCount( SharedPointerInternals::NewDefaultReferenceController< Mode >( InObject ) )
{
    SharedPointerInternals::EnableSharedFromThis( this, InObject, InObject );
}

FORCEINLINE TSharedPtr( OtherType* InObject, DeleterType&& InDeleter )
: Object( InObject )
, SharedReferenceCount( SharedPointerInternals::NewCustomReferenceController< Mode >( InObject, Forward< DeleterType >( InDeleter ) ) )
{
SharedPointerInternals::EnableSharedFromThis( this, InObject, InObject );
}

先关注一个细节:Shared指针只在类名处使用模板ObjectType,但实际构造时却使用了通用模板OtherType,这样做的好处是适配关联类型的指针指针构造,比如ObjectType为父类,通过此基础类型构造子类OtherType的指针,那么是可以的;如果两者不关联呢?请看以下构造函数模板声明:

template <
typename OtherType,
typename DeleterType,
typename = decltype(ImplicitConv<ObjectType*>((OtherType*)nullptr))

第三行意思就是检查OtherType类型是否可以转换成ObjectType类型,如果不通过,则编译期检查失败,那么这个模板就不会实例化,于是编译器就会报错。

其他常规的也不贴了,总之这些更上层的构造都有一个特点:只关注于UE对象本身的联动。

比如上层可以重载各种各样的构造函数来完善指针的各自初始化途径;

再比如上文使用普通指针去初始化一个智能指针,内部其实是调用了EnableSharedFromThis来创建,这个后面再说。

智能指针与普通指针同时指向一个对象的做法是不规范的,因为智能指针自动会析构对象,此时普通指针就会变成悬空指针

WeakPtr

弱指针的定义,大致结构和共享指针是一致的。

template< class ObjectType, ESPMode InMode >
class TWeakPtr
{
    ...
private:
    ObjectType* Object;
    SharedPointerInternals::FWeakReferencer< Mode > WeakReferenceCount;
}
//有一个FWeakReferencer,含有一个TReferenceControllerBase*对象,和共享指针是一致的

template< ESPMode Mode >
class FWeakReferencer
{

private:
/** Pointer to the reference controller for the object a TWeakPtr is referencing /
TReferenceControllerBase<Mode>
ReferenceController;
}

这个控制块,如果这个弱指针和共享指针指向的是同一个对象,那么这个控制块也是同一个控制块,他们共享内部的所有功能。

如上文所示,功能块内有共享指针计数与弱指针计数,而这两个计数的增加删除规则,其实是差不多的,重点在析构的时候有所不同

FORCEINLINE void ReleaseSharedReference() // 释放(减少)引用计数
{
    //如果为线程安全版本,则在此处多加,但减少计数的代码还是一样的
    checkSlow( SharedReferenceCount > 0 );
    if( --SharedReferenceCount == 0 )
    {
        DestroyObject(); // 如果变0了就释放对象
        ReleaseWeakReference();
    }
}

FORCEINLINE void ReleaseWeakReference()
{
checkSlow( WeakReferenceCount> 0 );
if( –WeakReferenceCount== 0 )
{
delete this;
}
}

维持了人设,即控制控制块,在计数为0的时候直接删除this。

 

Pin

[[nodiscard]] FORCEINLINE TSharedPtr< ObjectType, Mode > Pin() const&
{
    return TSharedPtr< ObjectType, Mode >( *this );
}

[[nodiscard]] FORCEINLINE TSharedPtr< ObjectType, Mode > Pin() &&
{
return TSharedPtr< ObjectType, Mode >( MoveTemp( *this ) );
}

pin用于利用一个WeakPtr对象来返回一个SharedPtr对象,如果指向的目的对象没有其他有效SharedPtr指向,则会返回空。

有左值引用和右值引用两个版本,如果是右值版本,则转化后这个弱引用则用不了了。

调用的是这个私有构造函数:

FORCEINLINE explicit TSharedPtr( TWeakPtr< OtherType, Mode > const& InWeakPtr )
    : Object( nullptr )
    , SharedReferenceCount( InWeakPtr.WeakReferenceCount )
{
    if( SharedReferenceCount.IsValid() )
    {
        Object = InWeakPtr.Object;
    }
}

检查引用计数的逻辑,卸载SharedReferenceCount里面,就不深入了。

 

SharedRef

和SharedPtr一样,但是必须有所指向,区别在于SharedPtr内允许空指针构造,而SharedRef不允许。

 

UniquePtr

UniquePtr写在Unique.h文件中。

template <typename T, typename Deleter = TDefaultDelete<T>>
class TUniquePtr : private Deleter
{
    ...
private:
    using PtrType = T*;
    LAYOUT_FIELD(PtrType, Ptr);
    ...
    FORCEINLINE ~TUniquePtr()
    {
        GetDeleter()(Ptr);
    }
    ...
}

LAYOUT_FIELD宏展开后是一个T*类型的变量,以及这个字段相关的反射代码。

和C++保持一致,他将拷贝构造和拷贝运算符直接删除了。

TUniquePtr(const TUniquePtr&) = delete;
TUniquePtr& operator=(const TUniquePtr&) = delete;

SharedFromThis

对标std::enable_shared_from_this

本质是一个类,总所周知想要调用这个,需要将自己的类继承它。

template< class ObjectType, ESPMode Mode >
class TSharedFromThis
{
public:
    [[nodiscard]] TSharedRef< ObjectType, Mode > AsShared()
    {
        TSharedPtr< ObjectType, Mode > SharedThis( WeakThis.Pin() );
        check( SharedThis.Get() == this );
       return MoveTemp( SharedThis ).ToSharedRef();
    }

[[nodiscard]] TWeakPtr< ObjectType, Mode > AsWeak()
{
TWeakPtr< ObjectType, Mode > Result = WeakThis;
check( Result.Pin().Get() == this );
return Result;
}

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 );
}
}
protected:
TSharedFromThis() { }
TSharedFromThis( TSharedFromThis const& ) { }
FORCEINLINE TSharedFromThis& operator=( TSharedFromThis const& )
{
return *this;
}
~TSharedFromThis() { }
private:
mutable TWeakPtr< ObjectType, Mode > WeakThis;
}

构造函数啥也不干,有一个弱指针的对象,此对象在UpdateWeakReferenceInternal内赋值,还有一个返回SharedPtr的版本,懒得抄了,然后就是应用端使用的AsShared和AsWeak,分别返回WeakThis.Pin和WeakThis自己。

UpdateWeakReferenceInternal其实得是private而不是public,但是模板的bug让他得是public(这是5.5版本注释自己写的)。

此函数在EnableSharedFromThis内直接调用。

这个EnableSharedFromThis挺眼熟,也就是之前分析过的SharedPtr(SharedRef)构造中必会出现的一个函数。

 

所以说继承TSharedFromThis的类,在初始化时,TSharedFromThis自身是啥都不干的,但是当有共享指针构造并指向它时,会调用EnableSharedFromThis→UpdateWeakReferenceInternal。而EnableSharedFromThis本身就接收一个TSharedFromThis对象(如果为空就跳出),这里产生逻辑闭环。

结果就是类内的弱指针得到此类对象的赋值,再然后就是用户自己调用AsShared或者AsWeak来获取它了。

 

MakeShared

对标C++的std::make_shared