11 - 智能指针
本文主要介绍了UE5中的智能指针,包括:
- 智能指针概述
- 共享指针
- 共享引用
- 弱指针
- 唯一指针
智能指针概述
虚幻引擎的智能指针库是C++11智能指针的自定义实现,旨在减轻引擎内存分配和追踪的负担。不过需要注意的是,虚幻智能指针不能与UObject
系统同时使用,因为后者使用更适合游戏代码的单独内存追踪系统。
类别
虚幻引擎的智能指针有如下4种:
- 共享指针
TSharedPtr
:拥有其引用的对象,无限防止该对象被删除,并在无共享指针或共享引用引用其时,最终处理其的删除。共享指针可为空白,意味其不引用任何对象。任何非空共享指针都可对其引用的对象生成共享引用。 - 共享引用
TSharedRef
:行为与共享指针类似,即其拥有自身引用的对象。对于空对象而言,其存在不同;共享引用须固定引用非空对象。共享指针无此类限制,因此共享引用可固定转换为共享指针,且该共享指针固定引用有效对象。要确认引用的对象是非空,或者要表明共享对象所有权时,请使用共享引用。 - 弱指针
TWeakPtr
:与共享指针类似,但不拥有其引用的对象,因此不影响其生命周期。此属性中断引用循环,因此十分有用,但也意味弱指针可在无预警的情况下随时变为空。因此,弱指针可生成指向其引用对象的共享指针,确保程序员能对该对象进行安全临时访问。 - 唯一指针
TUniquePtr
:仅会显式拥有其引用的对象。仅有一个唯一指针指向给定资源,因此唯一指针可转移所有权,但无法共享。复制唯一指针的任何尝试都将导致编译错误。唯一指针超出作用域时,其将自动删除其所引用的对象。
助手类和函数
UE智能指针库提供多个助手类和函数:
助手类 | 描述 |
---|---|
TSharedFromThis | 在添加 AsShared() 或 SharedThis() 函数的 TSharedFromThis<classname> 子类中,利用此类函数可获取对象的 TSharedRef 。AsShared() :返回TSharedFromThis<classname> 模板参数的classname 类型共享引用,该类型可能是调用对象的父类。SharedThis() :返回this 的类型共享引用。不能在类构造函数中调用这两个函数,因为共享引用此时未初始化,会导致崩溃。 |
使用该助手类可以实现侵入性访问对象:共享指针是非侵入性的,也就是说对象不知道其是否被智能指针拥有。这通常是可以接受的,但在某些情况下可能要将对象作为共享引用或共享指针进行访问。这需要将该类继承自TShredFromThis<classname>
,并使用它提供的两个函数AsShared()
和SharedThis()
,将对象转换为共享引用。例如下述代码:
TSharedFromThis<> 使用例点击展开/折叠代码
class FRegistryObject;
class FMyBaseClass: public TSharedFromThis<FMyBaseClass>
{
virtual void RegisterAsBaseClass(FRegistryObject* RegistryObject)
{
// 访问对"this"的共享引用。
// 直接继承自< TSharedFromThis >,因此AsShared()和SharedThis(this)会返回相同的类型。
TSharedRef<FMyBaseClass> ThisAsSharedRef = AsShared();
// RegistryObject需要 TSharedRef<FMyBaseClass>,或TSharedPtr<FMyBaseClass>。TSharedRef可被隐式转换为TSharedPtr.
RegistryObject->Register(ThisAsSharedRef);
}
};
class FMyDerivedClass : public FMyBaseClass
{
virtual void Register(FRegistryObject* RegistryObject) override
{
// 并非直接继承自TSharedFromThis<>,因此AsShared()和SharedThis(this)不会返回相同类型。
// 在本例中,AsShared()会返回在TSharedFromThis<> - TSharedRef<FMyBaseClass>中初始指定的类型。
// 在本例中,SharedThis(this)会返回具备"this"类型的TSharedRef - TSharedRef<FMyDerivedClass>。
// SharedThis()函数仅在与 'this'指针相同的范围内可用。
TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this);
// FMyDerivedClass是FMyBaseClass的一种类型,因此RegistryObject将接受TSharedRef<FMyDerivedClass>。
RegistryObject->Register(ThisAsSharedRef);
}
};
class FRegistryObject
{
// 此函数将接受到FMyBaseClass或其子类的TSharedRef或TSharedPtr。
void Register(TSharedRef<FMyBaseClass>);
};
助手函数 | 描述 |
---|---|
MakeShared() 和 MakeShareable() | 在常规C++指针中创建共享指针。MakeShared() 会在单个内存块中分配新的对象实例和引用控制器,但要求该对象提供public 构造函数。MakeShareable() 的效率较低,但即使对象的构造函数为private ,其仍可运行。利用此操作可拥有非自己创建的对象,并在删除对象时支持自定义行为。 |
StaticCastSharedRef() 和StaticCastSharedPtr() | 静态转换工具函数,通常用于向下转换到子类。 |
ConstCastSharedRef() 和ConstCastSharedPtr() | 将 const 智能引用或智能指针分别转换为 mutable 智能引用或智能指针。 |
优缺点
虚幻引擎的智能指针有如下优点:
- 防止内存泄漏:共享引用不存在时,智能指针(弱指针除外)会自动删除对象。
- 弱引用:弱指针会中断引用循环并阻止悬挂指针。
- 线程安全(可选择):虚幻智能指针库包括线程安全代码,可跨线程管理引用计数(原子引用计数)。如无需线程安全,可用其换取更好性能。要想启用线程安全,需要以
TXxxPtr<T, ESPMode::ThreadSafe>
形式声明智能指针。 - 运行时安全:共享引用从不为空,可固定随时取消引用。
- 传达意图:可轻松区分对象所有者和观察者。
- 内存:智能指针在64位下仅为C++指针大小的两倍(加上共享的16字节引用控制器)。唯一指针除外,其与C++指针大小相同。
虚幻引擎的智能指针在性能方面有些许缺陷:
- 创建和复制智能指针比创建和复制原始C++指针需要更多开销。
- 保持引用计数增加基本运算的周期。
- 部分智能指针占用的内存比原始的C++指针更多。
- 引用控制器有两个堆分配。使用
MakeShared()
代替MakeShareable()
可避免二次分配,并可提高性能。
共享指针
共享指针除了拥有智能指针的优点(避免内存泄漏、悬挂指针、野指针),还拥有一些其他特点:
- 共享所有权:通过引用计数实现数据对象的共享;
- 自动失效:可对指向对象安全引用,无需担心悬挂指针等问题;
- 弱引用:通过弱指针避免循环引用问题;
声明和初始化
共享指针可为空,创建它的方法如下:
共享指针 - 创建点击展开/折叠代码
// 创建一个空白的共享指针
TSharedPtr<FMyObjectType> EmptyPointer;
// 为新对象创建一个共享指针
TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType());
// 从共享引用创建一个共享指针
TSharedRef<FMyObjectType> NewReference(new FMyObjectType());
TSharedPtr<FMyObjectType> PointerFromReference = NewReference;
// 创建一个线程安全的共享指针, 推荐通过MakeShared<classType>()创建
TSharedPtr<FMyObjectType, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FMyObjectType, ESPMode::ThreadSafe>(MyArgs);
拷贝和移动
拷贝共享指针时,系统将向它指向的对象添加一个引用计数,该对象将持续存在,直到不再有共享指针(或共享引用)引用它为止:
共享指针 - 拷贝点击展开/折叠代码
// 增加ExistingSharedPointer指向对象的引用数。
TSharedPtr<FMyObjectType> AnotherPointer = ExistingSharedPointer;
可以使用 MoveTemp()
(或 MoveTempIfPossible()
)函数将一个共享指针的内容移动到另一个共享指针,将原共享指针置空:
共享指针 - 移动点击展开/折叠代码
// 将PointerOne的内容移至PointerTwo。在此之后,PointerOne为nullptr。
PointerTwo = MoveTemp(PointerOne);
// 将PointerTwo的内容移至PointerOne。在此之后,PointerTwo为nullptr。
PointerOne = MoveTempIfPossible(PointerTwo);
MoveTemp()
和MoveTempIfPossible()
的区别是,前者包含静态断言,强制其只能在非常量左值(lvalue)上执行。
此外,还能通过Reset()
函数将共享指针重置。
判等
如果两个共享指针指向同一对象,那么它俩就是相等的,可用==
判断。
解引用和访问
可像传统指针方式对共享指针解引用,或通过Get()
函数直接调用和访问指向对象的方法和成员:
共享指针 - 解引用和访问点击展开/折叠代码
// 在解引用前,检查节点是否引用了一个有效对象。
if (Node)
{
// 以下三行代码中的任意一行都能解引用节点,并且对它的对象调用ListChildren:
Node->ListChildren();
Node.Get()->ListChildren();
(*Node).ListChildren();
}
和共享引用转换
共享指针和共享引用之间可以互相转换。共享引用可直接隐式转换为共享指针;而只有指向非空对象的共享指针才能通过ToSharedRef()
函数创建对应共享引用:
共享指针 - 和共享引用互转点击展开/折叠代码
// 共享引用 -> 共享指针
TSharedPtr<FMyObjectType> MySharedPointer = MySharedReference;
// 共享引用 -> 共享指针
if (MySharedPointer.IsValid())
{
MySharedReference = MySharedPointer.ToSharedRef();
}
自定义删除器
共享指针和共享引用支持对它们引用的对象使用自定义删除器。如需运行自定义删除代码,请提供lambda函数,作为创建智能指针时使用的参数:
共享指针 - 自定义删除器点击展开/折叠代码
TSharedRef<FMyObjectType> NewReference(new FMyObjectType(), [](FMyObjectType* Obj){
// ...
});
TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType(), [](FMyObjectType* Obj){
// ...
});
共享引用
共享引用和共享指针类似,除非需要空白或可为空的对象,否则共享引用为优先选项。如需可能空白或可为空的引用,则应使用共享指针。
共享引用也有判等等操作,但没有IsValid()
等方法。
弱指针
弱指针主要用于打破循环引用,它不会阻止指向的对象被销毁。在访问弱指针引用的对象前,应使用 Pin()
函数将其转为共享指针再访问,此操作确保使用该对象时其将继续存在。
唯一指针
唯一指针指向的对象只能被它所引用。使用和共享指针类似,但不是TSharedPtr
而是TUniquePtr
。