TSet は TMap および TMultiMap と似ていますが、重要な違いがあります。それは、TSet では、データ値を独立したキーに関連付けるのではなく、データ値自体をキーとして使用し、要素を評価するオーバーライド可能な関数を使用することです。 TSet は、要素の追加、検索、削除を非常に迅速に (一定時間) 行います。 デフォルトでは、TSet は重複キーをサポートしませんが、この動作はテンプレート パラメータでアクティブ化できます。
TSet
TSet は、順序を無視したコンテキストで一意の要素を格納する高速コンテナ クラスです。 ほとんどのユース ケースでは、要素型という 1 つのパラメータが必要です。 ただし、TSet は別のテンプレート パラメータを使用して設定することで、その動作を変更して汎用性を高めることができます。 DefaultKeyFuncs に基づく派生構造体を指定して、ハッシュ関数を提供したり、同じ値を持つ複数のキーがセット内に存在したりできるようにします。 最後に、他のコンテナ クラスと同様に、データ ストレージにカスタム メモリ アロケーターを提供することができます。
TArray と同様に、TSet は同種のコンテナです。つまり、その要素は全て、厳密に同じ型です。 TSet も値の型であり、通常のコピー、割り当て、デストラクター操作と、TSet が破棄されるその要素の強力な所有権をサポートします。 キーの型も値の型である必要があります。
TSet はハッシュを使用します。つまり、KeyFuncs テンプレート パラメータ (提供される場合) は、要素からキーを決定する方法、2 つのキーが等しいかどうかを比較する方法、キーをハッシュ化する方法、重複キーを許可するかどうかをセットに指示します。 これらにはデフォルトでキーへの参照を返し、等価には演算子 ==、ハッシュには非メンバー GetTypeHash 関数を使用します。 デフォルトでは、セットは重複キーを許可しません。 キーの型がこれらの機能をサポートしている場合、カスタムの KeyFuncs を提供する必要がなく、セット キーとして使用できます。 カスタムの KeyFuncs を書き込むには、DefaultKeyFuncs 構造体を拡張します。
最後に、TSet は、メモリ割り当て動作をコントロールする、オプションのアロケーターを利用できます。 標準の Unreal Engine 4 (UE4) アロケーター (FHeapAllocator や TInlineAllocator など) を TSet のアロケーターとして使用することはできません。 代わりに、TSet はセット アロケーターを使用します。これにより、セットが使用するハッシュ バケットの数と、要素の格納に使用する標準 UE4 アロケーターが定義されます。 詳細は TSetAllocator を参照してください。
TArray とは異なり、メモリ内の TSet 要素の相対的な順序は信頼性や安定したものではなく、要素に対するイテレーションによって、追加された順序と異なる順序で要素が返される可能性があります。 各要素がメモリ内で連続してレイアウト表示されることはほとんどありません。 セットのバッキング データ構造はスパース配列で、これは要素間のギャップを効率的にサポートする配列です。 要素がセットから削除されると、スパース配列にギャップが現れます。 配列に新しい要素を追加すると、それらのギャップを埋めることができます。 ただし、TSet はギャップを埋めるために要素をシャッフルしませんが、ストレージ全体が満杯になり新しい要素が追加されると再割り当てできるため、要素を設定するためのポインタが無効になる場合があります。
TSet (多くの Unreal Engine コンテナと同様) では、要素の型は自明に再配置可能であると想定しています。つまり、未加工のバイトを直接コピーすることで、メモリ内のある場所から別の場所に要素を安全に移動できます。
セットを作成し、入力する
TSet は次のように作成できます。
TSet<FString> FruitSet;これにより、FString データを保持する空の TSet が作成されます。 TSet は演算子 == で直接要素を比較し、GetTypeHash でハッシュ化し、標準ヒープ アロケーターを使用します。 この時点でメモリは割り当てられていません。
セットに値を取り込む標準的な方法は、Add 機能を使用してキー (要素) を指定することです。
FruitSet.Add(TEXT("Banana"));
FruitSet.Add(TEXT("Grapefruit"));
FruitSet.Add(TEXT("Pineapple"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple" ]要素は挿入順にここに表示されますが、メモリ内での実際の順序は保証されていません。 新しいセットでは、挿入順に配置される可能性がありますが、挿入や削除が増えると、新しい要素が最後に表示される可能性が高くなる可能性が高くなります。
このセットはデフォルトのアロケータを使用するため、キーは確実に一意です。 以下は、複製キーの追加を試行した結果です。
FruitSet.Add(TEXT("Pear"));
FruitSet.Add(TEXT("Banana"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear" ]
// Note: Only one banana entry.セットには 4 つの要素があります。 「Pear」によってカウントが 3 から 4 に変わっていますが、新しい「Banana」は古い「Banana」エントリを置き換えたため、セット内の要素の数を変更しませんでした。
TArray と同様に、Add ではなく Emplace を使用することでも、セットに挿入する際に一時的なものが作成されるのを避けることができます。
FruitSet.Emplace(TEXT("Orange"));
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange" ]ここでは、引数がキーの型のコンストラクタに直接渡されています。 これにより、値の一時的な FString の作成が回避されます。 TArray とは異なり、単一引数のコンストラクタを使用して、要素をセットにのみ配置することができます。
Append 関数を使用して別のセットの要素を全て挿入し、マージすることもできます。
TSet<FString> FruitSet2;
FruitSet2.Emplace(TEXT("Kiwi"));
FruitSet2.Emplace(TEXT("Melon"));
FruitSet2.Emplace(TEXT("Mango"));
FruitSet2.Emplace(TEXT("Orange"));
FruitSet.Append(FruitSet2);
// FruitSet == [ "Banana", "Grapefruit", "Pineapple", "Pear", "Orange", "Kiwi", "Melon", "Mango" ]上記の例では、結果のセットは、Add または Emplace を使用して要素を個別に追加した場合と同じになっています。 ソース セットから複製されたキーは、ターゲットの対応するキーに置き換わります。
UPROPERTY TSet を編集する
UPROPERTY マクロと editable キーワード (EditAnywhere、EditDefaultsOnly、または EditInstanceOnly) のいずれかで TSet を設定すると、Unreal Editor で要素を追加および編集できるようになります。
UPROPERTY(Category = SetExample, EditAnywhere)
TSet<FString> FruitSet;イテレーション
TSet のイテレーションは TArray と似ています。 C++ の ranged-for 機能を使用できます。
for (auto& Elem : FruitSet)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT(" \"%s\"\n"),
*Elem
)
);
}
// Output:
CreateIterator 関数と CreateConstIterators 関数を使用してイテレーターを作成することもできます。 CreateIterator は読み取り/書き込みアクセス権を持つイテレーターを返し、CreateConstIterator は読み取り専用のイテレーターを返します。 いずれの場合も、これらのイテレーターの Key 関数および Value 関数を使用して要素を調べることができます。 イテレーターを使って例の「フルーツ」セットのコンテンツを出力すると、次のようになります。
for (auto It = FruitSet.CreateConstIterator(); It; ++It)
{
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT("(%s)\n"),
*It
)
);
}クエリ
現在セットにある要素数を調べるには、Num 関数を呼び出します。
int32 Count = FruitSet.Num();
// Count == 8セットに特定の要素が含まれているかどうかを判断するには、次のように Contains 関数を呼び出します。
bool bHasBanana = FruitSet.Contains(TEXT("Banana"));
bool bHasLemon = FruitSet.Contains(TEXT("Lemon"));
// bHasBanana == true
// bHasLemon == falseFSetElementId 構造体を使用して、セット内のキーのインデックスを見つけることができます。 そのインデックスを演算子 [] とともに使用すると、要素を取得できます。 非定数のセットで演算子 [] を呼び出すと、非定数の参照が返され、定数のセットでそれを呼び出すと、定数の参照が返されます。
FSetElementId BananaIndex = FruitSet.Index(TEXT("Banana"));
// BananaIndex is a value between 0 and (FruitSet.Num() - 1)
FPlatformMisc::LocalPrint(
*FString::Printf(
TEXT(" \"%s\"\n"),
*FruitSet[BananaIndex]
)
);
// Prints "Banana"
セットにキーが含まれているかどうかが不明な場合は、Contains 関数でチェックし、演算子 [] を使用できます。 ただし、これは最適ではありません。取得が成功するには、同じキーに対して 2 つのルックアップが必要だからです。 Find 関数は、これらの動作を単一のルックアップにまとめます。 Find は、セットにキーが含まれている場合は、その要素の値へのポインタを返し、含まれていない場合は null ポインタを返します。 定数セットで Find を呼び出すと、返されるポインタも定数になります。
FString* PtrBanana = FruitSet.Find(TEXT("Banana"));
FString* PtrLemon = FruitSet.Find(TEXT("Lemon"));
// *PtrBanana == "Banana"
// PtrLemon == nullptrこの Array 関数は、TSet 内のすべての要素のコピーが入っている TArray を返します。 渡す配列は演算の開始時に空になるため、結果として得られる要素の数は、常にセット内の要素の数と等しくなります。
TArray<FString> FruitArray = FruitSet.Array();
// FruitArray == [ "Banana","Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ] (order may vary)除去
要素は Remove 関数を使用してインデックスから削除することができますが、これは要素をイテレートする場合のみに使用することをお勧めします。 Remove 機能は削除された要素の数を返します。指定されたキーがセットに含まれていなかった場合は 0 を返します。 TSet で重複キーがサポートされている場合、Remove では一致するすべての要素が削除されます。
FruitSet.Remove(0);
// FruitSet == [ "Grapefruit","Pineapple","Pear","Orange","Kiwi","Melon","Mango" ]要素を削除するとデータ構造に穴が残る場合があります。これは、Visual Studio のウォッチ ウィンドウでセットを視覚化する際に確認できますが、ここではわかりやすくするために省略しています。
int32 RemovedAmountPineapple = FruitSet.Remove(TEXT("Pineapple"));
// RemovedAmountPineapple == 1
// FruitSet == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FString RemovedAmountLemon = FruitSet.Remove(TEXT("Lemon"));
// RemovedAmountLemon == 0最後に、Empty 関数または Reset 関数を使用すると、セットからすべての要素を削除できます。
TSet<FString> FruitSetCopy = FruitSet;
// FruitSetCopy == [ "Grapefruit","Pear","Orange","Kiwi","Melon","Mango" ]
FruitSetCopy.Empty();
// FruitSetCopy == []Empty と Reset は似ていますが、Empty はセットにどれだけのスラックを残すかを示すパラメータを受け取るのに対して、Reset は常にできるだけ多くのスラックを残します。
並び替え
TSet はソートできます。 ソート後、セットのイテレーションは、要素をソート順に表示しますが、この動作は次にセットを変更するまでしか保証されません。 ソートは不安定であるため、重複キーがサポートされているセット内の同等の要素は任意の順序で表示されることがあります。
Sort 関数は、次のようにソート順を指定するバイナリ述語を受け取ります。
FruitSet.Sort([](const FString& A, const FString& B) {
return A > B; // sort by reverse-alphabetical order
});
// FruitSet == [ "Pear", "Orange", "Melon", "Mango", "Kiwi", "Grapefruit" ] (order is temporarily guaranteed)
FruitSet.Sort([](const FString& A, const FString& B) {
return A.Len() < B.Len(); // sort strings by length, shortest to longest
});
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ] (order is temporarily guaranteed)演算子
TArray と同様に、TSet は通常の値型で、標準コピー コンストラクタまたは代入演算子でコピーできます。 Set は自身の要素を所有するため、Set のコピーはディープコピーとなります。新しい Set には、要素の独自のコピーが含まれます。
TSet<int32, FString> NewSet = FruitSet;
NewSet.Add(TEXT("Apple"));
NewSet.Remove(TEXT("Pear"));
// FruitSet == [ "Pear", "Kiwi", "Melon", "Mango", "Orange", "Grapefruit" ]
// NewSet == [ "Kiwi", "Melon", "Mango", "Orange", "Grapefruit", "Apple" ]スラック
スラックは要素を含まない、割り当てられたメモリです。 Reserve を呼び出すことで要素を追加せずにメモリを割り当てることができます。また、Reset を呼び出すか非ゼロのスラック パラメータで Empty を呼び出すと、使用されていたメモリの割り当てを解除せずに要素を削除できます。 スラックは、新しいメモリを割り当てる代わりに、事前割り当てメモリを使用して、セットに新しい要素を追加するプロセスを最適化します。 また、システムではメモリの割り当てを解除する必要がないため、要素の削除にも役立ちます。 これは特に、同じ数以下の要素をすぐに再投入することを想定してセットを空にする場合に効率的です。
TSet には、TArray の Max 関数のように、事前割り当てされている要素の数を確認する方法はありません。
次のコードでは、メモリの割り当てを解除せずにセットからすべての要素を削除するため、スラックが作成されます。
FruitSet.Reset();
// FruitSet == [ <invalid>, <invalid>, <invalid>, <invalid>, <invalid>, <invalid> ]要素を追加する前にメモリを事前に割り当てるなど、スラックを直接作成する場合は、Reserve 関数を使用します。
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" ]スラックの事前割り当てによって、新しい要素が逆の順序で追加されます。 配列とは異なり、セットは要素の順序を維持しようとしません。また、セット扱うコードでは要素の順序が安定したり予測可能になったりしないようにする必要があります。
TSet からすべてのスラックを除去するには、Collapse 関数と Shrink 関数を使用します。 Shrink はコンテナの最後からすべてのスラックを削除しますが、これにより途中または開始に空の要素が残ります。
// 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" ]上記のコードでは末尾に空の要素が 1 つしかないため、Shrink によって削除された要素は 1 つだけです。 全てのスラックを除去するには、Compact または CompactStable 関数を最初に呼び出します。これにより、空のスペースがグループ化され、Shrink 用に準備されます。 その名前が示すように、CompactStable は空の要素を統合しながら要素の順序を維持します。
FruitSet.CompactStable();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0", <invalid>, <invalid>, <invalid>, <invalid> ]
FruitSet.Shrink();
// FruitSet == ["Fruit8", "Fruit6", "Fruit4", "Fruit2", "Fruit0" ]DefaultKeyFuncs
型は要素であり、キーでもあるため、型に演算子 == と非メンバー GetTypeHash オーバーロードがあるかぎり、TSet はそれを使用できます。 ただし、それらの関数をオーバーロードすることが望ましくない場合には、型をキーとして使用することが役立つ場合があります。 そのような場合は、独自のカスタム DefaultKeyFuncs を指定できます。 キー型の KeyFunc を作成するには、次のように、2 つの typedef と 3 つの静的関数を定義する必要があります。
KeyInitType— キーのパスに使用される型。 通常は ElementType テンプレート パラメータから描画されます。ElementInitType— 要素の受け渡しに使用される型。 これも、通常、ElementType テンプレート パラメータから描画されるため、KeyInitType と同じですKeyInitType GetSetKey(ElementInitType Element)— 要素のキーを返します。 セットの場合、これは通常要素自体です。bool Matches(KeyInitType A, KeyInitType B)—AとBが等しい場合はtrueを返し、それ以外の場合はfalseを返します。uint32 GetKeyHash(KeyInitType Key)— キーのハッシュ値を返します。
KeyInitType および ElementInitType は、キー/要素の型の通常の引き渡し規則に対する typedef です。 これらは通常、トリビアル型に対しては値となり、非トリビアル型には定数参照となります。 セットの要素タイプはキー タイプでもあります。そのため、DefaultKeyFuncs は両方を定義するために ElementType という 1 つのテンプレート パラメータのみを使用します。
TSet では、Matches を使用して等しい値を比較した 2 つのアイテム (DefaultKeyFuncs 内) も GetKeyHash (KeyFuncs 内) から同じ値を返すと想定しています。
セットの内部ハッシュが無効になるため、これらの関数のいずれかの結果を変更するような方法で既存の要素のキーを変更しないでください。 これらのルールは、DefaultKeyFuncs のデフォルト実装を使用している場合、演算子 == および GetKeyHash のオーバーロードにも適用されます。
その他
CountBytes および GetAllocatedSize 関数は、内部配列が現在使用しているメモリ量を推計します。 CountBytes は FArchive パラメータを受け取りますが、GetAllocatedSize は受け取りません。 これらの関数は通常、統計情報のレポートに使用されます。
Dump 機能は FOutputDevice を受け取り、セットのコンテンツに関する実装情報を書き出します。 全てのハッシュ エントリからすべての要素をリストする DumpHashElements 関数もあります。 これらの関数は、通常デバッグ用に使用されます。