[UE C++] TSet

[UE C++] TSet

TSet 类似于 TMapTMultiMap,但有一个重要区别:TSet 是通过对元素求值的可覆盖函数,使用数据值本身作为键,而不是将数据值与独立的键相关联。TSet 可以非常快速地添加、查找和删除元素(恒定时间)。默认情况下,TSet 不支持重复的键,但使用模板参数可激活此行为。

TSet 是一种快速容器类,用于在排序不重要的情况下存储唯一元素。

  • 同质容器,所有元素的类型都应完全相同
  • 值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权。TSet 被销毁时,其元素也将被销毁。键类型也必须是值类型。
  • 散列容器,即如果给出了 KeyFuncs 模板参数,该参数会告知集合如何从某个元素确定键,如何比较两个键是否相等,如何对键进行散列,以及是否允许重复键。它们默认只返回对键的引用,使用 运算符== 对比相等性,使用非成员函数 GetTypeHash 进行散列。默认情况下,集合中不允许有重复的键。如果您的键类型支持这些函数,则可以将其用作集合键,无需提供自定义 KeyFuncs。要写入自定义 KeyFuncs,可扩展 DefaultKeyFuncs 结构体。
  • TSet 可通过任选分配器控制内存分配行为。标准虚幻引擎4(UE4)分配器(如 FHeapAllocator 和 TInlineAllocator)不能用作 TSet 的分配器。实际上,TSet 使用集合分配器,该分配器可定义集合中使用的散列桶数量以及用于存储元素的标准UE4分配器
  • TSet 元素的相对排序既不可靠也不稳定,对这些元素进行迭代很可能会使它们返回的顺序和它们添加的顺序有所不同
  • TSet元素也不太可能在内存中连续排列。集合中的后台数据结构是稀疏数组,即在数组中有空位。从集合中移除元素时,稀疏数组中会出现空位。将新的元素添加到阵列可填补这些空位。但是,即便 TSet 不会打乱元素来填补空位,指向集元素的指针仍然可能失效,因为如果存储器被填满,又添加了新的元素,整个存储可能会重新分配。

1. 创建和初始化

// 无参构造
TSet<FString> TestSet;
// 拷贝构造
TSet<FString> TempSet{TestSet};
// 移动拷贝构造
TSet<FString> TempSet{MoveTemp(TestSet)};
// TArray
TArray<FString> TempArray;
TSet<FString> TestSet{TempArray};
// TArray 移动构造
TArray<FString> TempArray;
TSet<FString> TestSet{MoveTemp(TempArray)};
// 初始化列表
TSet<FString> TestSet = { TEXT("One"),TEXT("Two"),TEXT("Three"),TEXT("Four") };

// 针对不同的Allocator也可进行构造
template<typename OtherAllocator>
TSet& operator=(const TSet<ElementType, KeyFuncs, OtherAllocator>& Other);

template<typename OtherAllocator>
TSet& operator=(TSet<ElementType, KeyFuncs, OtherAllocator>&& Other);

2. 增

2.1 Add

增加一个元素

  • bIsAlreadyInSetPtr:指向bool的可选指针,将根据元素是否已经在TSet中而设置。
  • FSetElementId,指向存储在集合中的元素的标识符
FSetElementId Add(const InElementType&  InElement, bool* bIsAlreadyInSetPtr = nullptr);
// 移动语义
FSetElementId Add(      InElementType&& InElement, bool* bIsAlreadyInSetPtr = nullptr);

由于此集合使用了默认分配器,可以确保键是唯一的。如果尝试添加重复键,会发生以下情况:

bool* bIsExit = new bool{ false };

// *bIsExit == false , TestSet == ["One"]
TestSet.Add(TEXT("One"), bIsExit);
// *bIsExit == true  , TestSet == ["One"]
TestSet.Add(TEXT("One"), bIsExit);

TestSet.Add(TEXT("Two"));
TestSet.Add(TEXT("Three"));
// TestSet == ["One", "Two", "Three"]

delete bIsExit;

此处的元素按插入顺序排列,但不保证这些元素在内存中实际保留此排序。如果是新集合,可能会保留插入排序,但插入和删除的次数越多,新元素不出现在末尾的可能性越大。

2.2 Emplace

TArray 一样,还可使用 Emplace 代替 Add,避免插入集合时创建临时文件。但只能使用单一参数构造函数将元素放到集合中。

template <typename ArgsType>
FSetElementId Emplace(ArgsType&& Args, bool* bIsAlreadyInSetPtr = nullptr);

2.3 Append

将另一个TSet追加到此TSet,支持移动语义,但与TMap不用,可以输入初始化列表

// TSet
TestSet.Append(TSet<FString>{TEXT("One"), TEXT("Two")});
// 初始化列表
TestSet.Append({ TEXT("Three"),TEXT("Four") });

3. 删

3.1 Remove

删除所有符合要求的元素

// 根据指定元素删除,返回删除数量,不存在则返回 0
int32 Remove(KeyInitType Key);

// 根据 FSetElementId 删除
void Remove(FSetElementId ElementId);
auto tem = TestSet.Add(TEXT("SS"));
TestSet.Remove(tem);

TestSet.Remove(TEXT("One"));

3.2 Reset

清空集合元素,但保留内存分配

void Reset();

3.3 Empty

清空元素,指定内存分配,默认为 0

void Empty(int32 ExpectedNumElements = 0);

4. 迭代

4.1 ranged-for

FString text;
for (auto& Item : TestSet)
{
    text += Item;
    text += "\t";
}

4.2 迭代器

CreateConstIterator()只读迭代器,与CreateIterator()可读可写迭代器

for (auto It = TestSet.CreateIterator(); It; ++It)
{
    UE_LOG(LogTemp, Warning, TEXT("%s"), *(*It));
}

5. 查

5.1 Num

返回集合元素数量

int32 Num() const;

5.2 Contains

根据指定元素查询是否存在,返回bool

bool Contains(KeyInitType Key) const;

5.3 Find

根据指定元素查询,返回指向集合元素的指针,若未查询到,返回nullptr

ElementType* Find(KeyInitType Key);

5.4 FindId

根据指定元素查询,返回指向集合元素的 FSetElementId,若未查询到,则 FSetElementId.IsValidId() == false

FSetElementId FindId(KeyInitType Key) const;

5.5 Array

返回元素的 TArray 拷贝

TArray<ElementType> Array() const;

5.6 IsValidId

检查 FSetElementId 是否有效

bool IsValidId(FSetElementId Id) const
auto tem = TestSet.Add(TEXT("SS"));
// 1
UE_LOG(LogTemp, Warning, TEXT("%d"), TestSet.IsValidId(tem));
TestSet.Reset();
// 0
UE_LOG(LogTemp, Warning, TEXT("%d"), TestSet.IsValidId(tem));

6. 排序

TSet 可以排序。排序后,迭代集合会以排序的顺序显示元素,但下次修改集合时,排序可能会发生变化。

6.1 Sort

不稳定排序,可能按任何顺序显示集合中支持重复键的等效元素。

template <typename PREDICATE_CLASS>
void Sort( const PREDICATE_CLASS& Predicate );
TestSet = { TEXT("a"),TEXT("aa"),TEXT("aaa"),TEXT("aaaa") };

TestSet.Sort([](FString A, FString B) { return A.Len() > B.Len(); });
// TestSet == [ "aaaa", "aaa", "aa", "a" ]

6.2 StableSort

稳定排序,按原来迭代顺序显示集合中支持重复键的等效元素。

template <typename PREDICATE_CLASS>
void StableSort(const PREDICATE_CLASS& Predicate);

7. 运算符

7.1 =

与构造函数相比,只缺少了 TSet = TArray ,此处不赘述

7.2 []

根据 FSetElementId 访问集合对应元素的引用

ElementType& operator[](FSetElementId Id);
const ElementType& operator[](FSetElementId Id) const;
FSetElementId Index = TestSet.Add(TEXT("One"));
TestSet[Index] = TEXT("OneOne");

8. Slack

Slack是不包含元素的已分配内存。调⽤ Reserve 可分配内存,⽆需添加元素; 调⽤ Reset 或 通过非零 slack 参数调⽤ Empty 可移除元素,⽆需将其使⽤的内存取消分配。Slack优化了将新元素添加到映射的过程,因为可以使⽤预先分配的内存,⽽不必分配新内存。它在移除元素时也⼗分实⽤,因为系统不需要将内存取消分配。在清空并希望⽤相同或更少的元素立即重新填充的映射时,此⽅法尤其有效。

TSet 不像 TArray 中的 Max 函数那样可以检查预分配元素的数量。

8.1 Reserve

预先分配内存,若输入的Number⼤于元素个数,则会产⽣Slack

TestSet.Reserve(10);

8.2 Shrink

从容器的末端移除所有slack,但这会在中间或开始处留下空⽩元素。若想移除所有slack,需要先调⽤ Compact

TSet<FString> FruitSet;
FruitSet.Reserve(10);
for (int32 i = 0; i < 10; ++i)
{
    FruitSet.Add(FString::Printf(TEXT("Fruit%d"), i));
}
// FruitSet == [ "Fruit9", "Fruit8", "Fruit7" ..."Fruit2", "Fruit1", "Fruit0" ]

// Remove every other element from the set.
for (int32 i = 0; i < 10; i += 2)
{
    FruitSet.Remove(FSetElementId::FromInteger(i));
}
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0", <invalid> ]

FruitSet.Shrink();
// FruitSet == ["Fruit8", <invalid>, "Fruit6", <invalid>, "Fruit4", <invalid>, "Fruit2", <invalid>, "Fruit0" ]

在上述代码中,Shrink 只删除了一个无效元素,因为末端只有一个空元素。

8.3 Compact

将空⽩元素组合到⼀起放在末尾。下述例⼦元素顺序不⼀定正确,Compact 可能会改变元素之间的顺序。若不
想改变顺序,可以使⽤ CompactStable

FruitSet.CompactStable();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ]

9. KeyFuns

默认使用 DefaultKeyFuncs<ElementType,false>,其中第二个参数代表是否允许重复的元素,默认 = false

template<typename ElementType,bool bInAllowDuplicateKeys /*= false*/>
struct DefaultKeyFuncs : BaseKeyFuncs<ElementType,ElementType,bInAllowDuplicateKeys>
{
    ····
}

若想TSet支持重复元素,可显示使用DefaultKeyFuncs,并将第二个模板参数传递true

TSet<int, DefaultKeyFuncs<int,true>> IntSet = {1,1,1,2,3,4,2,2};

只要类型具有 运算符== 和非成员 GetTypeHash 重载,就可为TSet所用,因为此类型既是元素又是键。然而,不便于重载这些函数时可将类型作为键使用。在这些情况下,可对 DefaultKeyFuncs 进行自定义。

为键类型创建 KeyFuncs,必须定义两个typedef和三个静态函数,如下所示:

  • KeyInitType —— 用于传递键的类型。
  • ElementInitType —— 用于传递元素的类型。
  • KeyInitType GetSetKey(ElementInitType Element) —— 返回元素的键。
  • bool Matches(KeyInitType A, KeyInitType B) —— 如果 A 和 B 等值将返回 true,否则返回 false。
  • uint32 GetKeyHash(KeyInitType Key) —— 返回 Key 的散列值。

KeyInitTypeElementInitType 是键/元素类型普通传递惯例的typedef。它们通常为浅显类型的一个值和非浅显类型的一个常量引用。请注意,集合的元素类型也是键类型,因此 DefaultKeyFuncs 仅使用一种模板参数 ElementType 定义两者。

作为元素/键的Struct:

struct FMyStruct
{
    // String which identifies our key
    FString UniqueID;

    // Some state which doesn't affect struct identity
    float SomeFloat;

    explicit FMyStruct(float InFloat)
        :UniqueID (FGuid::NewGuid().ToString())
        , SomeFloat(InFloat)
    {
    }
};

自定义 KeyFuncs:

template<bool bInAllowDuplicateKeys = false>
struct TMyStructSetKeyFuncs :BaseKeyFuncs<FMyStruct,FString,bInAllowDuplicateKeys>
{
private:
    typedef BaseKeyFuncs<FMyStruct, FString, bInAllowDuplicateKeys> Super;

public:
    typedef typename Super::ElementInitType ElementInitType;
    typedef typename Super::KeyInitType     KeyInitType;

    static KeyInitType GetSetKey(ElementInitType Element)
    {
        return Element.UniqueID;
    }

    static bool Matches(KeyInitType A, KeyInitType B)
    {
        return A.Compare(B, ESearchCase::CaseSensitive) == 0;
    }

    static uint32 GetKeyHash(KeyInitType Key)
    {
        return FCrc::StrCrc32(*Key);
    }
};

FMyStruct 具有唯一标识符(UniqueID),以及一些与身份无关的其他数据(SomeFloat)。GetTypeHash 和 运算符== 不适用于此,因为 运算符== 为实现通用目的不应忽略任何类型的数据,但同时又需要如此(忽略除UniqueID之外的其他数据)才能与 GetTypeHash 的行为保持一致,后者(GetTypeHash)只关注 UniqueID 字段。以下步骤有助于为 FMyStruct 创建自定义 KeyFuncs:

  1. 首先,继承 BaseKeyFuncs,因为它可以帮助定义某些类型,包括 KeyInitTypeElementInitType,以及一个bool参数bInAllowDuplicateKeys,代表是否可以容纳重复元素。
    BaseKeyFuncs 使用两个模板参数:集合的元素类型和Key类型。
    • 这里元素类型为 FMyStruct
    • 第二个 BaseKeyFuncs 参数是Key类型,不要与元素类型混淆。因为此集合应使用 UniqueID(来自 FMyStruct)作为键,所以此处使用 FString。
  2. 然后,定义三个必需的 KeyFuncs 静态函数:
    • 第一个是 GetSetKey,该函数返回给定元素类型的Key。由于元素类型是 FMyStruct,而键是 UniqueID,所以该函数可直接返回 Element.UniqueID
    • 第二个静态函数是 Matches,该函数接受两个元素的键(由 GetSetKey 获取),然后比较它们是否相等。在 FString 中,标准的等效测试(运算符==)不区分大小写;要替换为区分大小写的搜索,请用相应的大小写对比选项使用 Compare 函数。
    • 最后,GetKeyHash 静态函数接受提取的键并返回其散列值。由于 Matches 函数区分大小写,GetKeyHash 也必须区分大小写。区分大小写的 FCrc 函数将计算键字符串的散列值。

现在结构已满足 TSet 要求的行为,可创建它的实例。

TSet<FMyStruct, TMyStructSetKeyFuncs<false>> TempSet;

auto te1 = TempSet.Add(FMyStruct(3.14f));
auto te2 = TempSet.Add(FMyStruct(1.23f));
UE_LOG(LogTemp, Warning, TEXT("%f"), TempSet[te1].SomeFloat);
UE_LOG(LogTemp, Warning, TEXT("%f"), TempSet[te2].SomeFloat);

10. 其它

10.1 GetAllocatedSize

返回此容器分配的内存量,只返回容器直接分配的⼤⼩,⽽不返回元素本⾝。

10.2 CountBytes

计数序列化此数组所需的字节数。输入为FArchive对象。

11. 数学运算

11.1 Difference

求差集

TSet<int> TempSet1 = { 1,2,3,4 };
TSet<int> TempSet2 = { 2,3,5,6 };
TSet<int> TempSet3 = TempSet2.Difference(TempSet1);
// TempSet3 = [5,6] 实际遍历顺序可能会发生变化

11.2 Intersect

求交集

TSet<int> TempSet1 = { 1,2,3,4 };
TSet<int> TempSet2 = { 2,3,5,6 };
TSet<int> TempSet3 = TempSet2.Intersect(TempSet1);
// TempSet3 = [2,3] 实际遍历顺序可能会发生变化

11.3 Union

求并集

TSet<int> TempSet1 = { 1,2,3,4 };
TSet<int> TempSet2 = { 2,3,5,6 };
TSet<int> TempSet3 = TempSet2.Union(TempSet1);
// TempSet3 = [1,2,3,4,5,6] 实际遍历顺序可能会发生变化

11.4 Includes

判断是否为子集

TSet<int> TempSet1 = { 1,2,3,4 };
TSet<int> TempSet2 = { 2,3,5,6 };
TSet<int> TempSet3 = { 2,4 };
// 0-false
UE_LOG(LogTemp, Warning, TEXT("%d"), TempSet1.Includes(TempSet2));
// 1-true
UE_LOG(LogTemp, Warning, TEXT("%d"), TempSet1.Includes(TempSet3));

最后

本文结合官方文档总结了TSet的一些常用API,若有错误,还请指正。

参考

猜你喜欢

转载自blog.csdn.net/qq_52179126/article/details/130756860