淘先锋技术网

首页 1 2 3 4 5 6 7

➤ UE4智能指针

UE4基于C++11智能指针自己实现了一套智能指针库,旨在减轻内存分配和追踪的负担。基于标准C++有三类实现:共享指针(TSharedPtr)弱指针(TWeakPtr)唯一指针(TUniquePtr),以及UE4特有的 共享引用(TSharedRef),共享引用与不可为空的共享指针相同。因为UE4的UObject类群使用了更适合游戏代码的单独内存追踪系统,所以UE4的智能指针库不能与UObject系统同时使用

和C++的智能指针一样,智能指针主要是为了解决指针指向的内存释放管理问题,智能指针本质上是对普通指针的封装,是一个具有普通指针行为操作的类,利用C++类的生命周期概念来控制内存,确保内存能够及时释放,智能指针的实现利用了 RAII(Resource Acquisition Is Initialization) 即 “资源获取即初始化” 的思想,将内存的释放交给智能指针的析构函数,当智能指针类的实例化对象生命周期结束时,自动调用其析构函数,同时释放该实例所有权的内存。

资源获取即初始化(RAII):大致是说资源(内存、套接字等)的生命周期通过局部对象管理,局部对象指的是存储在栈内的对象,它的生命周期由操作系统来维护,这样就相当于转移了资源释放的所有权,使其间接受操作系统控制,避免了资源的不正确释放。

与标准C++的智能指针模板不同,UE4的智能指针模板参数除了需要数据类型之外,还有一个枚举类ESPMode,用以控制智能指针是否为线程安全的。通常仅在单线程上访问智能指针的操作才是安全的,如果需要在多线程的情况下操作智能指针,最好用其线程安全的版本,模板参数的枚举类型为: ESPMode::ThreadSafe,由于多了原子引用计数,线程安全版的智能指针比默认版本要稍慢,但其行为与标准C++保持一致

注意事项:

在使用智能指针的时候,应该避免用普通指针变量来初始化智能指针,因为由普通指针申请并拥有的内存是需要程序员维护的,智能指针本身就是为了减少对内存的手动维护,用普通指针来设置智能指针,有点本末倒置。更进一步,对于定义有指针成员的类,都不应该用一个普通指针变量对其进行赋值,若不得已为之,必须将原来的普通指针置nullptr且不可delete,使得只有智能指针指向这块内存

int*p=newint(2333);// 可以通过编译,但不建议用普通指针变量来设置智能指针 
TSharedPtr<int>sp(p);// 若不得已为之,必须在设置后将原来的普通指针置nullptr
p=nullptr;

② 智能指针没有重载加减运算和自加自减运算

TSharedPtr<int>sp=MakeShared<int>(123);// 以下操作都会编译报错:
++sp;--sp;sp+1;sp-1;

因为UE4的UObject类已经自带GC功能,所以智能指针不能指向UObject类群的实例,否则可能会导致内存被重复释放的问题,UE4本身也通过某种方式阻止了这种做法:

// 编译通过,FVector不是派生自UObject 
TSharedPtr<FVector>pVector(newFVector());// 编译报错,AActor继承自Uobject,自带GC功能 
TSharedPtr<AActor>pActor(newActor());

除了 TSharedRef 之外,其它类型的指针都可以可以使用 Reset() 接口来重置指针,原来的内存不一定会立即析构,需要注意的是,重置共享指针,会影响到所有指向它的弱指针

TSharedPtr<int>sp1=MakeShared<int>(666);TSharedPtr<int>sp2=sp1;TWeakPtr<int>wp1=sp1;TWeakPtr<int>wp2=sp1;// 重置弱指针wp1,不影响它指向的共享指针sp1的内存 
wp1.Reset();// 重置共享指针sp2,不影响和它有共同指向的sp1的内存 
sp2.Reset();// 重置共享指针sp1,指向它的弱指针wp2没有指向的内存了,相当于也被重置了 
sp1.Reset();

⑤ 智能指针通过接口 Get() 获取原始指针;

智能指针的线程安全是可选的,通过模板参数控制,参数类型是 ESPMode 枚举类,默认线程不安全使性能更佳:

TSharedPtr<int,ESPMode::ThreadSafe>spSafe=MakeShared<int,ESPMode::ThreadSafe>(666);// 需要注意的是,是否有线程安全的模板参数,在模板化后就是两种类型了,不能彼此赋值 
TSharedPtr<int>spNotSafe=spSafe;// 编译报错,类型不匹配 

在可以使用 TSharedPtr 和 TSharedRef 时,优先选 TSharedRef

UE4智能指针虽然是基于C++智能指针实现的,但也有与C++智能指针的不同之处

① 类型名称和方法名称与UE4的代码体系更加一致

② 对于TWeakPtr,必须使用接口 Pin() 来获取其指向的TSharedPtr,这点和C++中 lock() 类似;

线程安全对于UE4的智能指针来说是一个可选选项,更加灵活;

④ 一些C++智能指针通用的接口并没有实现,如 use_count()、unique() 等;

⑤ UE4智能指针不允许抛出异常

⑥ 有一个不允许为空的类型,TSharedRef,在创建或赋值时如果引用为空对象,则会触发断言,值得一提的是,和C++不同,TSharedRef 作为引用,在初始化后可重新指定到零一对象;

标准C++已经有了智能指针,且UE4的指针还不能用于UObject系统,那为何UE4需要自己实现一套智能指针?原因主要有以下几个方面:

① 保证不同平台或不同编译器的一致性,比如 std::shared_ptr(甚至 tr1::shared_ptr) 不是所有的平台都支持

② 能与UE4其它的容器和类型无缝协作

③ 更好的控制平台特性,如多线程的处理和优化,同时还提供了线程安全功能;

④ 官方希望智能指针的线程安全是一个可选选项,为了使性能更佳

⑤ 提供了针对UE4的改进,比如可分享,分配给nullptr等;

⑥ 在UE4的智能指针实现中不希望有抛出异常

⑦ (可能)会更加容易调试;

⑧ 倾向于在不需要的时候,就不引入第三方库

智能指针分类:

(1)TSharedPtr 共享指针:允许多个该类型的指针指向同一块内存,采用引用计数器的方式,统计所有指向同一块内存的指针变量的数量,当新的指针变量生命并初始化指向同一块内存,拷贝函数拷贝和赋值操作时引用计数器会自增加,当指针变量生命周期结束调用析构时,引用计数器会自减少。引用计数器减少至0时,释放指向的内存。

使用示例:

// 使用普通指针右值初始化(不建议用普通指针左值初始化) 
TSharedPtr<int>sp1(newint(111));// 强烈建议用MakeShared接口 
TSharedPtr<int>sp2=MakeShared<int>(222);TSharedPtr<int>sp3(MakeShared<int>(333));

(2)TWeakPtr 弱指针:主要是为了配合 TSharedPtr 而引入的一种智能指针,TWeakPtr 没有指针的行为,没有重载间接引用操作符(->)和解除引用操作符(*),它可以通过 TSharedPtr 和 TSharedRef 来初始化,只引用,不计数,不拥有内存的所有权,不会对 TSharedPtr 和 TSharedRef 的共享引用计数器产生影响,也不影响其生命周期,但会在控制块的WeakReferenceCount属性中统计弱指针引用数量。

使用示例:

TSharedPtr<int>sp=MakeShared<int>(666);// 多种创建方式 
TWeakPtr<int>wp1=sp;TWeakPtr<int>wp2(sp);TWeakPtr<int>wp3=wp2;// TWeakPtr 也可以指向 TSharedRef 
TSharedRef<int>sr=MakeShared<int>(888);TWeakPtr<int>wp4(sr);// 返回一个TSharedPtr,若其指向内存已被释放,则返回一个空智能指针对象 
// 特别注意,既然返回了一个TSharedPtr,其引用计数自然要加1
TSharedPtr<int>_sp=wp1.Pin();

需要特别注意的是,因为 TWeakPtr 对内存并没有实际所有权,如果指向的共享内存被释放TWeakPtr 也会变成空对象(因为 TSharedRef 必不为空,所以指向 TSharedRef 的 TWeakPtr 在不改变指向的情况下也不会为空):

TSharedPtr<int>sp=MakeShared<int>(666);TWeakPtr<int>wp(sp);sp.Reset();// TWeakPtr 对内存没有所有权,指向内存被释放后,其自身也失效 
boolbValid=wp.IsValid();

和标准C++的 weak_ptr 一样,TWeakPtr 主要也是为了解决 TSharedPtr 环形引用的问题:

{classB;classA{public:A(){UE_LOG(LogTemp,Log,TEXT("Construct class A"));}~A(){UE_LOG(LogTemp,Log,TEXT("Destruct class A"));}TSharedPtr<B>m_spB;};classB{public:B(){UE_LOG(LogTemp,Log,TEXT("Construct class B"));}~B(){UE_LOG(LogTemp,Log,TEXT("Destruct class B"));}TSharedPtr<A>m_spA;};autospA=MakeShared<A>();autospB=MakeShared<B>();// 发生了环形引用,两个对象的指针成员互相指向彼此的实例 
spA->m_spB=spB;spB->m_spA=spA;}// 离开作用域后两个局部对象并没有析构,日志输出:
// LogTemp: Construct class A
// LogTemp: Construct class B

因为A的实例中,有成员指向了B的实例,而B的实例,又有成员指向了A的实例,形成了一个环,导致彼此都不能正确释放(都在等待指向彼此的指针成员指向的对象释放完成),从而造成内存泄漏,此时就需要 TWeakPtr 上场了,只需要将A或B或二者类的定义中的指针成员由 TSharedPtr 改成 TWeakPtr 即可。因为 TWeakPtr 只引用不计数的特性,当拥有 TWeakPtr 成员的对象析构时,不会因为 TWeakPtr 所指向的内存还未释放而导致析构失败:

{classB;classA{public:A(){UE_LOG(LogTemp,Log,TEXT("Construct class A"));}~A(){UE_LOG(LogTemp,Log,TEXT("Destruct class A"));}// 类A,B更改其一即可,这里二者都改成了弱引用 
TWeakPtr<B>m_spB;};classB{public:B(){UE_LOG(LogTemp,Log,TEXT("Construct class B"));}~B(){UE_LOG(LogTemp,Log,TEXT("Destruct class B"));}// 类A,B更改其一即可,这里二者都改成了弱引用 
TWeakPtr<A>m_spA;};autospA=MakeShared<A>();autospB=MakeShared<B>();// 发生了环形引用,但 TWeakPtr 只引用不计数 
spA->m_spB=spB;spB->m_spA=spA;}// 离开作用域析构时,TWeakPtr 不会导致析构失败,日志输出:
// LogTemp: Construct class A
// LogTemp: Construct class B
// LogTemp: Destruct class B
// LogTemp: Destruct class A

(3)TSharedRef 共享引用:UE4中特有的一类强大且不可为空的智能指针,TSharedRef 必须初始化,且必须指向非空对象,否则会触发断言,因为其必定非空的特性,所以它不需要有 Reset() 和 IsValid() 接口。需要注意的是,只有 TSharedPtr 才能利用 ToSharedRef() 接口将共享指针转换为共享引用,和原 TSharedPtr 共享内存,且 TSharedPtr 中的共享引用计数器 SharedReferenceCount 属性值会自增加,此时,如果将 TSharedPtr 重置,和TWeakPtr不同的是 TSharedRef 仍保有内存所有权,不会为空。

使用示例:

// 使用普通指针右值初始化(不建议用普通指针左值初始化) 
TSharedRef<int>sr1(newint(555));// 强烈建议用MakeShared接口 
TSharedRef<int>sr2=MakeShared<int>(666);TSharedRef<int>sr3(MakeShared<int>(777));// 仅 TSharedPtr 可通过 ToSharedRef() 接口转换为 TSharedRef 
// 且原 TSharedPtr 重置不影响 TSharedRef 指向内存 
TSharedPtr<int>sp=MakeShared<int>(666);// sp 和 sr4 共享内存,引用计数自加 
TSharedRef<int>sr4=sp.ToSharedRef();// 重置sp后引用计数减少,但 sr4 仍然非空(这里不同于TWeakPtr) 
sp.Reset();// TSharedRef 在指向空对象时会触发断言:
// 1、未初始化指向一个非空对象,执行时触发断言 
TSharedRef<int>srEmpty1;// 2、指向的是一个空对象,执行时触发断言 
TSharedPtr<int>spEmpty;TSharedRef<int>srEmpty2=spEmpty.ToSharedRef();

(4)TUniquePtr 唯一指针:强调对内存所有权的唯一性,TUniquePtr 在构造时必须显示的调用构造函数(除非是默认构造),并且不能有赋值/拷贝操作,其拷贝/赋值重载被关键字 =delete 标记,只能通过 MoveTemp() 转移内存所有权,类似C++中的std::move(),其指向的内存仅会被唯一的一个 TWeakPtr 所指向。

使用示例:

// TUniquePtr 带参构造函数被关键字 explicit 标记 
TUniquePtr<int>up1(newint(666));// 强烈建议用MakeShared接口 
TUniquePtr<int>up2(MakeUnique<int>(888));TUniquePtr<int>up3=MakeUnique<int>(888);// 拷贝/赋值重载被关键字 =delete 标记 
TUniquePtr<int>up4=up1;// 编译报错,尝试引用已删除的函数 
TUniquePtr<int>up5(up2);// 编译报错,尝试引用已删除的函数 
// 只能通过 MoveTemp() 转移内存所有权, 
// 转移后 up1 指向内存转交给 up6,
// 同时指针变量 up1 被析构 
TUniquePtr<int>up6=MoveTemp(up1);

需要特别注意的是:

虽然不能有赋值/拷贝操作,却能通过函数返回值来赋值,本质上相当于重新构造,重新构造后会析构之前占用的内存

TUniquePtr<int>up=MakeUnique<int>(666);autoGetUniquePtr=[](int_value){returnMakeUnique<int>(_value);};up=GetUniquePtr(888);

可以声明以 TUniquePtr 为元素类型的容器,不过元素赋值还是只能通过移动语义,且不能使用容器的初始化列表来初始化

TUniquePtr<int>up=MakeUnique<int>(666);autoGetUniquePtr=[](int_value){returnMakeUnique<int>(_value);};// 即使通过转移语义或函数返回值,也不能用初始化列表 
TArray<TUniquePtr<int>>upArr={// 编译报错,尝试引用已删除的函数 
MoveTemp(up),GetUniquePtr(888)};// 给数组中 TUniquePtr 元素赋值:
for(unsignedintidx=0;idx<10;++idx){// 1、定义临时 TUniquePtr 变量然后用移动语义 
autotempUp=MakeUnique<int>(idx+1);upArr.Emplace(MoveTemp(tempUp));// 2、 通过函数返回值赋值 
upArr.Emplace(GetUniquePtr(idx+2));}

UE4的智能指针库提供了很多辅助类和函数,目的是为了使用智能指针时更加方便直观。主要有以下几种:

(1)TSharedFromeThis:如果要访问某个类实例的this指针,不建议直接将this指针返回,因为this指针是普通指针,对其进行delete操作是合法的,这会导致很多不好的后果,比如指针被重复释放操作野指针等。既然不能返回this指针,可能会考虑将其包装为智能指针返回,但这样会导致该类的实例被析构两次,因为包装成智能指针返回相当于创建了一个智能指针变量,该变量生命周期受操作系统控制,类的实例也是如此,返回的智能指针变量和类的实例相当于是指向了同一块内存,故而在析构它们时会导致内存被重复释放。TSharedFromThis,和C++中的 std::shared_from_this() 类似,就是用来解决这个问题的,让类公有继承自TSharedFromTthis<ClassName>,使得能过够安全使用类实例的this指针,其内部有一个 TWeakPtr 指针,若要获取类实例的this指针,它提供两类接口 AsShared() SharedThis(),它们会通过 TWeakPtr 返回一个 TSharedRef

需要注意的是:

调用AsShared() 的对象必须是一个智能指针,否则仍然不能保证使用this裸指针或对内存重复释放,在UE4中会触发断言;

② 在类外部调用静态方法 SharedThis() 时,当前操作模块的类也必须公有继承其自身的TSharedFromThis

③ 和C++类似,AsShared() 和 SharedThis() 不能在构造函数内部使用,否则会触发断言;

使用示例:

classMyClass:publicTSharedFromThis<MyClass>{public:TSharedRef<MyClass>SharedMyself(){returnSharedThis(this);}};// 普通指针或对象,使用TSharedFromThis内的方法会触发断言 
TSharedPtr<MyClass>ptr=MakeShared<MyClass>();// 通过接口获取类实例的智能引用,维护的是同一块内存,同一个计数器 
TSharedRef<MyClass>pRef1=ptr->AsShared();TSharedRef<MyClass>pRef2=ptr->SharedMyself();// 在类外部使用该接口,那么操作模块的类也必须继承其自身的TSharedFromThis 
TSharedRef<MyClass>pRef3=SharedThis(ptr.Get());

(2)MakeShared(包括用于TUniquePtr的MakeUnique):类似于C++中的 std::make_shared ,比直接用普通指针创建效率更高,因为智能指针内存包含两部分,除了数据本身的内存之外,还有一个控制块内存,普通指针创建时,会分别申请两次内存,而使用 MakedShared 只需要进行一次内存申请,因而效率更高。

(3)MakeShareable:主要针对将一个普通指针转换为智能指针,而 TSharedPtr 需要在定义时显示调用构造函数才可以将一个普通指针传入(仍然不建议提前定义一个指针变量然后用来初始化指针智能),与之不同的是,MakeShareabel 支持自定义删除对象的行为,将自定义删除处理通过参数传入。

// MakeShareable 自定义删除行为是一个右值引用 
autoDelFunc=[](int*_ptr){// 删除申请的内存,
// TSharedPtr 作为局部变量由操作系统控制生命周期 
delete_ptr;_ptr=nullptr;};TSharedPtr<int>sp;sp=MakeShareable(newint(666),MoveTemp(DelF));

(4)StaticCastSharedRefStaticCastSharedPtr:类似于C++中的 static_cast,但只用于派生类的转换:

// 始终注意UE4智能指针只能用于非UObject类群 
classBaseClass{};classDerivedClass:publicBaseClass{};// 共享指针派生类静态转换 
TSharedPtr<BaseClass>pBase1=MakeShared<DerivedClass>();TSharedPtr<DerivedClass>pDerived1=StaticCastSharedPtr<DerivedClass>(pBase1);// 共享引用派生类静态转换 
TSharedRef<BaseClass>pBase2=MakeShared<DerivedClass>();TSharedRef<DerivedClass>pDerived2=StaticCastSharedRef<DerivedClass>(pBase2);

(5)ConstCastSharedRefConstCastSharedPtr:用于将 const 类型的智能指针转换为非 const 类型,转换后两个指针指向的是同一块内存,不会申请新内存,如果修改转换后的非const类型的值,那么原来的const类型的值也会同步被修改:

TSharedPtr<constint>sp1=MakeShared<constint>(666);// sp2 和 sp1 指向同一一块内存,并不会重新申请新内存 
TSharedPtr<int>sp2=ConstCastSharedPtr<int>(sp1);// 常量指针 sp1 的值也会同步被修改
(*sp2)=888;// 对于指针引用也是类似 
TSharedRef<constint>sr1=MakeShared<constint>(666);TSharedRef<int>sr2=ConstCastSharedRef<int>(sr1);// 常量引用 sr1 的值也会同步被修改 
(*sr2)=888;