[UE C++] TArray
概念
TArray
是UE4中最常用的容器类。其速度快、内存消耗小、安全性高。TArray
类型由两大属性定义:元素类型 和 内存分配器。- 元素类型 是存储在数组中的对象类型。
TArray
被称为同质容器。换言之,其所有元素均完全为相同类型。单个TArray
中不能存储不同类型的元素。 - 分配器 常被省略,默认为最常用的分配器。其定义对象在内存中的排列方式;以及数组如何进行扩展,以容纳更多的元素。若默认行为不符合要求,可选取多种不同的分配器,或自行编写。
TArray
为 数值类型。意味其与其他内置类型(如int32
或float
)的处理方式相同。其设计时未考虑扩展问题,因此建议在实际操作中 勿使用new
和delete
创建或销毁TArray
实例。元素也为数值类型,为容器所拥有- TArray 被销毁时其中的元素也将被销毁。若在另一TArray中创建TArray变量,其元素将复制到新变量中,且不会共享状态。
TArray与std::vector类似,为可动态扩充内存容器。内部主要有三个成员变量:
ElementAllocatorType AllocatorInstance;
SizeType ArrayNum;
SizeType ArrayMax;
其中AllocatorInstance为分配器,ArrayNum为元素实际个数,ArrayMax为最大可容纳元素个数。内存扩容方式和std::vector相似,当容量满了之后,会额外分配一个更大的内存,并将整个数组的数据拷到新内存上,之后再释放旧的内存。
1. 创建和初始化
默认的Allocator为对分配器,关于分配器的用法,本文不会涉及。
//IntArray == [0,1]
// 原始指针 + 数量
int32 nums[3]{ 0,1,2 };
TArray<int32> IntArray{ nums ,2 };
// 初始化列表
TArray<int32> IntArray = {0,1};
// 拷贝构造
TArray<int32> IntArray = TArray<int32>{0,1};
// 移动拷贝构造
TArray<int32> tem{0,1};
TArray<int32> IntArray{MoveTemp(tem)};
// 拷贝构造 + 额外空间,主要是为了性能优化
TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3}; // ArrayMax=5, ArrayNum=2
// 移动拷贝构造 + 额外空间,主要是为了性能优化
TArray<int32> tem{0,1};
TArray<int32> IntArray{MoveTemp(tem),3}; // ArrayMax=5, ArrayNum=2
// TArrayView
TArray<int32> tem{ 0,1 };
TArrayView<int32> TestView{ tem };
TArray<int32> InArray{ TestView };
关于TArrayView: 相当于一个拥有生命周期的TArray(类似于std::span — C++20),没有提供直接改变容器元素的API,类似一个视图。元素的生命周期取决于为它进行赋值的容器,使用时请格外注意生命周期。
2. 增
2.1 Init
首先清空TArray,然后向TArray中填充Number个Element
void Init(const ElementType& Element, SizeType Number);
// IntArray == [10,10,10,10,10]
IntArray.Init(10, 5);
2.2 Emplace
使用给定参数构建元素类型的新实例,添加到TArray末尾。返回添加的位置Index
SizeType Emplace(ArgsType&&... Args);
// IntArray == [0,1,5],index == 2
int32 index = IntArray.Emplace(5);
2.3 Add/Push
将元素类型的实例复制(或移动)到数组中,并返回添加的位置Index。内部会调用Emplace
,不可避免的会涉及到临时变量的复制,Emplace
的效率始终高于 Add
,但 Add
的可读性可能更好。
//用于移动语义,需要对象类实现移动构造函数
SizeType Add(ElementType&& Item);
SizeType Add(const ElementType& Item);
IntArray.Add(3);
// IntArray == [0,1,3]
2.4 Append
在TArray末尾添加多个元素(来自另一个TArray、原始数组、初始化列表)
//添加TArray
void Append(const TArray<OtherElementType, OtherAllocator>& Source);
//添加TArray,用于移动语义
void Append(TArray<OtherElementType, OtherAllocator>&& Source);
//添加原始数组
void Append(const ElementType* Ptr, SizeType Count);
//使用初始化列表构造并添加
void Append(std::initializer_list<ElementType> InitList);
IntArray.Append({10,20});
int32* nums = new int32[2]{ 30,40 };
IntArray.Append(nums, 2);
TArray<int32> TemIntArray{ 50,60 };
IntArray.Append(TemIntArray);
// IntArray == [0,1,10,20,30,40,50,60]
2.5 AddUnique
仅在尚不存在等值元素时,AddUnique
才会向容器添加新元素,使用元素运算符==
来判断相等性。若存在需要添加的元素,则会返回它的index,若不存在则会返回新添加元素的index
//用于移动语义
SizeType AddUnique(ElementType&& Item);
SizeType AddUnique(const ElementType& Item);
IntArray.AddUnique(1);
IntArray.AddUnique(3);
// IntArray == [0,1,3]
2.6 Insert
在给定的Index中插入一个或者多个元素(来自另一个TArray、原始数组、初始化列表),返回插入的index
//插入一个元素
SizeType Insert(const ElementType& Item, SizeType Index);
//插入一个元素,用于移动语义
SizeType Insert(ElementType&& Item, SizeType Index);
//插入原生数组
SizeType Insert(const ElementType* Ptr, SizeType Count, SizeType Index);
//插入另一个TArray
SizeType Insert(const TArray<ElementType, OtherAllocator>& Items, const SizeType InIndex);
//插入另一个TArray,用于移动语义
SizeType Insert(TArray<ElementType, OtherAllocator>&& Items, const SizeType InIndex);
//初始化列表构造并插入
SizeType Insert(std::initializer_list<ElementType> InitList, const SizeType InIndex);
IntArray.Insert(2, 1);
IntArray.Insert({ 10,20 },1);
int32* nums = new int32[2]{ 30,40 };
IntArray.Insert(nums, 2,1);
TArray<int32> TemIntArray{ 50,60 };
IntArray.Insert(TemIntArray,1);
// IntArray == [0,50,60,30,40,10,20,2,1]
2.7 SetNum
SetNum
函数可直接设置数组元素的数量。
- 如新数量大于当前数量,则使用元素类型的默认构造函数新建元素。
- 如新数量小于当前数量,
SetNum
将移除元素 - bAllowShrinking 代表是否可以在合适的情况下收缩正在使用的内存,改变ArrayMax
SetNum(SizeType NewNum, bool bAllowShrinking = true);
// IntArray == [0,1,0]
IntArray.SetNum(3);
// IntArray == [0]
IntArray.SetNum(1);
2.8 AddDefaulted
采用默认构造函数,向TArray增加Count个元素
SizeType AddDefaulted(SizeType Count = 1);
2.9 更多
TArray存在很多变式的增加元素的函数,比如AddDefaulted_GetRef
、InsertDefaulted
、Add_GetRef
等等。一般增加之后返回的是元素的index,XXX_GetRef
返回的就是增加元素的引用; XXXDefaulted
代表以默认构造函数增加元素,一般都有一个输入值Count代表增加多少个元素,也有_GetRef变式,但只能增加一个元素了。
大家根据自己的需求进行关键词组合找到合适的API进行代码逻辑编写,这里就不行一一列举了(实在太多了😭)
3. 删
3.1 Remove
移除所有与提供元素等值的元素,返回删除元素的个数
SizeType Remove(const ElementType& Item);
IntArray = {0,0,1,1};
IntArray.Remove(1);
// IntArray == [0,0],return == 2
3.2 RemoveSingle
擦除数组中的首个匹配元素。以下情况尤为实用——此函数在数组中可能存在重复,而只希望删除一个时;或作为优化,数组只能包含一个匹配元素时。删除成功返回1,失败返回0。
SizeType RemoveSingle(const ElementType& Item);
IntArray = {0,1,0,1};
IntArray.RemoveSingle(1);
// IntArray == [0,0,1],return == 1
3.3 RemoveAt
按照从零开始的index移除元素。可使用 IsValidIndex
确定数组中的元素是否使用计划提供的索引,将无效索引传递给此函数会导致运行时错误:
// 删除指定Index的元素
void RemoveAt(SizeType Index);
// 删除指定Index开始的Count个元素, bAllowShrinking是否可以在合适的情况下在remove之后收缩数组,默认为true。不一定会减少Slack
void RemoveAt(SizeType Index, CountType Count, bool bAllowShrinking = true);
IntArray = {0,1,0,1};
IntArray.RemoveAt(2);
// IntArray == [0,1,1]
IntArray.RemoveAt(99);
// fatal error,无效的index
3.4 RemoveAll
数移除与谓词函数匹配的元素,下面的例子会移除0和1
SizeType RemoveAll(const PREDICATE_CLASS& Predicate);
IntArray = {0,1,0,1,2,3,4};
IntArray.RemoveAll([](int32 val) -> bool { return (val == 1 || val == 0); });
// IntArray == [2,3,4]
3.5 Empty
移除数组中所有元素,输入Slack代表移除元素后TArray的ArrayMax大小,默认为0。
void Empty(SizeType Slack = 0);
IntArray.Empty(2);
// IntArray == []
3.6 RemoveXXXSwap
移动过程存在开销。如不需要剩余元素排序,可使用 RemoveSwap
、RemoveSingleSwap
、RemoveAtSwap
和 RemoveAllSwap
函数减少此开销。此类函数的工作方式与其非交换变种相似,不同之处在于其不保证剩余元素的排序,因此可更快地完成任务:
IntArray.Empty(10);
for (int32 i = 0; i != 10; ++i)
IntArray.Add(i % 5);
// IntArray == [0,1,2,3,4,0,1,2,3,4]
IntArray.RemoveSwap(2);
// IntArray == [0,1,4,3,4,0,1,3]
IntArray.RemoveAtSwap(1);
// IntArray == [0,3,4,3,4,0,1]
IntArray.RemoveAllSwap([](int32 Val) {return Val % 3 == 0;});
// IntArray == [1,4,4]
RemoveXXXSwap
可以输入bAllowShrinking参数
4. 迭代
4.1 基于索引
for (int32 Index = 0; Index != IntArray.Num(); ++Index)
{
IntArray[Index];
}
4.2 ranged-for
for (auto& val : IntArray)
{
val;
}
4.3 迭代器
TArray.CreateIterator()
可读可写与TArray.CreateConstIterator()
只读迭代器
for (auto It = IntArray.CreateIterator(); It; ++It)
{
*It;
}
5. 查
5.1 Num
返回ArrayNum,即已经存储的元素个数
FORCEINLINE SizeType Num() const
{
return ArrayNum;
}
5.2 GetData
返回指向数组内存的原生指针,若容器为常量,则返回的指针也为常量。
int32* Ptr = IntArray.GetData();
// *Ptr == 0
// Ptr[0] == 0
// Ptr[1] == 1
5.3 GetTypeSize
返回单个元素所占大小
uint32 ElementSize = IntArray.GetTypeSize();
// ElementSize == sizeof(int32)
5.4 IsValidIndex
判断指定index是否有效
// true
IntArray.IsValidIndex(0);
// false
IntArray.IsValidIndex(2);
5.5 Last
返回从数组末端反向索引的元素引用。索引默认为零
ElementType& Last(SizeType IndexFromTheEnd = 0);
// return == 1
IntArray.Last();
// return == 1
IntArray.Last(0);
// return == 0
IntArray.Last(1);
5.6 Top
返回末端元素,相当于IntArray.Last()
。不接受索引输入
// return == 1
IntArray.Top();
5.7 Contains
判断是否存在特定元素,返回bool。
template <typename ComparisonType>
bool Contains(const ComparisonType& Item) const;
// return == true
IntArray.Contains(1);
// return == false
IntArray.Contains(3);
请注意这里通过==
进行判断是否相等,同时 Contains
是一个模板函数。代表若是不同的对象,但是重载了==
运算符也是可以比较的。
class TestClass
{
public:
int32 a;
TestClass(int x)
{
a = x;
}
};
bool operator==(int32 a, TestClass x)
{
return a == x.a;
}
bool operator==(TestClass x,int32 a)
{
return a == x.a;
}
TestClass tem(1);
// res == true
bool res = IntArray.Contains(tem);
5.8 ContainsByPredicate
通过谓词函数判断是否存在特定元素
bool ContainsByPredicate(Predicate Pred) const;
// return == true
IntArray.ContainsByPredicate([](const int32 item) { return (item == 0 || item == 3); });
5.9 Find
从头开始查找特定元素。若想从末尾向前查找则使用FindLast
,使用方法相同
// 通过给定元素查找,若存在则会返回首个匹配的index,若不存在则会返回 INDEX_NONE(-1)
SizeType Find(const ElementType& Item) const;
// 通过给定元素查找,并将传入的Index进行设置,若查找成功返回true,失败则返回false
bool Find(const ElementType& Item, SizeType& Index) const;
// return == 1
IntArray.Find(1);
// return == -1
IntArray.Find(3);
// return == true , Index == 1
int32 Index;
IntArray.Find(1,Index);
// return == false , Index == -1
int32 Index;
IntArray.Find(3,Index);
5.10 FindByPredicate
从头开始通过谓词函数查找特定元素,返回元素 指针 而不是index。若想从末尾开始查找,则使用FindLastByPredicate
。
ElementType* FindByPredicate(Predicate Pred);
// *res == 0
auto* res = IntArray.FindByPredicate([](const int32 item) { return (item == 0 || item == 3); });
FindLastByPredicate
有一点区别在于可以指定从第几个开始向前查找。如FindLastByPredicate(Predicate,3)
代表从第三个元素开始向前查找(会查找0,1,2)
FORCEINLINE SizeType FindLastByPredicate(Predicate Pred) const
{
return FindLastByPredicate(Pred, ArrayNum);
}
SizeType FindLastByPredicate(Predicate Pred, SizeType Count) const;
5.11 FindByKey
从头开始查找特定元素,通过==元素符。与Contains
类似,支持不同对象之间的查找。返回对应元素的 指针
template <typename KeyType>
ElementType* FindByKey(const KeyType& Key);
TestClass tem(1);
// *re == 1
auto* re = IntArray.FindByKey(tem);
5.12 IndexOfByKey
与FindByKey
效果相同,返回的是元素的index而非指针
template <typename KeyType>
SizeType IndexOfByKey(const KeyType& Key) const;
TestClass tem(1);
// return == 1
IntArray.IndexOfByKey(tem)
5.13 IndexOfByPredicate
查找与特定谓词匹配的首个元素的index;若未找到,返回 INDEX_NONE 值。FindByPredicate
返回的是指针
template <typename Predicate>
SizeType IndexOfByPredicate(Predicate Pred) const
//return == 0
IntArray.IndexOfByPredicate([](const int32 item) { return (item == 0 || item == 3); });
5.14 FilterByPredicate
获取与特定谓词匹配的元素数组
template <typename Predicate>
TArray<ElementType> FilterByPredicate(Predicate Pred) const;
IntArray.Emplace(3);
IntArray.Emplace(1);
TArray<int32> ResArray = IntArray.FilterByPredicate([](const int32 item) { return (item == 0 || item == 3); });
// IntArray == [0,1,3,1]
// ResArray == [0,3]
6. 排序
6.1 Sort
底层使用 快速排序,不稳定。默认排序规则:运算符<,支持输入谓词函数。
IntArray = { 5,6,3,2,1,9,8,7,0,4 };
// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.Sort();
// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.Sort([](const int32& A, const int32& B) {
return A > B;
});
6.2 HeapSort
底层使用 堆排序,不稳定。默认排序规则:运算符<,支持输入谓词函数。
IntArray = { 5,6,3,2,1,9,8,7,0,4 };
// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.HeapSort();
// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.HeapSort([](const int32& A, const int32& B) {
return A > B;
});
6.3 StableSort
底层使用 归并排序,稳定。默认排序规则:运算符<,支持输入谓词函数。
IntArray = { 5,6,3,2,1,9,8,7,0,4 };
// IntArray = [0,1,2,3,4,5,6,7,8,9]
IntArray.StableSort();
// IntArray = [9,8,7,6,5,4,3,2,1,0]
IntArray.StableSort([](const int32& A, const int32& B) {
return A > B;
});
7. 运算符
7.1 []
返回指定index元素的引用
IntArray[0] = 10;
IntArray[1] = 11;
// IntArray == [10,11]
7.2 == !=
判断是否相等,所含元素进行比较(元素的顺序和数量相同时,两个数组才被视为相同)
// res == true
bool res = (IntArray == TArray<int32>{0, 1});
// res == false
bool res = (IntArray != TArray<int32>{0, 1});
7.3 +=
作为Append
的替代,内部其实调用了Append
TArray& operator+=(const TArray& Other);
// 用于移动语义
TArray& operator+=(TArray&& Other);
// 初始化列表
TArray& operator+=(std::initializer_list<ElementType> InitList);
IntArray += TArray<int32>{0, 1};
IntArray += {3, 4};
7.4 =
赋值运算符,若赋值对象不是自己,则会销毁原来保存的数据。和构造函数大致相同,例子就不举了
// 另一个TArray
TArray& operator=(const TArray& Other);
// 用于移动语义
TArray& operator=(TArray&& Other);
// 初始化列表
TArray& operator=(std::initializer_list<InElementType> InitList);
// 另一个TArray,用于Allocator发生改变时
TArray& operator=(const TArray<ElementType, OtherAllocator>& Other);
// TArrayView
TArray& operator=(const TArrayView<OtherElementType, OtherSizeType>& Other);
8. Slack
当前未使用的有效预分配元素储存槽(ArrayMax - ArrayNum)。为避免每次添加元素时重新分配内存,分配器提供的内存通常会超过必要内存,使之后调用 Add
时不会因重新分配内存而降低性能。同样,删除元素通常不会释放内存,此操作会使数组拥有Slack元素。默认构建的数组不分配内存,Slack初始为零。
8.1 GetSlack()
返回Slack数量
TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3};
// return == 3
IntArray.GetSlack();
8.2 Max
返回ArrayMax
TArray<int32> tem{0,1};
TArray<int32> IntArray{tem,3};
// return == 5
IntArray.Max();
8.3 Reset
与Empty
类似,但不改变内存分配,除非新大小大于当前数组。如果需要,它调用保存项的析构函数,然后将ArrayNum归零。
void Reset(SizeType NewSize = 0);
IntArray.Reset();
// return == 2
IntArray.Max();
8.4 Shrink
将数组使用的内存缩小到最小,以存储当前在其中的元素。Slack == 0
IntArray.Reset();
IntArray.Shrink();
// return == 0
IntArray.Max();
8.5 Reserve
保留内存,使数组至少可以包含Number个元素。
void Reserve(SizeType Number);
IntArray.Reserve(10);
// return == 10
IntArray.Max();
9. 堆
TArray 拥有支持二叉堆数据结构的函数。堆是一种二叉树,其中父节点的排序等于或高于其子节点。作为数组实现时,树的根节点位于元素0,索引N处节点的左右子节点的指数分别为2N+1和2N+2。子节点彼此间不存在特定排序。
9.1 Heapify
调用 Heapify 函数可将现有数组转换为堆。此会重载为是否接受谓词,无谓词的版本将使用元素类型的 运算符< 确定排序。同一组元素可存在多个有效堆。
void Heapify(const PREDICATE_CLASS& Predicate);
void Heapify();
TArray<int32> HeapArr;
for (int32 Val = 10; Val != 0; --Val)
{
HeapArr.Add(Val);
}
// HeapArr == [10,9,8,7,6,5,4,3,2,1]
HeapArr.Heapify();
// HeapArr == [1,2,4,3,6,5,8,10,7,9]
HeapArr.Heapify([](const int32& A, const int32& B) {
return A > B;
});
// HeapArr == [10,9,8,7,6,5,4,3,2,1]
9.2 HeapPush
将新元素添加到堆,会对其他节点进行重新排序,以对堆进行维护。若在创建堆时指定了谓词函数,这时也需要将谓词函数输入,不然会采用默认运算符< 确定排序,造成堆顺序混乱。
// 默认 <
SizeType HeapPush(const ElementType& InItem);
// 默认 <,用于移动语义
SizeType HeapPush(ElementType&& InItem);
// 自定义谓词
template <class PREDICATE_CLASS>
SizeType HeapPush(const ElementType& InItem, const PREDICATE_CLASS& Predicate);
// 自定义谓词,用于移动语义
template <class PREDICATE_CLASS>
SizeType HeapPush(ElementType&& InItem, const PREDICATE_CLASS& Predicate);
返回值为push元素在数组中的index
HeapArr = {10,9,8,7,6,5,4,3,2,1};
HeapArr.HeapPush(11);
// HeapArr == [10,9,8,7,6,5,4,3,2,1,11]
HeapArr = {10,9,8,7,6,5,4,3,2,1};
HeapArr.HeapPush(11,[](const int32& A, const int32& B) {
return A > B;
});
// HeapArr == [11,10,8,7,9,5,4,3,2,1,6]
9.3 HeapPop
移除堆顶元素(index = 0),输入OutItem会被设置为堆顶元素的引用。注意自定义谓词需要被输入
// 自定义谓词
void HeapPop(ElementType& OutItem, const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);
// 默认 <
void HeapPop(ElementType& OutItem, bool bAllowShrinking = true);
HeapArr = {1,2,4,3,4,5,8,10,7,9,6};
int32 TopNode;
HeapArr.HeapPop(TopNode);
// TopNode == 1
// HeapArr == [2,3,4,6,4,5,8,10,7,9]
9.4 HeapPopDiscard
移除堆顶元素(index = 0),不做任何形式的返回
// 默认 <
void HeapPopDiscard(bool bAllowShrinking = true);
// 自定义谓词
void HeapPopDiscard(const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);
9.5 HeapRemoveAt
删除数组中给定索引处的元素,然后重新排列元素,对堆进行维护:
// 默认 <
void HeapRemoveAt(SizeType Index, bool bAllowShrinking = true);
// 自定义谓词
void HeapRemoveAt(SizeType Index, const PREDICATE_CLASS& Predicate, bool bAllowShrinking = true);
HeapArr = {2,3,4,6,4,5,8,10,7,9};
HeapArr.HeapRemoveAt(1);
// HeapArr == [2,4,4,6,9,5,8,10,7]
9.6 HeapTop
返回堆顶元素引用,不改变堆结构
int32 Item = HeapArr.HeapTop();
9.7 VerifyHeap
根据输入的谓词确保TArray为一个堆
template <class PREDICATE_CLASS>
void VerifyHeap(const PREDICATE_CLASS& Predicate)
{
check(Algo::IsHeap(*this, Predicate));
}
注意事项: 所有关于堆的操作都建立在TArray本身为一个堆的基础上(Heapify
)才能正常运行,堆操作默认使用元素类型的 运算符< 确定排序。如使用自定义谓词,须在所有堆操作中使用相同谓词。
10. 其它
10.1 Swap
交换两个index的元素,会对index进行合法性检测
void Swap(SizeType FirstIndexToSwap, SizeType SecondIndexToSwap);
IntArray.Swap(1,0);
// IntArray == [1,0];
存在SwapMemory
,功能相同,但不会对index进行合法性检测
10.2 BulkSerialize
序列化函数,可用作替代 运算符<<
,将数组作为原始字节块进行序列化,而非执行逐元素序列化。如使用内置类型或纯数据结构体等浅显元素,可改善性能。
10.3 GetAllocatedSize
返回此容器分配的内存量,只返回容器直接分配的大小,而不返回元素本身。
10.4 CountBytes
计数序列化此数组所需的字节数。输入为FArchive对象
最后
本文结合 官方文档 总结了TArray的常见API,最后还剩"Uninitialized"
和"Zeroed"
函数族没有讲解,这两个函数族用于原始数据的bit操作,并不会调用元素的构造函数,感兴趣的可以参考官方文档进行查阅,或者进源码查看注解。
本文较为基础,并没有涉及内部实现原理,可参考:
关于TArray的性能优化,可参考: