1 ObjectPtr

[!bug] Title

  • 声明 UObject 指针的地方都可以用 TObjectPtr 替换
  • 但是函数返回类型仍然要使用原始指针!

在 UE5 中,新增了对象指针类型 FObjectPtr/TObjectPtr,以提供编辑器下动态解析和访问追踪功能。很多引擎类的 UPROPERTY 的 UObject* 的裸指针也被替换成了 TObjectPtr<UObject>(例如 AActor 的 RootComponent 成员)。
而在非编辑器下,TObjectPtr<UObject > 会退化为 UObject*,从而避免额外的运行时开销。

本文将从源码的角度对 FObjectPtr/TObjectPtr 对象指针的原理做一些简要的分析,并提供使用建议以及示例代码。

动机:为什么要引入 FObjectPtr/TObjectPtr 对象指针类

在 UE4 中,很多引擎类型的成员是 UObject* 裸指针,其中只记录内存地址。由于内存地址在每次运行时都会发生变化,这就给编辑器下的序列化静态保存、延迟加载等带来了不便。此外,裸指针也不能提供访问追踪功能,这会在对 UObject 进行访问 Debug 时带来极大的不便:需要事先封装 Getter/Setter 函数(若在出现 Bug 后再封装则需要涉及代码重构,成本很大),且不能追踪反射等非直接的访问。

由于 UE5 放弃了对 32 位系统的支持,从而只考虑 64 位的指针。而在现有的硬件条件下,寻址空间不可能将指针的 64 位全部用完。因此,这 64 位就可以用来存储额外的信息:这样就可以把 UObject 的静态引用关系编码到这 64 位中,运行时再解析为内存地址。同时,在这种封装中,就可以在访问变量等需要 Debug 的时机加入 Hook 点,这样就可以很方便的通过注册 Hook 函数进行 Debug。

而在另一方面,在不需要 Debug 的情况下(如 Release 版本),这些封装与 Hook 等新引入的特性不应带来额外的性能开销,它们的效用应该与 UObject * 裸指针没有区别。

由此,FObjectPtr 的特性应该包含以下方面:

  1. 信息封装:将 Object 的静态信息封装到 64 位中,以便序列化静态保存、延迟加载等。
  2. 动态解析:将封装的 64 位信息可以在运行时解析为 UObject* 指针。
  3. Debug 追踪:在对 FObjectPtr 进行访问和动态解析时,可以通过 Hook 的方式获取此次访问 / 解析的信息以供 Debug。
  4. 还原:在如 Release 等不需要上述功能的情况下,FObjectPtr 应退化为 UObject* 裸指针,不应增加额外性能开销。
  • 此外,根据当前 UE5.0 release 版的源码,TObjectPtr<>只保留了 “Debug 追踪” 和“还原”功能的对象指针,而不再直接提供 “信息封装” 和“动态解析”功能,原因见后文信息封装“FObjectPtr 与 TObjectPtr” 部分。

按目前 UE5 的引擎设计,“信息封装”、“动态解析”、“Debug 追踪”功能仅在编辑器下启用,即表示启用信息封装 / 动态解析的宏 UE_WITH_OBJECT_HANDLE_LATE_RESOLVE 以及启用 Debug 追踪的宏 UE_WITH_OBJECT_HANDLE_TRACKING 均被定义为 WITH_EDITORONLY_DATA。因此,后文中用 “编辑器下” 表示启用 FObjectPtr/TObjectPtr 的额外功能,而 “非编辑器下” 表示其被还原为 UObject * 裸指针。

信息封装

FObjectPtr/TObjectPtr 中,可以封装有 UObject 的静态信息(主要是所在 Package 的名字,以及自身的名字 Path 等),从而提供序列化静态保存、延迟加载等方面的功能。
可以看到,TObjectPtr 对象指针 private 继承自 FObjectPtr,没有自己的数据成员;而 FObjectPtr 唯一的成员仅有 FObjectHandle 类型的 Handle,且 FObjectHandle 在编辑器下也仅有一个 UPTRINT 类型的成员 PointerOrRef(UPTRINT 可以看作是 uint64 的一个别名),而在非编辑器下直接定义为 UObject * 裸指针:

正因如此,FObjectPtr/TObjectPtr 在编辑器下不再像 UObject* 裸指针一样,仅仅只持有 UObject 对象的内存地址,而是根据指针值的最低位进行区分:如果最低位不是 1,则其表示真实的内存地址,此时与 UObject* 裸指针没有区别;反之其中保存编码后的 UObject 的静态信息,如所属 Package 的 FName、引用关系(即 UObject 的 Path 等)。而封装这些信息的方式也较为直接,即使用 “全局容器中存储这些信息数据”+“将信息在全局容器中的索引编码到 64 位” 的方式。

UObject 中信息封装到 FObjectPtr/TObjectPtr 的过程

UObject 中信息封装到 FObjectPtr/TObjectPtr 的过程如下图所示:

_UObject → FObjectRef:提取并封装名字等静态信息_*

将 UObject * 中信息封装到 FObjectRef 由 MakeObjectRef () 函数实现,FObjectRef 中包含了 UObject 对象及其所在 Package 的名字信息等:

↑ 这里会使用 Object->GetOutermost ()、Object->GetClass ()->ClassGeneratedBy 等函数提取 UObject 的信息,而其引用关系则保存在 FObjectPathId 类型的 ObjectPath 中。

↑ FObjectPathId 的构造器中会调用 StoreObjectPathId () 函数,其中会向上遍历引用链,并将引用链上每一个对象的 Name 存储到全局数组 GComplexPaths 中,然后将其在 GComplexPaths 中的索引移位后存入 FObjectPathId 唯一的数据成员 PathId。

当然,由于 FObjectRef 所需的信息均是字符串,在已知 UObject 静态信息的情况下也可以手动构建 FObjectRef,示例可见 \ Engine\Source\Runtime\CoreUObject\Private\Tests\ObjectPtrTest. cpp:

FObjectRef → FPackedObjectRef:将名字信息编码到 64 位

可以看到,编码同样是使用 “全局容器存储数据 + index 索引” 的方法,其原理简单来说,就是全局容器 GObjectHandleIndex 中保存各个 Package 的信息,而 Package 信息中保存着 UObject 的信息。这样一来,使用 32 位 PackageID 即可快速找到 UObject 所在 Package 对应的容器,然后使用用 32 位 ObjectID 从 Package 对应容器中找到对象信息。

从源码实现可知,将 FObjectRef 中的名字信息编码到 64 位,并保存到 FPackedObjectRef:: EncodedRef 中是通过 MakePackedObjectRef () 函数实现,而 MakePackedObjectRef () 会调用到 ObjectHandle_Private:: MakeReferenceIds (),将 FObjectRef 中的信息编码为 FPackageId 和 FObjectId。具体数据结构可以从下面的代码与图中看到,FObjectRef 中的信息可以映射为 FObjectHandlePackageData,所有 FObjectHandlePackageData 保存在 GObjectHandleIndex 的数组成员 TArray<FObjectHandlePackageData> PackageData 中。

此外,GObjectHandleIndex 还有另一个 TMap<> 成员 NameToPackageId,可以将 PackageName 映射为 PackageId。调用 ObjectHandle_Private:: MakeReferenceIds () 时会先去 NameToPackageId 中找是否已经添加过,若未添加则根据 FObjectRef 中的中的 Package 信息创建 FObjectHandlePackageData 并添入 GObjectHandleIndex. PackageData 并获得 OutPackageId;之后也是同样的方式,在获得的 FObjectHandlePackageData 的 TMap<> PathToObjectId 中查找是否已经将 UObject 的信息添入(若没有则添入),并获得 OutObjectId。

最后,调用 Pack () 函数将 32 位的 OutPackageId 和 OutObjectId 调用 Pack () 合并放入 FPackedObjectRef:: EncodedRef 成员,即完成了编码过程。

FPackedObjectRef → FObjectHandle → FObjectPtr/TObjectPtr:仅拷贝 64 位值信息

此时,在 FPackedObjectRef 中已经获得了 64 位的信息编码,之后从 FPackedObjectRef 到 FObjectHandle 以及 FObjectPtr/TObjectPtr 仅是用对应的函数拷贝数据即可。这样一来,UObject 中的信息就已经封装在了 FObjectPtr/TObjectPtr 中。

FObjectPtr 与 TObjectPtr

根据源码,TObjectPtr<>对象指针 private 继承自 FObjectPtr,但只允许从 UObject * 和其他 TObjectPtr<>进行赋值 / 初始化,且没有提供 “信息封装” 和“动态解析”功能的相关接口函数(FObjectPtr 向 TObjectPtr<>的转换函数 FObjectPtr:: ToTObjectPtr ()已被标注为已废弃 DEPRECATED)—— 也就是说,正常情况下 TObjectPtr<>在初始化时获得的只可能是 UObject * 指针信息,而不是封装后的名字索引 Handle。因此可以将 TObjectPtr<>视为只保留了 “Debug 追踪” 和“还原”功能的对象指针。

此外,若需要用 FObjectPtr 中已封装的信息初始化 TObjectPtr<>,则应手动完成动态解析 FObjectPtr 后获取 UObject * 指针,然后用 UObject * 裸指针对 TObjectPtr<> 进行初始化。动态解析 FObjectPtr 的过程见下一节。

动态解析

FObjectPtr/TObjectPtr 进行解引用时,会通过运算符重载调用到其 Get()函数。在此过程中,会通过 IsObjectHandleResolved () 函数判断其最低位是否为 1:如果不是则说明其中 Handle 保存的是 UObject * 裸指针,直接返回使用;如果不是则需要执行解析 Resolve 过程,将编码的 64 位信息还原为 UObject * 裸指针。

动态解析基本过程为 “HandleId → 名字 / Path → UObject * 指针”,其调用栈如下所示:

  • 动态解析过程由 ResolvePackedObjectRef () 函数完成,其将 FPackedObjectRef 的 64 位编码信息解析为 UObject * 裸指针并返回,其由 MakeObjectRef () 和 ResolveObjectRef () 两个部分组成。
  • MakeObjectRef () 将 FPackedObjectRef 的 64 位编码解析为 FObjectRef,主要通过内部调用 ObjectHandle_Private:: MakeObjectRef () 函数完成。其过程基本就是上文信息封装中 FObjectRef 编码为 FPackedObjectRef 的逆过程,即通过 ID 从全局容器 GObjectHandleIndex 中获取 Package 和 Object 的名字信息等。
  • ResolveObjectRef () 则是通过 ObjectPath.Resolve () 从 GComplexPaths 获取对象的名字数组,并使用 UObjectHash 引擎对象管理的接口 FindOrLoadPackage () 及 StaticFindObjectFastInternal () 通过名字找到 UObject 及其 Package,然后将获得的 UObject * 裸指针返回。
  • 最后,使用获得的 UObject * 裸指针替换 FObjectPtr/TObjectPtr 的 Handle,这样下次访问时就无需再次解析。

访问追踪 / 动态解析追踪

FObjectPtr/TObjectPtr 在编辑器下提供了 Hook 接口,使得可以很方便地在 FObjectPtr/TObjectPtr 每次访问和动态解析时进行输出日志等 Debug 操作。

访问追踪

访问追踪可以在每次 FObjectPtr/TObjectPtr 进行 Get ()(包含 ->、* 运算符重载,其都会调用 Get () 函数)时时获取 Debug 信息。

可以通过 SetObjectHandleReadCallback () 接口注册 Hook 函数:

在编辑器下调用 FObjectPtr/TObjectPtr 的 Get () 时,会通过 ResolveObjectHandle () 调用 ObjectHandle_Private:: OnHandleRead ()。其中会调用已注册的 Hook 函数,并将访问的 UObject 作为参数透传:

↑ 具体示例可以参考 \ Engine\Source\Editor\UnrealEd\Private\Cooker\PackageBuildDependencyTracker. cpp 中的 FPackageBuildDependencyTracker:: StaticOnObjectHandleRead (UObject* ReadObject) 函数。

动态解析追踪

同样的,在对 Class 和 Object 进行 Resolve 时,也可以通过 SetObjectHandleClassResolvedCallback () 和 SetObjectHandleReferenceResolvedCallback () 注册 Hook 函数,例如:

非编辑器下 FObjectPtr/TObjectPtr 的还原

在非编辑器下,UE_WITH_OBJECT_HANDLE_LATE_RESOLVE 和 UE_WITH_OBJECT_HANDLE_TRACKING 宏会被置 0。此时 FObjectPtr/TObjectPtr 中唯一数据成员 FObjectHandle 被 using 为 UObject,此时其退化为对应类型 UObject 的裸指针,从而避免额外的运行时开销。以下是 Get () 函数的展开写法:

1
2
3
4
5
6
7
8
9
FORCEINLINE UObject* Get () const {
return ResolveObjectHandle (Handle)
{
return ReadObjectHandlePointerNoCheck (Handle)
{
{ return Handle; } //此时 using FObjectHandle = UObject*;
}
}
}

↑ 可以看到,在函数内联编译后,其效用与裸指针无异。

此外,一些其他相关函数也会在非编辑器下由变化,如 IsObjectHandleResolved () 会始终返回 true(理所应当,因为 FObjectPtr/TObjectPtr 只保存裸指针),ResolveObjectHandleNoRead () 也会直接调用 ReadObjectHandlePointerNoCheck () 等。

对反射的影响

对于生成代码和类型注册:生成特殊的 FProperty 子类型

我们在类中声明以下两个 UPROPERTY:

1
2
3
4
5
UPROPERTY (EditAnywhere, BlueprintReadWrite)
UStaticMeshComponent* rawPtrComponent;

UPROPERTY (EditAnywhere, BlueprintReadWrite)
TObjectPtr<UStaticMeshComponent> objPtrComponent;

启动 UHT 后,在其 gen. cpp 中生成的,用于创建其 FProperty 的参数代码:

1
2
3
const UECodeGen_Private:: FObjectPropertyParams Z_Construct_UClass_ATryTObjPtr_Statics:: NewProp_rawPtrComponent = { "rawPtrComponent", nullptr, (EPropertyFlags) 0x001000000008000d, UECodeGen_Private::EPropertyGenFlags:: Object, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET (ATryTObjPtr, rawPtrComponent), Z_Construct_UClass_UStaticMeshComponent_NoRegister, METADATA_PARAMS (Z_Construct_UClass_ATryTObjPtr_Statics:: NewProp_rawPtrComponent_MetaData, UE_ARRAY_COUNT (Z_Construct_UClass_ATryTObjPtr_Statics::NewProp_rawPtrComponent_MetaData)) };

const UECodeGen_Private:: FObjectPtrPropertyParams Z_Construct_UClass_ATryTObjPtr_Statics:: NewProp_objPtrComponent = { "objPtrComponent", nullptr, (EPropertyFlags) 0x001400000008000d, UECodeGen_Private::EPropertyGenFlags:: Object | UECodeGen_Private::EPropertyGenFlags:: ObjectPtr, RF_Public|RF_Transient|RF_MarkAsNative, 1, STRUCT_OFFSET (ATryTObjPtr, objPtrComponent), Z_Construct_UClass_UStaticMeshComponent_NoRegister, METADATA_PARAMS (Z_Construct_UClass_ATryTObjPtr_Statics:: NewProp_objPtrComponent_MetaData, UE_ARRAY_COUNT (Z_Construct_UClass_ATryTObjPtr_Statics::NewProp_objPtrComponent_MetaData)) };

可以看到,除了变量名之外,唯一的区别就是在初始化 FObjectPropertyParams 的第四个成员 EPropertyGenFlags Flags 时,使用或运算添加了 UECodeGen_Private::EPropertyGenFlags:: ObjectPtr 标记。在为这些 UPROPERTY 构建 FProperty 对象时,则会为其生成 FObjectPtrProperty 子类而不是 FObjectProperty(如果是 TObjectPtr<UClass> 则会生成 FClassPtrProperty 而不是 FClassProperty)。FObjectPtrProperty 继承自 FObjectProperty(FClassPtrProperty 也同样继承自 FClassProperty),其中覆写了一些接口函数,如 SerializeItem () 等。

对于 Cast<>() 类型转换:可以直接当作裸指针使用

在 \ Engine\Source\Runtime\CoreUObject\Public\Templates\Casts. h 中可以看到,Cast<>() 类型转换函数为 TObjectPtr<> 创建了单独的版本:

1
2
3
4
5
template <class T, class U> 
FORCEINLINE T* Cast (const TObjectPtr<U>& Src)
{
return TCastImpl<U, T>:: DoCast ((const FObjectPtr&) Src);
}

而在其 DoCast 的实现版本中,则是将 TObjectPtr<> 通过 ResolveObjectHandleNoRead () 解析为 UObject * 裸指针,然后再按照裸指针的逻辑进行 Cast 类型转换,如 ECastType:: UObjectToUObject 版本的类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
FORCEINLINE static To* DoCast ( UObject* Src ) //UObject*裸指针版本 {
return Src && Src->IsA<To>() ? (To*) Src : nullptr;
}

FORCEINLINE static To* DoCast ( const FObjectPtr& Src ) //FObjectPtr&TObjectPtr<>版本 {
UObject* SrcObj = ResolveObjectHandleNoRead (Src.GetHandleRef ());
if (SrcObj && SrcObj->IsA<To>())
{
ObjectHandle_Private:: OnHandleRead (SrcObj);
return (To*) SrcObj;
}
return nullptr;
}

在捕获 Cast<> 返回值时,用裸指针和 TObjectPtr<> 均可,因为 Cast 返回裸指针,而 TObjectPtr<> 可以由 UObject* 隐式转换得到。

对于通过字符串获取 FProperty 并访问成员变量:应使用 TObjectPtr<T>* 获取反射解析后的指针,而不是 T** 二级指针

如果 TObjectPtr<T> 成员对象是 IsResolved 的,则其中信息与 UObject* 裸指针没有区别,此时即使使用 T** 二级指针也不会有问题;但如果是没有 Resolve 的,则会触发断言崩溃。示例代码(完整的示例代码见文章最后):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
FObjectRef ObjRef = MakeObjectRef (objPtrComponent.Get ());
FObjectPtr fObjPtr (ObjRef);
oriObjPtrComponent = fObjPtr.ToTObjectPtr ();

//{ //反射 TObjectPtr(二级指针): 崩溃!!!
// FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("oriObjPtrComponent"));
// UObject** reflectComp = reflectVar->ContainerPtrToValuePtr<UObject*>(this);
// if (reflectComp && *reflectComp)
// {
// UE_LOG (LogTemp, Log, TEXT ("This is objPtrComponent: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
// }
//}

{ //反射 TObjectPtr(TObjectPtr<UStaticMeshComponent>)
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("oriObjPtrComponent"));
TObjectPtr<UObject>* reflectComp = reflectVar->ContainerPtrToValuePtr<TObjectPtr<UObject>>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is TObjectPtr<UStaticMeshComponent>: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

objPtrComponent = Cast<UStaticMeshComponent>(oriObjPtrComponent);

{ //反射 TObjectPtr(二级指针)此时已经 Resolve 过,不会崩溃
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("objPtrComponent"));
UStaticMeshComponent** reflectComp = reflectVar->ContainerPtrToValuePtr<UStaticMeshComponent*>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is objPtrComponent: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

{ //反射 TObjectPtr(TObjectPtr<UStaticMeshComponent>)
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("objPtrComponent"));
TObjectPtr<UStaticMeshComponent>* reflectComp = reflectVar->ContainerPtrToValuePtr<TObjectPtr<UStaticMeshComponent>>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is TObjectPtr<UStaticMeshComponent>: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

对于 TMap<> 等容器类及委托函数的影响

如官方文档 https://docs.unrealengine.com/5.0/en-US/unreal-engine-5-migration-guide/ 所说,类似于 TMap<int32,TObjectPtr<UStaticMeshComponent>> 类型的容器类使用 Find () 函数时,catch 返回值的局部变量应与容器声明中的类型一致,即 TObjectPtr<UStaticMeshComponent>* 而非 UStaticMeshComponent**,否则编译报错:

1
2
3
4
5
6
//UStaticMeshComponent** foundComp = ObjPtrMap.Find (1); //无法通过编译
TObjectPtr<UStaticMeshComponent>* foundComp = ObjPtrMap.Find (1);
if (foundComp && *foundComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is ObjPtrMap.Find (1): %s"), *(*foundComp)->GetPathName ());
}

对于原先声明参数为裸指针类型的委托,则需要像官方文档示例那样修改参数类型,或者重新写一个包装函数并透传参数(来自官方文档):

1
2
3
4
5
6
7
8
9
10
11
12
// Original function signature, using raw pointers, which we will use in most cases:
static bool MyFunction (UObject* FirstParameter);

// In rare cases where implicit conversion is not available, use this pass-through function.
// Pass-through function signature, using TObjectPtr:
static bool MyFunction (TObjectPtr<UObject> FirstParameter);

// Pass-through function body (in the source file):
bool UMyClass:: MyFunction (TObjectPtr<UObject> FirstParameter)
{
return ShouldShowResetToDefault (FirstParameter.Get ());
}

使用建议

  • 对于需要进行访问追踪的 UPROPERTY 成员变量,可以使用 TObjectPtr<T> 替换裸指针;
  • 对于函数参数、局部指针变量等,则建议使用 UObject* 裸指针。
  • 在进行容器 Find 时、反射访问变量时,捕获类型应与变量声明的类型保持一致,不宜混用 TObjectPtr<T>* T**
  • 注册的 Hook 函数由于访问频次通常会很高,应保证在大多数情况下的低开销。

附:完整测试示例代码

TryTObjPtr. h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal. h"
#include "GameFramework/Actor. h"
#include "TryTObjPtr. generated. h"

UCLASS ()
class MYFPS_API ATryTObjPtr : public AActor
{
GENERATED_BODY ()

public:
// Sets default values for this actor's properties
ATryTObjPtr ();

UPROPERTY (EditAnywhere, BlueprintReadWrite)
UStaticMeshComponent* rawPtrComponent;

UPROPERTY (EditAnywhere, BlueprintReadWrite)
TObjectPtr<UStaticMeshComponent> objPtrComponent;

UPROPERTY (EditAnywhere, BlueprintReadWrite)
TMap<int32,TObjectPtr<UStaticMeshComponent>> ObjPtrMap;

UPROPERTY (EditAnywhere, BlueprintReadWrite)
TObjectPtr<UObject> oriObjPtrComponent;

protected:
// Called when the game starts or when spawned
virtual void BeginPlay () override;

public:
// Called every frame
virtual void Tick (float DeltaTime) override;

UFUNCTION (BlueprintCallable)
void TryReflectionPtr ();
};

TryTObjPtr. cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Fill out your copyright notice in the Description page of Project Settings.

#include "TryTObjPtr. h"
#include "Kismet/GameplayStatics. h"

ATryTObjPtr:: ATryTObjPtr ()
{
PrimaryActorTick. bCanEverTick = true;
}

void ATryTObjPtr:: BeginPlay ()
{
Super:: BeginPlay ();
UGameplayStatics:: GetPlayerController (this, 0)->EnableInput (nullptr); //启用键盘事件,方便调用入口函数
}

void ATryTObjPtr:: Tick (float DeltaTime)
{
Super:: Tick (DeltaTime);

if (UGameplayStatics:: GetPlayerController (this, 0)->WasInputKeyJustPressed (EKeys::C)) //按下键盘 C 键时调用测试代码入口函数
{
TryReflectionPtr ();
}
}

void ATryTObjPtr:: TryReflectionPtr ()
{
{ //反射原始指针
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("rawPtrComponent"));
UStaticMeshComponent** reflectComp = reflectVar->ContainerPtrToValuePtr<UStaticMeshComponent*>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is TryReflectionPtr: %s"), *(*reflectComp)->GetPathName ());
}
}

//测试 FObjectRef 动态解析功能
FObjectRef ObjRef = MakeObjectRef (objPtrComponent.Get ());
FObjectPtr fObjPtr (ObjRef);
oriObjPtrComponent = fObjPtr.ToTObjectPtr ();

//{ //反射 TObjectPtr(二级指针): 崩溃!!!
// FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("oriObjPtrComponent"));
// UObject** reflectComp = reflectVar->ContainerPtrToValuePtr<UObject*>(this);
// if (reflectComp && *reflectComp)
// {
// UE_LOG (LogTemp, Log, TEXT ("This is objPtrComponent: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
// }
//}

{ //反射 TObjectPtr(TObjectPtr<UStaticMeshComponent>)
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("oriObjPtrComponent"));
TObjectPtr<UObject>* reflectComp = reflectVar->ContainerPtrToValuePtr<TObjectPtr<UObject>>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is TObjectPtr<UStaticMeshComponent>: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

objPtrComponent = Cast<UStaticMeshComponent>(oriObjPtrComponent);

{ //反射 TObjectPtr(二级指针)此时已经 Resolve 过,不会崩溃
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("objPtrComponent"));
UStaticMeshComponent** reflectComp = reflectVar->ContainerPtrToValuePtr<UStaticMeshComponent*>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is objPtrComponent: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

{ //反射 TObjectPtr(TObjectPtr<UStaticMeshComponent>)
FProperty* reflectVar = this->GetClass ()->FindPropertyByName (TEXT ("objPtrComponent"));
TObjectPtr<UStaticMeshComponent>* reflectComp = reflectVar->ContainerPtrToValuePtr<TObjectPtr<UStaticMeshComponent>>(this);
if (reflectComp && *reflectComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is TObjectPtr<UStaticMeshComponent>: %s %p %p"), *(*reflectComp)->GetPathName (), reflectComp, *reflectComp);
}
}

{
//UStaticMeshComponent** foundComp = ObjPtrMap.Find (1); //无法通过编译
TObjectPtr<UStaticMeshComponent>* foundComp = ObjPtrMap.Find (1);
if (foundComp && *foundComp)
{
UE_LOG (LogTemp, Log, TEXT ("This is ObjPtrMap.Find (1): %s"), *(*foundComp)->GetPathName ());
}
}

{
UE_LOG (LogTemp, Log, TEXT ("This is objPtrComponent.GetPath (): %s"), *objPtrComponent.GetPath ());
}
}

2 智能指针

概述

UE 智能指针不能用于 UObject 及其派生类。常用于 Uobject 系统外的数据对象(比如自己创建的类)
引擎具有 UObject 管理的单独内存管理系统(对象处理文档),两个系统未互相重叠。

智能指针类型

智能指针可影响其包含或引用对象的寿命。不同智能指针对对象有不同的限制和影响。下表可用于协助决定各类型智能指针的适用情况:

共享指针:最常用的指针, 用来存储。

共享引用

  • 共享引用永远不会为 null,且总是可以进行解引用。
  • 共享指针的性能消耗最小。所有操作所占时间都是固定的。

弱指针

  1. 弱指针允许您安全地检查一个对象是否已经被销毁。
  2. 使用弱引用来断开引用循环。
    缺点: 最慢

智能指针优点

为什么原则使用 UE 智能指针:

  1. C++原生智能指针不是在所有平台上都可用。UE 智能指针可以跨平台。
  2. 可以和其他虚客器及类型无缝地协作。
  3. 更好地控制平台特性、包括线程处理和优化。
  4. 我们想提供线程安全的功能以获得好的性能。
  5. 我们想在性能方面有更多的控制权 (内联函数、内存、虚函数的应用等)。
  6. 在不需要的时候倾向于不引入新的第三方依赖。
优点 描述
防止内存泄漏 共享引用不存在时,智能指针(弱指针除外)会自动删除对象。
弱引用 弱指针会中断引用循环并阻止悬挂指针。
可选择的线程安全 虚幻智能指针库包括线程安全代码,可跨线程管理引用计数。如无需线程安全,可用其换取更好性能。
运行时安全 共享引用从不为空,可固定随时取消引用。
授予意图 可轻松区分对象所有者和观察者。
内存 智能指针在 64 位下仅为 C++指针大小的两倍(加上共享的 16 字节引用控制器)。唯一指针除外,其与 C++指针大小相同。

在性能上的优势

  1. 所有运算均为常量时间。
  2. 共享指针解引用的速度和 C+指针一样快。
  3. 复制共享指针永远不会分配内存。
  4. 线程安全的版本是无锁的。
  5. 和 STL 相比,其实现更快。

劣势

  1. 创建和复制智能指针比创建和复制原始 C++指针需要更多开销
  2. 保持引用计数增加基本运算的周期
  3. 共享指针使用的内存比 C++指针多
  4. 引用控制器有两个堆分配。使用 MakeShared 代替 MakeShareable 可避免二次分配,并可提高性能。
  5. 由多个共享指针引用的每个独立对象都有性能消耗。
  6. 弱指针访问速度比共享指针访问速度略慢。

内存使用情况(在 32 位操作系统)

  1. 所有的共享指针 (TSharedPtr,TSharedRef,TWeakPtr)都占 8 个字节当针对 32 一位系统编译时)
  2. C 指针 (无符号 32 位整型)
  3. 引用控制器指针无符号 32 位整型)
  4. TSharedFromThis 也占 8 个字节, 因为它内嵌了弱指针/

引用控制器(当针对 32-位系统编译时)

  1. 引用控制器对象占 12 个字节
  2. C+指针无符号 32 位整型)
  3. 共享引用计数 (无符号 32 位整型)
  4. 弱引]用计数 (无符号 32 位整型)
    注意: 无论有多少个共享指针/弱指针引用一个对象,都仅为每个对象创建一个引用控制器。.

TSharedPtr

[!NOTE] 共享指针(强指针)
共享指针(Shared Pointers) 是指既健壮、又能为空指针的智能指针。共享指针沿袭了普通智能指针的所有优点,它能避免出现内存泄漏、悬挂指针,还能避免指针指向未初始化的内存。

还有一些其他特点

  • 共享所有权(Shared Ownership): 引用计数支持多个共享指针,以确保它们引用的数据对象永远不被删除,前提是它们中的任意一个仍指向数据对象。
  • 自动失效(Automatic Invalidation): 你可安全引用易变对象,无需担心出现悬挂指针。
  • 弱引用: 弱指针可中断引用循环。
  • 意向指示(Indication of Intent): 区分拥有者(参见共享引用)和观察者,并提供不可为空的引用。

共享指针有一些值得注意的基本特性,包括:

  • 语法非常健壮
  • 非侵入性(但能反射)
  • 线程安全(视情况而定)
  • 性能佳,占用内存少

[!NOTE] 非侵入性
共享指针是非侵入性的,即对象不知道其是否为智能指针拥有

共享指针类似于共享引用,主要区别在于共享指针可以指向空对象,共享引用不可为空
除非需要空对象或可为空的对象,否则建议你优先选择共享引用

1 声明和初始化

共享指针可为空,所以无论有无数据对象,都可以对它们进行初始化。

  • **MakeShared<T>() / MakeShareable() :创建共享指针
  • MakeShared:在单个内存块中分配新的对象实例和引用控制器,但要求对象提供公共构造函数。
  • MakeShareable:效率较低,但即使对象的构造函数是私有的也可以工作,使您能够获得不是您创建的对象的所有权,并在删除对象时支持自定义行为。
  • 注意只能用于动态分配内存!new
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 创建空白的共享指针
    TSharedPtr<FMyObjectType> EmptyPointer;

    // 为新对象创建共享指针
    TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType());
    TSharedPtr<FMyObjectType> NewPointer = MakeShareable(new FMyObjectType());

    // 从共享引用创建共享指针
    TSharedRef<FMyObjectType> NewReference(new FMyObjectType());
    TSharedPtr<FMyObjectType> PointerFromReference = NewReference;

    // 创建线程安全的共享指针
    TSharedPtr<FMyObjectType, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FMyObjectType, ESPMode::ThreadSafe>(MyArgs);

2 复制/转移

复制共享指针时,系统将向它引用的对象添加一个引用。

1
2
// 增加对象ExistingSharedPointer引用的引用数。
TSharedPtr<FMyObjectType> AnotherPointer = ExistingSharedPointer;
  • 使用 MoveTemp(或 MoveTempIfPossible)函数将一个共享指针的内容转移到另一个共享指针,将原始的共享指针保留为空:(对应 C++的 std::move)
1
2
3
4
// 将PointerOne的内容移至PointerTwo。在此之后,PointerOne将引用nullptr。
PointerTwo = MoveTemp(PointerOne);
// 将PointerTwo的内容移至PointerOne。在此之后,PointerTwo将引用nullptr。
PointerOne = MoveTempIfPossible(PointerTwo);

MoveTemp 和 MoveTempIfPossible 的唯一不同之处在于 MoveTemp 包含静态断言,强制其只能在非常量左值(lvalue)上执行。

3 重置

**使用 Reset 函数、或分配一个空指针来重置共享指针

1
2
3
PointerOne.Reset();
PointerTwo = nullptr;
// PointerOne和PointerTwo现在都引用nullptr。

5 比较 / 有效性

== !=:共享指针是否相等
相等被定义为两个共享指针引用同一对象。

1
2
3
4
5
TSharedPtr<FTreeNode> NodeA, NodeB;
if (NodeA == NodeB)
{
// ...
}
  • **IsValid 函数(是否为有效的)和 bool 运算符有助于判断共享指针是否引用了有效对象。
  •  Get返回对象的原生 C++指针,若为空返回 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (Node.IsValid())
{
// ...
}
if (Node)
{
// ...
}
if (Node.Get() != nullptr)
{
// ...
}

//更严格的检查
if (Node.IsValid() && Node.Get())
{
//...
}

6 解引用和访问

你可以像使用普通 C++指针那样解引用,调用方法和访问成员。你也可以像使用其他 C++指针那样,通过调用 IsValid 函数或使用重载的 bool 运算符,在取消引用之前执行空检查。

1
2
3
4
5
6
7
8
// 在解引用前,检查节点是否引用了一个有效对象。
if (Node)
{
// 以下三行代码中的任意一行都能解引用节点,并且对它的对象调用ListChildren:
Node->ListChildren();
Node.Get()->ListChildren(); //推荐
(*Node).ListChildren();
}

TSharedRef

共享引用(强引用)

  • 不可为空,初始化必须有数据对象
  • 无法重置 Reset 共享引用、向其指定空对象,或创建空白引用。因此固定包含有效对象,甚至没有 IsValid 方法
  • **与标准的 C++引用不同,可在创建后将共享引用重新指定到另一对象。

在共享引用和共享指针之间进行选择时,除非需要空对象或可为空的对象,否则建议你优先选择共享引用。

避免将数据作为 TSharedRef 或 TSharedPtr 参数传到函数,此操作将因取消引用和引用计数而产生开销。相反,建议将引用对象作为 const & 进行传递。

声明和初始化

  • **MakeShared<T>() / MakeShareable() :创建共享引用
1
2
3
//创建共享引用
TSharedRef<FMyObjectType> NewReference(new FMyObjectType());
TSharedRef<FMyObjectType> NewReference = MakeShared<FMyObjectType>();

在无有效对象的情况下尝试创建的共享引用将不会编译,并尝试将共享引用初始化为空指针变量:

1
2
3
4
5
//以下两者均不会编译:
TSharedRef<FMyObjectType> UnassignedReference;
TSharedRef<FMyObjectType> NullAssignedReference = nullptr;
//以下会编译,但如NullObject实际为空则断言。
TSharedRef<FMyObjectType> NullAssignedReference = NullObject;

比较

== !=:共享引用是否相等
相等表示引用相同对象。

1
2
3
4
5
TSharedRef<FMyObjectType> ReferenceA, ReferenceB;
if (ReferenceA == ReferenceB)
{
// ...
}

TWeakPtr

  • 弱指针存储对象的弱引用,不参与引用计数。
  • 在访问弱指针引用的对象前,应使用 Pin 函数生成共享指针。此操作确保使用该对象时其将继续存在。
  • 如只需要确定弱指针是否引用对象,可将其与 nullptr 比较,或在之上调用 IsValid

声明、初始化

**可创建空白弱指针,或通过共享指针和共享引用创建

1
2
3
4
//创建共享引用。
TSharedRef<FMyObjectType> ObjectOwner = MakeShared<FMyObjectType>();
//创建指向对象的弱指针。
TWeakPtr<FMyObjectType> ObjectObserver(ObjectOwner);

复制

与共享指针相同,弱指针是否引用有效对象,均可进行安全复制:

1
TWeakPtr<FMyObjectType> AnotherObjectObserver = ObjectObserver;

重置

Reset ()nullptr

1
2
3
4
//可通过将弱指针设为nullptr进行重置。
ObjectObserver = nullptr;
//也可使用重置函数。
AnotherObjectObserver.Reset();

有效性

  • **IsValid 函数(是否为有效的)和 bool 运算符有助于判断共享指针是否引用了有效对象。
  •  **Get**:返回对象指针,若为空返回 null

转换为共享指针

Pin 函数将创建指向弱指针对象的共享指针。

共享指针(包括由 Pin 函数返回的指针)可在条件句中作为 bool 类型进行求值,其中 true 表示有效对象。

以下代码检查弱指针是否引用有效对象。如是,至少在共享指针(由 Pin 函数创建)超出范围或被显式清除前,将保证其持续有效。

1
2
3
4
5
6
7
//获取弱指针中的共享指针,并检查其是否引用有效对象。
if (TSharedPtr<FMyObjectType> LockedObserver = ObjectObserver.Pin())
{
//共享指针仅在此范围内有效。
//该对象已被验证为存在,而共享指针阻止其被删除。
LockedObserver->SomeFunction();
}

访问对象

  1. 首先使用 Pin 函数,将其转换为共享指针。
  2. 然后通过共享指针或弱指针上的 Get 函数进行访问。此方法可确保使用该对象时,其将持续有效。

打破引用循环

两个或多个对象使用智能指针保持彼此间的强引用时,将出现引用循环。在此类情况下,对象间会相互保护以免被删除。各对象固定被另一对象引用,因此对象无法在另一对象存在时被删除。如外部对象未对引用循环中对象进行引用,其实际上将出现内存泄漏。

弱指针不会保留自身引用的对象,因此其可中断此类引用循环。要在未拥有对象时对其进行引用,并延长其寿命时,可使用弱指针。

使用警告

在以下情况中请谨慎使用弱指针:

  • **在 Set 或 Map 中用作键。弱指针可能会在未通知容器的情况下随时无效,因此共享指针或共享引用更适用于充当键。可安全地将弱指针用作数值。
  • 虽然弱指针提供 IsValid 函数,但是检查 IsValid 无法保证对象在任何时间长度内均可持续有效。线程安全共享指针可能会因另一线程上的活动而随时无效,因此使用线程安全共享指针应尤其注意。Pin 返回的共享指针将使对象在代码将其清除或其超出范围前保持活跃状态,因此 Pin 函数是用于检查的首选方法,此类检查会导致取消引用或访问存储对象。

TUniquePtr

  • 唯一指针仅会显式拥有其引用的对象。
  • 唯一指针可转移所有权,但无法共享。复制唯一指针的任何尝试都将导致编译错误。
  • 唯一指针超出范围时,其将自动删除其所引用的对象。
  • 不要为共享指针或共享引用引用的对象创建唯一指针

创建 / 初始化 / 判断 / 解引用 / 重置

  • MakeUnique () 创建唯一指针
  • IsValid ()
  • -> 运算符
  • Get() 函数
  • Release() 释放并移交所有权
  • Reset()nullptr 重置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建唯一指针
TUniquePtr<SimpleObject> ObjUniquePtr = MakeUnique<SimpleObject>();

// 判断有效性
if (ObjUniquePtr.IsValid())
{
ObjUniquePtr->ExeFun(); // 解引用
}

// 释放指针,移交
TUniquePtr<SimpleObject> ObjUniquePtr2(ObjUniquePtr.Release());

// 重置
ObjUniquePtr.Reset();
ObjUniquePtr2 = nullptr;

智能指针类型转换

引用转指针

共享引用隐式转换为共享指针

1
TSharedPtr<FMyObjectType> MySharedPointer = MySharedReference;

指针转引用

ToSharedRef从共享指针创建共享引用

  • 要求共享指针引用了一个非空对象
  • 从空共享指针创建共享引用将触发断言。
1
2
3
4
5
// 在解引用之前,请确保共享指针有效,以避免可能出现的断言。
if (MySharedPointer.IsValid())
{
MySharedReference = MySharedPointer.ToSharedRef();
}

弱指针转强指针

Pin 函数将创建指向弱指针对象的共享指针。

1
2
//ObjectObserver为弱指针
TSharedPtr<FMyObjectType> LockedObserver = ObjectObserver.Pin()

子类转父类

隐式转换

1
2
3
4
5
6
//SimpleObject是ComplexObject的父类
TSharedPtr<SimpleObject> simpleObj;
TSharedPtr<ComplexObject> complexObj = MakeShared<ComplexObject>();

// 派生类转基类
simpleObj = complexObj;

父类转子类

StaticCastSharedRef 和 StaticCastSharedPtr

1
2
3
//SimpleObject是ComplexObject的父类
// 基类转派生类
TSharedPtr<ComplexObject> complexObj2 = StaticCastSharedPtr<ComplexObject>(simpleObj);

constmutable

[[1 C++ Primer#mutable关键字]]:简单理解就是把 const 转换成非 const

ConstCastSharedRef 和 ConstCastSharedPtr :将 const 智能引用或智能指针分别转换为 mutable 智能引用或智能指针。

1
2
3
4
//创建常量指针
const TSharedPtr<SimpleObject> simpleObj_const(new SimpleObject());
//常量指针转非常量指针
TSharedPtr<SimpleObject> simpleObj_mutable = ConstCastSharedPtr<SimpleObject>(simpleObj_const);

TSharedFromThis

共享指针是非侵入性的,意味对象不知道其是否为智能指针拥有。
有些函数的参数为共享引用或共享指针,我们就需要传进去一个对象,但如何让对象知道自己就是智能指针?
将一个类继承自 TSharedFromThis 后,那么这个类的对象就会知道自己是属于哪一个共享指针。

对标的是原生 C++的 std::enable_shared_from_this。用法也非常相似?? 存疑

TSharedFromThis 意思就使用 this 指针来构造一个共享指针,通过这个共享指针可以安全的使用 this 指针。
其内部有一个弱指针,若要获取类实例的 this 指针,它提供两类接口 AsShared () 和 **SharedThis (),它们会通过 TWeakPtr 返回一个共享引用;

  • 自定义类继承 TSharedFromThis 模板类
  • AsShared() 将 C++原生指针转换为共享引用,如果需要,我们可以再隐式转为共享指针
  • SharedThis(this) 会返回具备 “this” 类型的共享引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BaseClass : public TSharedFromThis<BaseClass>
{
public:
void printf(){};
}

void Func()
{
//创建共享指针访问成员函数
TSharedPtr<BaseClass> sharePtr = MakeShareable(new BaseClass());
sharePtr->printf();

//通过.Get()将共享指针解引用, 我们将可以通过智能指针获得原生C++指针
//原生C++指针ptr指向sharePtr所指的对象
BaseClass* ptr = sharePtr.Get();

//对于普通的类,我们如果想把ptr在转换为共享指针,需要再次调用MakeShareable创建新的共享指针
//⭐对于继承了TSharedFromThis的类,类对象知道自己是共享指针
//因此我们可以直接使用AsShared()将指向BaseClass的C++原生指针ptr转换为共享引用
//然后隐式转换为共享指针
if(ptr)
{
TSharedPtr<BaseClass> sharePtr2 = ptr->AsShared();
}

需要注意的是:
① 调用 AsShared () 的对象必须是一个智能指针,否则仍然不能保证使用 this 裸指针或对内存重复释放,在 UE4 中会触发断言;
② 在**类外部调用静态方法 SharedThis() 时,当前操作模块的类也必须公有继承其自身的 TSharedFromThis**;
AsShared()SharedThis() 不能在构造函数内部使用,共享引用此时并未初始化,将导致崩溃或断言。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass : public TSharedFromThis<MyClass>
{
public:
TSharedRef<MyClass> SharedMyself()
{
return SharedThis(this);
}
};

// 普通指针或对象,使用TSharedFromThis内的方法会触发断言
TSharedPtr<MyClass> ptr = MakeShared<MyClass>();

// 通过接口获取类实例的智能引用,维护的是同一块内存,同一个计数器
TSharedRef<MyClass> pRef1 = ptr->AsShared();
TSharedRef<MyClass> pRef2 = ptr->SharedMyself();

// 在类外部使用该接口,那么操作模块的类也必须继承其自身的TSharedFromThis
TSharedRef<MyClass> pRef3 = SharedThis(ptr.Get());

自定义删除器

共享指针和共享引用支持对它们引用的对象使用自定义删除器。如需运行自定义删除代码,请提供 lambda 函数,作为创建智能指针时使用的参数,就像这样:

1
2
3
4
5
6
7
8
void DestroyMyObjectType(FMyObjectType* ObjectAboutToBeDeleted)
{
// 此处添加删除代码。
}
// 这些函数使用自定义删除器创建指南指针。
TSharedRef<FMyObjectType> NewReference(new FMyObjectType(), [](FMyObjectType* Obj){ DestroyMyObjectType(Obj); });

TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType(), [](FMyObjectType* Obj){ DestroyMyObjectType(Obj); });

线程安全

通常仅在单线程上访问智能指针的操作才是安全的。

如需访问多线程,请使用智能指针类的线程安全版本:

  • TSharedPtr<T, ESPMode::ThreadSafe>
  • TSharedRef<T, ESPMode::ThreadSafe>
  • TWeakPtr<T, ESPMode::ThreadSafe>
  • TSharedFromThis<T, ESPMode::ThreadSafe>

由于原子引用计数,此类线程安全版本比默认版本稍慢,但其行为与常规 C++指针一致:

  • 读取和复制固定为线程安全。
  • 写入和重置须同步后才安全。

如了解多线程永不访问指针,可通过避免使用线程安全版本获得更好性能。