跳到主要内容
信息
文章中可能会出现一些错误,希望大佬们可以在评论区指出错误,感谢支持!

11 - 智能指针

本文主要介绍了UE5中的智能指针,包括:

  • 智能指针概述
  • 共享指针
  • 共享引用
  • 弱指针
  • 唯一指针

智能指针概述

虚幻引擎的智能指针库是C++11智能指针的自定义实现,旨在减轻引擎内存分配和追踪的负担。不过需要注意的是,虚幻智能指针不能与UObject系统同时使用,因为后者使用更适合游戏代码的单独内存追踪系统。

类别

虚幻引擎的智能指针有如下4种:

  1. 共享指针 TSharedPtr:拥有其引用的对象,无限防止该对象被删除,并在无共享指针或共享引用引用其时,最终处理其的删除。共享指针可为空白,意味其不引用任何对象。任何非空共享指针都可对其引用的对象生成共享引用。
  2. 共享引用 TSharedRef:行为与共享指针类似,即其拥有自身引用的对象。对于空对象而言,其存在不同;共享引用须固定引用非空对象。共享指针无此类限制,因此共享引用可固定转换为共享指针,且该共享指针固定引用有效对象。要确认引用的对象是非空,或者要表明共享对象所有权时,请使用共享引用。
  3. 弱指针 TWeakPtr:与共享指针类似,但不拥有其引用的对象,因此不影响其生命周期。此属性中断引用循环,因此十分有用,但也意味弱指针可在无预警的情况下随时变为空。因此,弱指针可生成指向其引用对象的共享指针,确保程序员能对该对象进行安全临时访问。
  4. 唯一指针 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

参考资料