UE与STL智能指针

参考链接:C++ STL 四种智能指针
浅析UE5中的智能指针源码(上)
浅析UE5中的智能指针源码(下)

1. STL智能指针

C++ 标准模板库 STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,其中 auto_ptr 是 C++98 提出的,C++11 已将其摒弃,并提出了 unique_ptr 替代 auto_ptr。shared_ptr 和 weak_ptr 则是 C+11 从准标准库 Boost 中引入的两种智能指针,其中shared_ptr作为标准的共享所有权得智能指针最为常用。STL智能指针一般可以有效防止内存泄漏,但不是线程安全的

1.1 unique_ptr

unique_ptr 是 C++11 新增的智能指针,它是一种独占式智能指针,它禁止其他智能指针与其共享同一对象,从而保证代码的安全性,如果出现了共享所有权的情况可能会编译出错。unique_ptr定义在头文件中,无法复制到其他的unique_ptr,无法通过值传递给函数,也无法用于需要副本得STL算法,但是可以通过std::move转移所有权。unique_ptr可以改变指向的对象,也可以动态释放或者转移所有权.其主要的操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建unique_ptr
unique_ptr<T> u1; //创建空的unique_ptr
u1.reset(new T()); //动态绑定对象
unique_ptr<T> u2(new T()); //创建指向类型为T的对象的unique_ptr
unique_ptr<T,D> u(d); //创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器
auto u = make_unique<T>(); //使用辅助函数创建

// 所有权得释放和转移
T* p = u1.release(); //释放u1对资源的所有权,返回指针
unique_ptr<T> u2;
u2 = std::move(u1); // 通过移动语义转移所有权,此时u1为空
u2.reset(u1.release()); //将u1对资源的所有权转移给u2
u1.reset(); //手动释放u1指向的资源
u1=nullptr; //与上一条等价

1.2 shared_ptr

shared_ptr是一个标准的共享智能指针,同样定义在头文件中,通过引用计数的方式来管理资源,当引用计数为0时,自动释放资源,可以自定义释放的规则.shared_ptr的引用计数主要通过专门的控制块实现,其中包含应用计数和weak_ptr使用的weak count.除此之外还有指向资源的指针,自定义的deleter,allocator等.通过make_shared函数创建,以及通过原始指针和unique_ptr创建的shared_ptr会直接创建新的控制块,而通过拷贝或者直接赋值则不会产生新的控制块.

shared_ptr主要的操作如下:

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
// 创建shared_ptr
shared_ptr<T> sp; //创建空的shared_ptr
shared_ptr<T> sp(p); //创建shared_ptr,指向指针p所指向的对象
unique_ptr<T> u(new T());
shared_ptr<T> sp(u); //创建shared_ptr,指向unique_ptr u所指向的对象
shared_ptr<T> sp2(sp); //创建shared_ptr sp2,指向shared_ptr sp所指向的对象,会使引用计数+1
shared_ptr sp = std::make_shared<T>(); //推荐的创建方式,可以避免显式的new操作,避免内存泄漏
shared_ptr sp = std::shared_from_this(); //在类内使用,创建一个this的shared_ptr,需要类T本身public继承enable_shared_from_this<T>

// 所有权的释放和转移
shared_ptr<T> sp1(new T());
shared_ptr<T> sp2(new T());
sp2 = sp1;// sp1引用计数+1,sp2指向的对象被释放,shared_ptr赋值只能赋值给相同类型的shared_ptr对象
sp1.reset(); //手动释放sp1指向的资源
sp1.reset(new T()); //原有的资源被释放

// 使用基类的shared_ptr可以引用派生类
shared_ptr<base> spbase(new derived());
shared_ptr<derived> spderived(new derived());
shared_ptr<base> spbase2(dynamic_pointer_cast<base>(spderived)); //spbase2引用计数+1,spderived引用计数不变

// 不同的shared_ptr不能通过引用同一块资源创建,会导致两个指针的引用计数块不共享
T* p = new T();
shared_ptr<T> sp1(p);
shared_ptr<T> sp2(p); //错误,不能通过引用同一块资源创建

1.3 weak_ptr

shared_ptr的循环引用问题:虽然shared_ptr通过引用计数能够有效共享资源以及防止内存泄漏,但是还是存在循环引用的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
{

int dataA_;
std::share<B> ptrb_;
};

class B
{
int dataB_
std::share<A> ptra_;
};

int main{
std::share_ptr<A> ptra=std::make_share<A>();
std::share_ptr<B> ptrb=std::make_share<B>();

ptra->ptrb_ =ptrb;
ptrb->ptra_ =ptra;
}

在退出main函数时,两个对象的引用计数都不为0,因此两个对象都不会被释放,导致内存泄漏,这就是shared_ptr循环引用的问题.这里类成员变量的shared_ptr如果换成weak_ptr,则不会出现这个问题.

weak_ptr是一种弱引用的智能指针,可以解决shared_ptr的循环引用问题.它可以指向shared_ptr所指向的对象,但是不会增加引用计数,当shared_ptr的引用计数为0时,即使weak_ptr还指向该对象,该对象也会被释放.weak_ptr不具有普通指针的行为,没有重载operator*和operator->,因此只能观测资源的使用情况.weak_ptr同样采用引用计数的方式,

weak_ptr的主要的操作如下:

1
2
3
4
5
6
7
8
weak_ptr<T>w; //创建空的weak_ptr
weak_ptr<T> w(sp); //创建weak_ptr,指向shared_ptr sp所指向的对象
w=p; //将weak_ptr w指向shared_ptr p所指向的对象,p也可以是weak_ptr
w.reset(); //将w置空
w.use_count(); //返回w所指向对象的shared_ptr引用计数
w.expired(); //返回w所指向对象的shared_ptr引用计数是否为0
w.lock(); //返回一个shared_ptr,指向w所指向的对象,如果w所指向的对象已经被释放,则返回空的shared_ptr

2. UE5智能指针

UE引擎在STL智能指针的基础上,重新搞了一套智能指针,其与STL智能指针的对标关系为:TSharedPtr对应shared_ptr,TWeakPtr对应weak_ptr,TUniquePtr对应unique_ptr,TSharedFromThis对应enable_shared_from_this.除此之外,UE5还提供了独有的TSharedRef.在UE中智能指针只能用于C++,不能共享到蓝图中.除了这些指针,针对资源加载的控制,UE5还提供了FSoftObjectPtr,FSoftClassPtr,FSoftClassPath,FSoftObjectPath等引用.针对UObject类别系统,UE提供了FObjectPtr/TObjectPtr,一般在使用需要进行访问追踪的UPROPERTY的成员变量时使用,而函数参数或者局部参数直接使用UObject*裸指针即可.

  • STL的智能指针无法做到全平台可用,因此UE5提供了自己的智能指针;
  • UE的智能指针可以兼容UE提供的容器,同时也可以切换成线程安全模式;
  • UE智能指针拥有更好的性能,占用跟小的空间,同时也更容易调试;
  • UE智能指针目前没有自定义删除函数,也无法支持动态分配的数组,共享的指针不能和uobject一起使用;
  • 应该尽量少将弱指针转化为共享指针或者共享引用的操作,会对性能有较大的影响.

2.1 TSharedRef

共享应用是一类不可为空的智能指针,一般用于Uobject系统之外的数据对象,因此无法重置或者向其指定空对象,或者创建空白引用.在与TSharedPtr之间进行选择时,除非需要空白应用或者指向空对象,否则都应该优先使用TSharedRef.同时,共享指针也无法主动减少引用数,只能通过生命周期终结或者指向其他共享引用减少引用数.其基础使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建共享引用
TSharedRef<FMyObjectType> ref = MakeShared(new T());
TSharedRef< float, Mode > FloatRef( new float( 123.0f ));
TSharedRef<FMyObjectType> ref2 = ref; //引用计数+1
TSharedRef<FMyObjectType> ref3 = MoveTemp(ref); //ref3指向ref所指向的对象,ref置空

//以下两者均不会编译:
TSharedRef<FMyObjectType> UnassignedReference;
TSharedRef<FMyObjectType> NullAssignedReference = nullptr;

// 共享指针和共享引用的互相转换,共享指针转换成共享引用之前需要判断非空
TSharedPtr<FMyObjectType> MySharedPointer = MySharedReference;
TSharedRef<FMyObjectType>MySharedReference = MySharedPointer.ToSharedRef();

const float& MyFloat = *FloatRef; //可以直接通过解引用获取到里面的值
const float& MyFloat2 = FloatRef.Get(); //也可以用Get()
TWeakPtr< float, Mode > WeakFloat = FloatRef;

2.2 TUniquePtr

唯一指针,不能将其赋值给不能为共享引用或者共享指针指向的对象创建唯一指针.TUniquePtr的基本使用如下:

1
2
3
4
5
6
7
8
9
10
11
// 唯一指针的创建
TUniquePtr<MyObject> ObjUniquePtr = MakeUnique<MyObject>();
TUniquePtr<MyObject> ObjUniquePtr2(new MyObject());

ObjUniquePtr.IsValid(); //判断是否为空
ObjUniquePtr.Get(); //获取裸指针
ObjUniquePtr.Reset(); //重置
ObjUniquePtr.Release(); //释放所有权
TUniquePtr<SimpleObject> ObjUniquePtr2(ObjUniquePtr.Release()); //移交所有权
ObjUniquePtr.Reset(new SimpleObject()); //动态绑定新对象
ObjUniquePtr.ExeFun();//解引用

2.3 TWeakPtr&TWeakObjectPtr

弱指针存储对资源的弱引用,同时不会阻止其引用的对象的释放.由于Uobject使用的是GC机制,而共享指针使用的是引用计数,因此对于UObject和其他类只能定义两种弱指针.TWeakptr是一种弱引用的智能指针,只能用于UObject之外的对象.TWeakObjectPtr是TWeakPtr的特化版本,用于UObject对象,弱引用可以在忽略一个对象是否有效的情况下直接使用该对象.常用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建弱指针
TWeakPtr<MyObject> ObjWeakPtr = ObjUniquePtr;
TSharedRef<FMyObjectType> ObjectOwner = MakeShared<FMyObjectType>();
TWeakPtr<FMyObjectType> ObjectObserver(ObjectOwner);// 通过共享指针创建
TWeakPtr<FMyObjectType> ObjectObserver2 = ObjectOwner;// 直接赋值

ObjectObserver.pin();//转化为TSharedPtr
ObjectObserver.IsValid();//判断是否为空
ObjectObserver.Reset();//重置
ObjectObserver = nullptr;//重置,弱指针可以直接置空

// 创建弱对象指针
TWeakObjectPtr<AActor> ActorWeakPtr = nullptr;
TWeakObjectPtr<const AActor> ActorWeakPtr2 = this;// 模板中使用const可以防止修改对象成员

2.4 TSharedPtr

可为空指针的鲁棒共享智能指针,可以用于UObject之外的对象,也可以用于UObject对象,但是不建议这么做,因为UObject对象使用GC机制,而共享指针使用引用计数机制,两者不兼容,会导致内存泄漏.常用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建共享指针
TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType());
TSharedPtr<FMyObjectType, ESPMode::ThreadSafe> NewThreadsafePointer = MakeShared<FMyObjectType, ESPMode::ThreadSafe>(MyArgs);// 线程安全模式的共享指针
TSharedPtr<FMyObjectType> AnotherPointer = ExistingSharedPointer;//直接赋值会增加计数

SharedPointer.IsValid();//判断是否为空
SharedPointer.Reset();//重置
SharedPointer = nullptr;//重置,共享指针可以直接置空
// 拥有权转移
TSharedPtr<FMyObjectType> PointerTwo = MoveTemp(PointerOne);// 转移后PointerOne直接为空

//解引用
SharedPointer->Function();
(*SharedPointer).Function();
SharedPointer.Get()->Function();

2.5 TSharedFromThis

使用this指针构造返回一个共享指针,与STL中的enable_shared_from_this非常相似,这里对其就不做过多的赘述了.

3. 结语

STL的智能指针为C++的内存泄漏问题引入了一个初步的解决方案,而UE中针对游戏开发下的不同使用场景以及能否兼容UObject的GC机制重新增加了不少功能.如果单纯知道这些指针的功能还是远远不够的,还是希望以后能有机会解析一下UE智能指针的源码吧.