TArray:Unreal Engine における配列

Unreal Engine の主要なコンテナ クラスは TArray です。 TArray は同じ型を持つ他のオブジェクト (要素と呼びます) のシーケンスのオーナーシップと構成を定義するクラスです。TArray はシーケンスで、要素の順序は明確に定義され、その関数を使ってこれらのオブジェクトと順序を確定的に操作します。

TArray

TArray は、Unreal Engine で最も一般的なコンテナ クラスです。高速で、メモリ効率がよく、安全に作成されています。TArray 型は 2 つのプロパティで定義されます。要素型、そしてオプションのアロケータです。

要素型は、配列に格納されるオブジェクトの型です。TArray は、いわゆる均一なコンテナです。つまり、すべてのエレメントが完全に同じ型です。一つの TArray の中に異なる要素型を混在させることはできません。

アロケータは多くの場合省略され、ほとんどのユースケースに適切なアロケータがデフォルト設定になります。メモリでのオブジェクトの置き方や、さらに多くの要素を収容できるように、配列の拡大方法を定義します。デフォルトと違うビヘイビアを使いたい、もしくは自分で書き出すことができると感じた場合は、他にも様々なアロケータを使用できます。これについては、後述します。

TArray は value 型です。つまり、int32float などの他のビルドイン型と同様に処理されます。継承はされないので、new / delete を使ってヒープ上で TArray を作成 / 破棄することは通常はありません。要素も value 型で、コンテナに所有されます。TArray を破棄すると、それがまだ含まれるすべての要素が破棄されます。他のものから TArray 変数を作成すると、その要素は新しい変数へコピーされます。ステートが共有されることはありません。

配列の作成と追加

次のように定義して、配列を作成します。

    TArray<int32> IntArray;

整数のシーケンスを持つように設定された空の配列が作成されます。要素型には、int32FStringTSharedPtr などの一般的な C++ 値ルールに従ってコピーと破棄が可能であれば、どんな値でも使うことができます。アロケータは指定されていないので、TArray はデフォルトのヒープを基本とする割り当てになります。この時点では、メモリは割り当てられていません。

TArrays を埋める方法は何通りかあります。1 つ目は、要素のコピーを多く使って配列を埋めていく Init 関数です。

    IntArray.Init(10, 5);
    // IntArray == [10,10,10,10,10]

Add 関数と `Emplace`関数は、次の点だけが若干異なります。

    TArray<FString> StrArr;
    StrArr.Add    (TEXT("Hello"));
    StrArr.Emplace(TEXT("World"));
    // StrArr == ["Hello","World"]

新しい要素が配列に追加されると、配列のアロケータは必要に応じてメモリを提供します。デフォルトのアロケータは、現在の配列サイズを超えると常に複数の新しい要素に十分なメモリを追加します。AddEmplace はほぼ同じですが、以下の点が異なります。

  • Add (または Push) は要素型のインスタンスを配列へコピー (または移動) します。

  • Emplace は与えた引数を使って要素型の新しいインスタンスを構築します。

TArray<FString> の場合、Add は文字列リテラルから一時的な FString を作成し、その後で一時的なコンテンツをコンテナ内の新しい FString に移動しますが、Emplace は文字列リテラルを使って直接 Emplace を作成します。結果は同じですが、Emplace は一時的な変数の作成を避けるので、FString などの非トリビアルな value 型には適さない場合が多いです。

呼び出し側で一時的な変数が不要に作成され、コンテナへコピーあるいは移動されることを防ぐので、一般的に Add より Emplace が望ましいです。ざっくり分けると、Add は トリビアル型、Emplace はそれ以外に使います。Emplace は常に Add より効率的ですが、Add の方が見やすいです。

Append は複数の要素を別の TArray または ポインタのいずれかからその配列のサイズの通常の C 配列に一度に追加します。

    FString Arr[] = { TEXT("of"), TEXT("Tomorrow") };
    StrArr.Append(Arr, ARRAY_COUNT(Arr));
    // StrArr == ["Hello","World","of","Tomorrow"]

AddUnique は、同等の要素がまだ存在しない場合に、新規要素のみをコンテナに追加します。等価性は要素型の 演算子== を使って確認します。

    StrArr.AddUnique(TEXT("!"));
    // StrArr == ["Hello","World","of","Tomorrow","!"]

    StrArr.AddUnique(TEXT("!"));
    // StrArr is unchanged as "!" is already an element

AddEmplaceAppend と同様に Insert は、単一の要素または要素の配列のコピーを任意のインデックスに追加します。

    StrArr.Insert(TEXT("Brave"), 1);
    // StrArr == ["Hello","Brave","World","of","Tomorrow","!"]

新しい数が現在の数よりも大きい場合、要素の型のデフォルト コンストラクタを使って作成されている新しい要素によって、SetNum 関数は配列要素の数を直接設定することができます。

    StrArr.SetNum(8);
    // StrArr == ["Hello","Brave","World","of","Tomorrow","!","",""]

新しい数が現在の数より小さい場合、SetNum は要素も取り除きます。要素の削除の詳細は後述します。

    StrArr.SetNum(6);
    // StrArr == ["Hello","Brave","World","of","Tomorrow","!"]

イタレーション

配列要素のイタレーション方法の中でも C++ の ranged-for 機能を使う方法を推奨します。

    FString JoinedStr;
    for (auto& Str :StrArr)
    {
        JoinedStr += Str;
        JoinedStr += TEXT(" ");
    }
    // JoinedStr == "Hello Brave World of Tomorrow ! "

通常のインデックス ベースのイタレーションももちろん可能です。

    for (int32 Index = 0; Index != StrArr.Num(); ++Index)
    {
        JoinedStr += StrArr[Index];
        JoinedStr += TEXT(" ");
    }

結果、配列にはイタレーションの制御を強める独自のイタレータ型も含まれます。CreateIteratorCreateConstIterator という 2 つの関数は、CreateIterator は要素の読み書きが可能、CreateConstIterator は読み取り専用です:

    for (auto It = StrArr.CreateConstIterator(); It; ++It)
    {
        JoinedStr += *It;
        JoinedStr += TEXT(" ");
    }

ソート

Sort 関数を呼び出すだけで、配列をソートすることができます。

    StrArr.Sort();
    // StrArr == ["!","Brave","Hello","of","Tomorrow","World"]

ここで、値は要素の型の 演算子 < によってソートされます。FString の場合、これは大文字・小文字を区別しない辞書式比較です。バイナリ述語を使って、異なる順序で動作を実装することもできます。

    StrArr.Sort([](const FString& A, const FString& B) {
        return A.Len() < B.Len();
    });
    // StrArr == ["!","of","Hello","Brave","World","Tomorrow"]

この場合、文字列は長さによってソートされます。"Hello"、"Brave"、"World" という長さが同じの 3 つの文字列が、配列の位置に相対してどのように順序が変わっているかに注目してください。これは、Sort が不安定で、同等の要素 (述語は長さのみ比較するので、これらの文字列はここでは同等) の相対的な順序が保証されないためです。Sort はクイックソートとして実装されます。

バイナリ述語の有無に関係なく、HeapSort 関数はヒープソートの実行に使用します。Sort 関数を選択するかどうかは、特定のデータ、および特定のデータがが Sort 関数と比べてどの程度効率的かによります。Sort と同様に、HeapSort は安定していません。上の例で Sort の代わりに HeapSort を使った場合、結果はこのようになります (この場合でも同じ):

    StrArr.HeapSort([](const FString& A, const FString& B) {
        return A.Len() < B.Len();
    });
    // StrArr == ["!","of","Hello","Brave","World","Tomorrow"]

最後に、StableSort を使って、ソート後に同等の要素の相対順序を保証することができます。上の例の SortHeapSort の代わりに StableSort を呼び出していたら、結果は以下のようになるはずです:

    StrArr.StableSort([](const FString& A, const FString& B) {
        return A.Len() < B.Len();
    });
    // StrArr == ["!","of","Brave","Hello","World","Tomorrow"]

つまり、辞書式比較でソートした後、"Brave" 、"Hello" 、"World" の相対順序は変わりません。StableSort はマージソートとして実行されます。

クエリ

Num 関数を使って、維持している要素数を配列に問い合わせることができます。

    int32 Count = StrArr.Num();
    // Count == 6

配列メモリに直接アクセスする必要がある場合、C-style API との相互運用性のために、GetData() 関数を使って配列中の要素へポインタを返すことができます。このポインタは、配列が存在し、配列を変化させる操作が行われるまでは有効です。StrPtr からの最初の Num() インデックスのみ間接参照が可能です。

    FString* StrPtr = StrArr.GetData();
    // StrPtr[0] == "!"
    // StrPtr[1] == "of"
    // ...
    // StrPtr[5] == "Tomorrow"
    // StrPtr[6] - undefined behavior

コンテナが const であれば、返されるポインタも const になります。

要素の大きさをコンテナに問い合わせることもできます。

    uint32 ElementSize = StrArr.GetTypeSize();
    // ElementSize == sizeof(FString)

要素を取得するには、インデックス 演算子[] を使って、ゼロから始まるインデックスを取得したい要素に渡します。

    FString Elem1 = StrArr[1];
    // Elem1 == "of"

0 未満あるいは Num() 以上の無効のインデックスを渡すと、ランタイム エラーが発生します。IsValidIndex 関数を使って、特定のインデックスの有効性をコンテナに問い合わせることができます。

    bool bValidM1 = StrArr.IsValidIndex(-1);
    bool bValid0  = StrArr.IsValidIndex(0);
    bool bValid5  = StrArr.IsValidIndex(5);
    bool bValid6  = StrArr.IsValidIndex(6);
    // bValidM1 == false
    // bValid0  == true
    // bValid5  == true
    // bValid6  == false

演算子[] は参照を返します。従って、配列内で要素を可変するために使用することもできます。配列が const ではないと仮定します。

    StrArr[3] = StrArr[3].ToUpper();
    // StrArr == ["!","of","Brave","HELLO","World","Tomorrow"]

GetData 関数と同様に、演算子[] は配列が const であれば const reference を返します。Last 関数を使って、インデックスを配列末尾から逆方向に開始することも可能です。インデックスのデフォルトはゼロに設定されています。Top 関数は、インデックスを受け取らない点を除けば、Last 関数と同じです。

    FString ElemEnd  = StrArr.Last();
    FString ElemEnd0 = StrArr.Last(0);
    FString ElemEnd1 = StrArr.Last(1);
    FString ElemTop  = StrArr.Top();
    // ElemEnd  == "Tomorrow"
    // ElemEnd0 == "Tomorrow"
    // ElemEnd1 == "World"
    // ElemTop  == "Tomorrow"

所定の要素が含まれているかどうかを配列に問い合わせることができます。

    bool bHello   = StrArr.Contains(TEXT("Hello"));
    bool bGoodbye = StrArr.Contains(TEXT("Goodbye"));
    // bHello   == true
    // bGoodbye == false

もしくは、特定の述語と一致する要素が含まれているかどうかを配列に問い合わせることができます。

    bool bLen5 = StrArr.ContainsByPredicate([](const FString& Str){
        return Str.Len() == 5;
    });
    bool bLen6 = StrArr.ContainsByPredicate([](const FString& Str){
        return Str.Len() == 6;
    });
    // bLen5 == true
    // bLen6 == false

関数の Find ファミリを使って、要素の検索ができます。Find を使って、要素が存在しているか、インデックスを返すかどうかを確認します。

    int32 Index;
    if (StrArr.Find(TEXT("Hello"), Index))
    {
        // Index == 3
    }

これにより、最初に検出された要素のインデックスを Index に設定します。要素が重複していて、最後の要素のインデックスを探したい場合は、FindLast 関数を使います。

    int32 IndexLast;
    if (StrArr.FindLast(TEXT("Hello"), IndexLast))
    {
        // IndexLast == 3, because there aren't any duplicates
    }

これらの関数は両方とも、要素が検出されると、要素のインデックスを変数に書き込みながら、bool を使って要素の検索結果を表します。

FindFindLast は要素 インデックスを直接返すこともできます。インデックスを明示的な引数として渡さない場合にそうなります。こちらの方が上記の関数より簡単です。ニーズや形式に適しているかどうかによって使い分けます。

要素が検出されなかった場合は、INDEX_NONE という特別な値が返されます。

    int32 Index2     = StrArr.Find(TEXT("Hello"));
    int32 IndexLast2 = StrArr.FindLast(TEXT("Hello"));
    int32 IndexNone  = StrArr.Find(TEXT("None"));
    // Index2     == 3
    // IndexLast2 == 3
    // IndexNone  == INDEX_NONE

IndexOfByKey も同様に機能し、さらに任意のオブジェクトと要素の比較が可能です。Find 関数を使うと、検索開始前に引数が実際に要素の型 (このケースでは FString) に変換されます。IndexOfByKey 関数の場合はキーを直接比較するので、key 型が直接要素の型に変換できない場合でも検索をサポートします。

IndexOfByKey演算子==(ElementType, KeyType) が存在するすべてのキー型に対して機能します。IndexOfByKey は最初に検出された要素のインデックスを返し、要素が検出されなかった場合は INDEX_NONE を返します。

    int32 Index = StrArr.IndexOfByKey(TEXT("Hello"));
    // Index == 3

指定された述語と一致する最初の要素のインデックスを探すために IndexOfByPredicate 関数を使います。何も検出されなければ、ここでも特別な INDEX_NONE 値を返します。

    int32 Index = StrArr.IndexOfByPredicate([](const FString& Str){
        return Str.Contains(TEXT("r"));
    });
    // Index == 2

インデックスを返す代わりに検出した要素へポインタを返すことができます。FindByKeyIndexOfByKey と同様に要素を任意のオブジェクトと比較しますが、検出した要素へポインタを返します。要素を検出しない場合は nullptr を返します。

    auto* OfPtr  = StrArr.FindByKey(TEXT("of")));
    auto* ThePtr = StrArr.FindByKey(TEXT("the")));
    // OfPtr  == &StrArr[1]
    // ThePtr == nullptr

FindByPredicateIndexOfByPredicate と使用方法は同じですが、インデックスではなくポインタを返す点が違います。

    auto* Len5Ptr = StrArr.FindByPredicate([](const FString& Str){
        return Str.Len() == 5;
    });
    auto* Len6Ptr = StrArr.FindByPredicate([](const FString& Str){
        return Str.Len() == 6;
    });
    // Len5Ptr == &StrArr[2]
    // Len6Ptr == nullptr

最後に、特定の述語と一致する要素の配列が FilterByPredicate 関数で返されます。

    auto Filter = StrArray.FilterByPredicate([](const FString& Str){
        return !Str.IsEmpty() && Str[0] < TEXT('M');
    });

Removal

関数の Remove ファミリを使って、配列から要素を除去することができます。Remove 関数は、要素型の 演算子== 関数に従って提供した要素と等しいとみなされるすべての要素を除去します。例:

    TArray<int32> ValArr;
    int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 };
    ValArr.Append(Temp, ARRAY_COUNT(Temp));
    // ValArr == [10,20,30,5,10,15,20,25,30]

    ValArr.Remove(20);
    // ValArr == [10,30,5,10,15,25,30]

RemoveSingle を使って、配列の先頭に一番近い要素を消去することもできます。配列に複製が含まれているかもしれないのでそれを消去したい場合や、配列に一致する要素を 1 つだけ含むことができない場合の最適化に有用です。

    ValArr.RemoveSingle(30);
    // ValArr == [10,5,10,15,25,30]

RemoveAt 関数を使用して、インデックス番号を指定して要素を取り除くこともできます。無効のインデックスをこの関数に渡すとランタイム エラーが発生するので、IsValidIndex を使って配列に提供する予定のインデックスをもつ要素があるか検証すると良いです。

    ValArr.RemoveAt(2); // インデックス 2 のエレメントを削除します
    // ValArr == [10,5,15,25,30]

    ValArr.RemoveAt(99); // これによりランタイムエラーが発生します
                           // インデックス 99 にエレメントがないためです。

RemoveAll 関数を使って、述語と一致する要素を削除することもできます。例えば、3 の乗数となるすべての値を削除するには以下のようになります。

    ValArr.RemoveAll([](int32 Val) {
        return Val % 3 == 0;
    });
    // ValArr == [10,5,25]

これらすべてのケースでは、要素が削除されて配列内にできた「隙間」を埋める必要があるので、その後に続く要素を低いインデックス番号へずらしました。

シャッフル処理にはオーバーヘッドが伴います。残りの要素の順序にこだわらない場合は、RemoveSwapRemoveAtSwapRemoveAllSwap 関数によって、このオーバーヘッドを減らすことができます。これらの関数は、残りの要素の順序を保証しない点を除いて、スワッピングしないものと同じ動作をするので、タスクを迅速に完了することができます。

    TArray<int32> ValArr2;
    for (int32 i = 0; i != 10; ++i)
        ValArr2.Add(i % 5);
    // ValArr2 == [0,1,2,3,4,0,1,2,3,4]

    ValArr2.RemoveSwap(2);
    // ValArr2 == [0,1,4,3,4,0,1,3]

    ValArr2.RemoveAtSwap(1);
    // ValArr2 == [0,3,4,3,4,0,1]

    ValArr2.RemoveAllSwap([](int32 Val) {
        return Val % 3 == 0;
    });
    // ValArr2 == [1,4,4]

最後に、Empty 関数は配列からすべてを除去します。

    ValArr2.Empty();
    // ValArr2 == []

演算子

配列は一般的な value 型です。標準のコピー コンストラクタもしくは代入演算子でコピーすることができます。配列は必ず要素を所有しているため、配列は「深いコピー」が行われ、新規の配列にはその要素の個々のコピーが作られます。

    TArray<int32> ValArr3;
    ValArr3.Add(1);
    ValArr3.Add(2);
    ValArr3.Add(3);

    auto ValArr4 = ValArr3;
    // ValArr4 == [1,2,3];
    ValArr4[0] = 5;
    // ValArr3 == [1,2,3];
    // ValArr4 == [5,2,3];

Append 関数の代わりに、演算子+= を使って配列を連結できます。

    ValArr4 += ValArr3;
    // ValArr4 == [5,2,3,1,2,3]

TArray は、MoveTemp 関数で呼び出し可能な移動の動作もサポートしています。移動後、もとの配列が空になることが保証されます。

    ValArr3 = MoveTemp(ValArr4);
    // ValArr3 == [5,2,3,1,2,3]
    // ValArr4 == []

配列は 演算子==演算子!= を使っても比較することができます。要素の順序は重要です。順序と要素数が同じ場合、その 2 つの配列は等しくなります。要素は固有の 演算子== で比較します。

    TArray<FString> FlavorArr1;
    FlavorArr1.Emplace(TEXT("Chocolate"));
    FlavorArr1.Emplace(TEXT("Vanilla"));
    // FlavorArr1 == ["Chocolate","Vanilla"]

    auto FlavorArr2 = Str1Array;
    // FlavorArr2 == ["Chocolate","Vanilla"]

    bool bComparison1 = FlavorArr1 == FlavorArr2;
    // bComparison1 == true

    for (auto& Str : FlavorArr2)
    {
        Str = Str.ToUpper();
    }
    // FlavorArr2 == ["CHOCOLATE","VANILLA"]

    bool bComparison2 = FlavorArr1 == FlavorArr2;
    // bComparison2 == true, because FString comparison ignores case

    Exchange(FlavorArr2[0], FlavorArr2[1]);
    // FlavorArr2 == ["VANILLA","CHOCOLATE"]

    bool bComparison3 = FlavorArr1 == FlavorArr2;
    // bComparison3 == false, because the order has changed

Heap

TArray にはバイナリ ヒープ データ構造をサポートする関数が含まれています。ヒープとは、親ノードがその子ノードと同等あるいは上に順序付けされるバイナリ ツリーのタイプです。配列として実行されると、ツリーのルートノードが要素 0 、インデックス N のノードの左右の子ノードがそれぞれ 2N+1 と 2N+2 になります。子ノードには、特に決められた順序はありません。

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]

ツリーを視覚化すると下記のようになります。

image alt text

ツリー内のノードは、ヒープ化された配列内の要素の順序通りに、左から右、上から下へ読み取られます。配列は必ずしもヒープ内へ移動された後にソートされるわけではありません。ソートされた配列は有効なヒープにもなります。ヒープ構造定義は緩いので、同じ要素群に対して複数の有効なヒープを許可することができます。

HeapPush 関数を使って、他のノードの順序を変更してヒープを維持しつつ、要素をヒープに追加することができます。

    HeapArr.HeapPush(4);
    // HeapArr == [1,2,4,3,4,5,8,10,7,9,6]

image alt text

ヒープの最上位にあるノードを削除するには、HeapPop 関数と HeapPopDiscard 関数を使います。この 2 つの違いは、HeapPop 関数が最上位の要素のコピーを返すために要素の型を参照するのに対して、HeapPopDiscard 関数は最上位のノードを返さずにそのまま削除します。いずれの関数の場合も配列への変更結果は同じで、他の要素の順序を適切に入れ替えることでヒープが維持されます。

    int32 TopNode;
    HeapArr.HeapPop(TopNode);
    // TopNode == 1
    // HeapArr == [2,3,4,6,4,5,8,10,7,9]

image alt text

HeapRemoveAt は所定のインデックス番号で配列から要素を削除し、要素の順序を入れ替えてヒープを維持します。

    HeapArr.HeapRemoveAt(1);
    // HeapArr == [2,4,4,6,9,5,8,10,7]

image alt text

HeapPushHeapPopHeapPopDiscardHeapRemoveAt は、例えば Heapify()

さらに、これらの各関数は、Heapify を含めて、オプションでバイナリ述語を使ってヒープ内のノード要素の順序を決定できます。デフォルトで、ヒープ操作では要素の型の 演算子< を使って順序を決定します。カスタム述語を使用するときは、すべてのヒープ演算子に同じ述語を使用することが重要です。

最後に、ヒープの最上位ノードは、HeapTop を使えば配列を変更することなく確認できます。

    int32 Top = HeapArr.HeapTop();
    // Top == 2

Slack

配列はリサイズできるので、使用するメモリ量も変動します。要素を追加するたびにメモリを再割り当てしなくて済むように、アロケータは常に必要以上のメモリを確保しておくことで、 Add コールによる再アロケートでパフォーマンスが低下しないようにしておきます。同様に、要素を除去しても通常はメモリは解放されません。これによりスラック要素をもつ配列が残り、現在使用していない要素ストレージ スロットが効率的にプリアロケートされます。配列内のスラック数は、配列内に保存された要素数と、アロケートされているメモリ容量で配列が保存可能な要素数の差として定義されます。

デフォルト構造の配列にはメモリがアロケートされていないので、スラックは最初はゼロです。GetSlack 関数を使って、配列内のスラック数を調べます。コンテナが再割り当てを行うまでの間に配列が保有できる要素の最大数を Max 関数で取得できます。GetSlackMaxNum の差と同等です。

    TArray<int32> SlackArray;
    // SlackArray.GetSlack() == 0
    // SlackArray.Num()      == 0
    // SlackArray.Max()      == 0

    SlackArray.Add(1);
    // SlackArray.GetSlack() == 3
    // SlackArray.Num()      == 1
    // SlackArray.Max()      == 4

    SlackArray.Add(2);
    SlackArray.Add(3);
    SlackArray.Add(4);
    SlackArray.Add(5);
    // SlackArray.GetSlack() == 17
    // SlackArray.Num()      == 5
    // SlackArray.Max()      == 22

再割り当て後のコンテナ内のスラック数はアロケータが決定します。従って、ユーザーはスラックに残っている定数に頼るべきではありません。

スラック管理は要件ではありませんが、配列を最適化するヒントに利用することができます。例えば、配列に要素を 100 個追加する場合、最低 100 個のスラックを追加前に確保することができるので、配列は新しい要素を追加するときにメモリをアロケートする必要がありません。上記の Empty 関数は、オプションでスラック引数を受け取ります。

    SlackArray.Empty();
    // SlackArray.GetSlack() == 0
    // SlackArray.Num()      == 0
    // SlackArray.Max()      == 0
    SlackArray.Empty(3);
    // SlackArray.GetSlack() == 3
    // SlackArray.Num()      == 0
    // SlackArray.Max()      == 3
    SlackArray.Add(1);
    SlackArray.Add(2);
    SlackArray.Add(3);
    // SlackArray.GetSlack() == 0
    // SlackArray.Num()      == 3
    // SlackArray.Max()      == 3

Reset 関数は Empty 関数とよく似ていますが、要求されたスラック数が現在の割り当てで既に確保されている場合はメモリを解放しない点が異なります。ただし、要求されているスラック数が確保できていない場合はメモリの再割り当てが必要です。

    SlackArray.Reset(0);
    // SlackArray.GetSlack() == 3
    // SlackArray.Num()      == 0
    // SlackArray.Max()      == 3
    SlackArray.Reset(10);
    // SlackArray.GetSlack() == 10
    // SlackArray.Num()      == 0
    // SlackArray.Max()      == 10

最後に、Shrink 関数ですべてのスラックを除去します。これにより、アロケーションが現在の要素を維持するための最小サイズにリサイズされます。Shrink は配列内の要素に影響しません。

    SlackArray.Add(5);
    SlackArray.Add(10);
    SlackArray.Add(15);
    SlackArray.Add(20);
    // SlackArray.GetSlack() == 6
    // SlackArray.Num()      == 4
    // SlackArray.Max()      == 10
    SlackArray.Shrink();
    // SlackArray.GetSlack() == 0
    // SlackArray.Num()      == 4
    // SlackArray.Max()      == 4

Raw メモリ

結局のところ、TArray はアロケートされたメモリのラッパーに過ぎません。TArray は常に持っている情報で最大限の実力を発揮しようとしますが、レベルを下げる必要がある場合があります。TArray は持っている情報を使って常に最大限機能しようとしますが、時としてレベルを落とす必要があります。

TArray とそれがもつデータに高速で低レベルアクセスします。以下の関数を使って TArray

AddUninitialized 関数と InsertUninitialized 関数は、配列に初期化されていない空間を追加します。それぞれ、Add 関数と Insert 関数と同じですが、要素の型のコンストラクタを呼び出さない点が異なります。コンテンツの最適化に便利な手法です。Memcpy コールで構造体全体をオーバーライトする場合、以下の例のように行うことができます。

    int32 SrcInts[] = { 2, 3, 5, 7 };
    TArray<int32> UninitInts;
    UninitInts.AddUninitialized(4);
    FMemory::Memcpy(UninitInts.GetData(), SrcInts, 4*sizeof(int32));
    // UninitInts == [2,3,5,7]

この機能でも、自身で構築するオブジェクト用のメモリを確保することができます。

    TArray<FString> UninitStrs;
    UninitStrs.Emplace(TEXT("A"));
    UninitStrs.Emplace(TEXT("D"));
    UninitStrs.InsertUninitialized(1, 2);
    new ((void*)(UninitStrs.GetData() + 1)) FString(TEXT("B"));
    new ((void*)(UninitStrs.GetData() + 2)) FString(TEXT("C"));
    // UninitStrs == ["A","B","C","D"]

AddZeroedInsertZeroed は、追加 / 挿入された空間のバイトもゼロにする点を除けば、機能は同じです。

    struct S
    {
        S(int32 InInt, void* InPtr, float InFlt)
            : Int(InInt)
            , Ptr(InPtr)
            , Flt(InFlt)
        {
        }
        int32 Int;
        void* Ptr;
        float Flt;
    };
    TArray<S> SArr;
    SArr.AddZeroed();
    // SArr == [{ Int: 0, Ptr: nullptr, Flt: 0.0f }]

SetNumUninitialized 関数と SetNumZeroed 関数も SetNum と同じ動きをします。異なる点は、新しい数が現在の数より大きい場合、新規要素の空間はそれぞれ、初期化されないまま、全てのビットをゼロにされます。AddUninitialized 関数と InsertUninitialized 関数と同様に、必要があれば新規要素を新規の空間に確実に適切に構築するようにします。

    SArr.SetNumUninitialized(3);
    new ((void*)(SArr.GetData() + 1)) S(5, (void*)0x12345678, 3.14);
    new ((void*)(SArr.GetData() + 2)) S(2, (void*)0x87654321, 2.72);
    // SArr == [
    //   { Int: 0, Ptr: nullptr,    Flt: 0.0f  },
    //   { Int: 5, Ptr: 0x12345678, Flt: 3.14f },
    //   { Int: 2, Ptr: 0x87654321, Flt: 2.72f }
    // ]

    SArr.SetNumZeroed(5);
    // SArr == [
    //   { Int: 0, Ptr: nullptr,    Flt: 0.0f  },
    //   { Int: 5, Ptr: 0x12345678, Flt: 3.14f },
    //   { Int: 2, Ptr: 0x87654321, Flt: 2.72f },
    //   { Int: 0, Ptr: nullptr,    Flt: 0.0f  },
    //   { Int: 0, Ptr: nullptr,    Flt: 0.0f  }
    // ]

関数の "Uninitialized" ファミリと "Zeroed" ファミリを注意して使用してください。コンストラクションが必要なメンバを含むために要素の型を修正した場合、あるいは有効なビット単位のゼロステートがない場合、配列要素は無効となり、動作が未定義になります。これらの関数は、FMatrix や FVector などの変更することがほぼない型の配列で使用する場合、非常に有用です。

その他

BulkSerialize 関数は、要素別のシリアル化ではなく raw バイトのブロックとして配列をシリアル化するために 演算子<< の代用として使用できるシリアル化関数です。ビルトイン型あるいはプレーンなデータ構造体など、要素の型がトリビアル型の場合、パフォーマンスが上がります。

CountBytes 関数と GetAllocatedSize 関数は、配列が使用中のメモリを概算します。これらの関数は一般的には統計情報の報告に使用されます。

Swap 関数と SwapMemory 関数は両方とも 2 つのインデックス番号を受け取り、これらのインデックス番号の要素の値をスワップします。これらは同等の関数ですが、Swap 関数がインデックス上で追加のエラーチェックを行い、インデックスが範囲外の場合アサートするという点が異なります。

タグ
このページは Unreal Engine の前のバージョン用です。現在リリースされている Unreal Engine 5.3 に対して更新は行われていません。
Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
キャンセル