関連ブログ
- [UE4][UE5]開発環境の容量を少しでも減らす 2024.08.14UE
- [UE5] PushModel型のReplicationを使い、ネットワーク最適化を図る 2024.05.29UE
- [UE5]マテリアルでメッシュをスケールする方法 2024.01.17UE
CATEGORY
2014.07.17UE4UE/ C++
改訂バージョン: Unreal Engine 4.21 |
今回はプログラマ向けのお話です。
TArrayクラスの実装や、C++からTArrayを使用する際に覚えておきたい注意点についてご紹介したいと思います。
TArrayはUE4の提供する可変長配列クラスです。
機能や用途は、std::vectorに近いものになります。
UPROPERTYとして、シリアライズやブループリントからのアクセスの対象にすることが可能です。
可変長配列への要素の追加には、Add(), Append(), Insert()といった関数を使用します。
要素の削除には、RemoveAt()などの関数を使用します。
TArrayは、メンバ変数として ArrayNum, ArrayMax という2つの整数を持っています。
ArrayNumは現在の要素数、ArrayMaxは確保された要素数です。
ArrayMaxというメンバが存在することからも分かるように、TArrayは、必要な数以上のメモリ領域を予め確保しておくことが出来ます。
これらの値は、Num(), Max()という関数で、外部からも取得することが出来ます。
使用する要素数が決まっている場合は、Reserve()で予めメモリを確保しておいてから要素を追加すると、効率的に使用出来ます。
TArrayが要素を保持するために確保するメモリ領域は、必ず連続しており、List構造のように要素を辿ることなく、インデックスから直接アクセスすることが可能になっています。
しかしこれは、Insert()によって若いインデックスに要素を追加したり、RemoveAt()でインデックスの若い要素を削除したりすると、その分のメモリコピーが行われていることを意味します。
例えば下記のコードは、非常に効率の悪いリセット方法となります。
インデックス[0]の要素を削除する度、それ以外の全ての要素が前詰めするためにコピーされます。
1 2 3 4 |
while(MyArray.Num()) { MyArray.RemoveAt(0); } |
ArrayMax以上の要素数が追加され、予め確保されていたメモリが不足した場合、メモリの再確保が行われます。
また、RemoveAt()等によって要素が削除される場合にも、効率化のためにより小さいサイズで再確保される場合があります。(RemoveAt()の場合、引数bAllowShrinkingにfalseを指定することで、明示的に再確保させないことも可能です)
この際に、再確保された領域の先頭アドレスが既存のアドレスと一致するかどうかは保証されません。
例えば、次のコードは非常に危険なケースです。
“C”が格納された[2]のアドレスを変数に保持していますが、RemoveAt()を行った時点でインデックスを詰めるようにメモリコピーされ、pTextCの指すアドレスには有効なデータが残っていません。
1 2 3 4 |
TArray MyArray; MyArray.Add(TEXT("A")); MyArray.Add(TEXT("B")); MyArray.Add(TEXT("C")); |
Print(MyArray[0]); // A
Print(MyArray[1]); // B
Print(MyArray[2]); // C
FString* pTextC = &MyArray[2];
明示的に指示しない限り、pTextCの指す領域がまだMyArrayによって確保されているか、解放済みとなっているかの保証はありません。
エディタ上では動くのに、パッケージ化するとクラッシュする、というようなことも起こり得ます。
bool, int, float等のサイズの小さな基本型の場合は、問題になることは少ないと思われます。
FString, FVector のようなサイズの小さな構造体の場合も、基本型と同様に問題になることは少ないと思われます。
しかし、自分で定義したサイズの大きな構造体の場合は、メモリコピーや再確保の負荷が問題になる可能性があります。
要素数を事前に予測して確保しておく、明示的に再確保しないようにする、インデックス詰めによるコピーが極力発生しないように運用する、といった注意が必要になる場合があるかもしれません。
TArrayは要素毎にサイズが違う場合には対応出来ません。また、メモリコピーによってデータの移動が行われるため、コンストラクタやデストラクタも呼び出されません。
TArrayに追加されるのはあくまでAdd()に渡した要素のコピーです。別の場所で動的確保した要素の参照を渡しても、TArrayはその解放を行いません。
UObjectやAActorなど、継承しての運用を前提としたクラスは、そのままTArrayを使用するのではなく、そのクラスのポインタ型でTArrayを使用します。
下記の例はNGです。
1 |
TArray MyObjects; |
下記のように使用します。
1 |
TArray<UObject*> MyObjects; |
これならば、要素の実体はTArrayとは別の場所に確保されるので、知らないうちにアドレスが変更される、ということも無く安心です。
TArrayがUPROPERTYであれば、参照を保持してくれるため、ガベージコレクタの対象にもなりません。
ブループリントからObject型のArrayを使用する際も、この方法が用いられていると思われます。
※ここでの構造体型という言葉は、あくまで、サイズの固定されたデータを保持することを目的とした型のことを指します。structで定義していれば大丈夫という訳ではありません。
今回は、開発中にハマったケースからの教訓をまとめてみました。
長々と、文字ばかりの記事を最後まで読んで頂き、ありがとうございました。