はじめに
UEでよく使われる動的配列(TArray)を使う上で気をつけたほうがいいことなどについて話します。
要素を追加するときの再配置を意識しよう
要素を一つだけ追加した場合、メモリに確保される要素数はいくつでしょうか?
TArray<int32> Array;
Array.Emplace(1);
UE_LOG(LogTemp, Log, TEXT("size: %d"), Array.Max());
正解は4です。
TArray<int32> Array;
Array.Emplace(1);
Array.Emplace(1);
Array.Emplace(1);
Array.Emplace(1);
Array.Emplace(1);
UE_LOG(LogTemp, Log, TEXT("size: %d"), Array.Max());
5つ追加したらどうなるでしょうか?22でした。
ちょうど5個目の要素を追加した時に再配置が発生し、領域が拡張されます。
再配置は比較的重たい処理なのでなるべく減らすことを考えましょう。
もし追加する要素数が決まっているのであれば、事前にReserve()で予約してあげると1回ですみます。
また、配列の中の要素のポインタを変数などに保持して使うのも危険です。
なぜなら再配置がおきるとアドレスが変わってしまうためです。
TArray<FString> Array = { TEXT("Test0") };
// 危険!
FString* Text = &Array[0];
Array.Emplace(TEXT("Test1"));
Array.Emplace(TEXT("Test2"));
Array.Emplace(TEXT("Test3"));
Array.Emplace(TEXT("Test4")); // 再配置がおきてTextポインタは無効な領域を指してしまいます
要素の追加はAddよりEmplace?
UEのドキュメントを見ると「Emplace は常に Add より効率的です」と書かれているのでEmplaceを使いましょう。
https://docs.unrealengine.com/4.26/ja/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/TArrays/
なんで?
FORCEINLINE SizeType Add(const ElementType& Item)
{
CheckAddress(&Item);
return Emplace(Item);
}
Addの関数を見ると中でEmplaceを呼んでます。最適化されたらどっちも一緒な気がします。
なのでどっちでもいいんじゃないかな?
forループ中で要素を削除するのは危険
TArray<int32> Array = { 0, 1, 2, 3, 4 };
for(int32 i : Array)
{
if (i == 2)
{
// 危険!
Array.Remove(i);
}
}
foreachループは内部でイテレータを持っています。イテレーターは今処理している要素のindexをもっています。
今、イテレーターのindexは2で、2の要素を指している状態です。
この状態で2の要素を削除するとどうなるでしょうか?
index | 内容 |
---|---|
0 | 0 |
1 | 1 |
2 | 3 |
3 | 4 |
イテレーター(index:2)の次の要素は3ではなく4になります。
3をすっとばしてしまいますね。
なので特定の要素を削除したいときはRemoveAllを使いましょう。
Array.RemoveAll([](int32 i) { return i == 2; });
削除するときのコストも考えよう
配列の途中にある要素を削除すると後ろの要素が前に詰められます。これにはオーバーヘッドが発生してしまいます。
もし配列の順番に意味がないのであれば以下の関数を使うようにしましょう。
- RemoveAllSwap
- RemoveAtSwap
- RemoveSingleSwap
- RemoveSwap
これらの関数は順序が保証されない代わりにオーバーヘッドを回避することができます。
調べてないけど、空いたところにお尻の要素を詰めてるんだと思います。
便利な機能紹介
- FindByPredicate
- ContainsByPredicate
特定の条件に合致するものを検索
int32* item = Array.FindByPredicate([](int32 i) { return i == 2; });
-
AddUnique
要素がなければ追加 -
TMap::FindOrAdd
配列じゃないけど、検索時に無かったら追加してくれるので存在しなかった場合の処理を書かなくていい。
最後に
便利な配列ですが、内部でどういう挙動をしているか把握して無駄のないコードが書けるといいですね。
for文で手続き的にガリガリ書くのもいいですが、RemoveAllSwapやFindByPredicateなどでラムダ式を使えばもっとスッキリ書けて読みやすいコードになると思います。