UE5:Unreal Engineのポインタについてまとめた 中編
はじめに
UE5:Unreal Engineのポインタについてまとめた 前編 の続きです。
中編では、マネージドポインタについて記載します。
- UE5:Unreal Engineのポインタについてまとめた 前編
- UE5:Unreal Engineのポインタについてまとめた 中編
- UE5:Unreal Engineのポインタについてまとめた 後編
ポインタ一覧再掲
アンマネージドポインタ
| アンマネージド | 名前 | 補足 |
|---|---|---|
FCppStruct* Object; |
生ポインタ | 使うべきでない |
TUniquePtr<FCppStruct> Pointer; |
ユニークポインタ |
std::unique_ptrに該当 |
TSharedPtr<FCppStruct> Pointer; |
シェアードポインタ |
std::shared_ptrに該当 |
TWeakPtr<FCppStruct> Pointer; |
ウィークポインタ |
std::weak_ptrに該当 |
マネージドポインタ
| マネージド | 名前 | 補足 |
|---|---|---|
UPROPERTY() Ubject* Pointer{}; |
ハードオブジェクトポインタ | 古い書き方で今どき使わない。TObjectPtr使え |
UPROPERTY() TObjectPtr<UObject> Pointer; |
オブジェクトポインタ | ハードオブジェクトポインタの進化版。基本はこれ。 |
UPROPERTY() TSoftObjectPtr<UObject> Pointer; |
ソフトオブジェクトポインタ | UE5独自のやわらかい参照(後述) |
UPROPERTY() TWeakObjectPtr<UObject> Pointer; |
ウィークブジェクトポインタ |
TObjectPtrの所有権を持たない版 |
UPROPERTY() TStrongObjectPtr<UObject> Pointer; |
ストロングブジェクトポインタ |
TObjectPtrの所有権を持つ版 |
Ubject* Pointer{}; |
ワイルドポインタ | UPROPERTY()としてリフレクションシステムに辿られない野生のポインタ。ダングリングポインタになる。バグなので直せ。TObjectPtrにしろ |
特別なマネージドポインタ
| マネージド | 名前 | 補足 |
|---|---|---|
UPROPERTY() TScriptInterface<IMyInterface> Pointer; |
Nativeインターフェースへのポインタ | pure C++なinteface型を指すポインタ(後述) |
UPROPERTY() TSubClassOf<UMyBase> Pointer; |
UCLASSへのポインタ | BP含め任意のUMyBase派生型を指すポインタ(後述) |
UPROPERTY() TObjectPtr<AActor> Actor; |
AActorへのポインタ |
BP含め任意のAActor派生型を指すポインタ(後述) |
UPROPERTY() TObjectPtr<UActorComponent> Component; |
UActorComponentへのポインタ |
BP含め任意のUActorComponent派生型を指すポインタ(後述) |
UPROPERTY() TArray<TObjectPtr<UObject>> PointerArray; |
TArrayの中のポインタ |
コンテナの中のオブジェクトポインタ |
UPROPERTY() TMap<TObjectPtr<UObject>, TObjectPtr<UObject>> PointerMap; |
TMapの中のポインタ |
コンテナの中のオブジェクトポインタ |
UPROPERTY() TSet<TObjectPtr<UObject>> PointerSet; |
TSetの中のポインタ |
コンテナの中のオブジェクトポインタ |
詳解
Hard Object Pointer
UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()
UPROPERTY()
UObject* Pointer{};
}
古い形式のUObjectを指すポインタです。
使いません。必ずTObjectPtr<T>に乗り換えましょう。
UPROPERTY() を付与することで、リフレクションシステムに登録された生のポインタです。GCのマークアンドスイープで索引されるようなっています。
このポインタはUMyObjectクラスのインスタンスが、Pointerが指すUObjectを"保持"もしくは"依存"していることを示します。このポインタはGarbage Collectionシステムで走査対象になります。Garbage Collectionシステムはオブジェクトへの全てのハードポインタがnullptrになるか、オブジェクトが明示的にDestroy()がされない限り、そのオブジェクトを破棄しません。全てのハードポインタです。ハードオブジェクトポインタを含みます。
ハードオブジェクトポインタは static メンバー変数にできません。
コンパイルエラーになります。
UPROPERTY() static inline UObject* Pointer{};

Hard Object Pointerは使わない
もう使いません。必ずTObjectPtr<T>に乗り換えましょう。
大事なことなので重ねて言及しました。
Hard Object Pointerの使い方
使いませんが、一応使い方について述べます。
Hard Object Pointerのデリファレンス
デリファレンスするときは必ず IsValid()を用いて死活チェックしてから利用します。どこでMarkAsGarbageされるか分からないので、基本的にIsValid()した方が安全です。ゲーム内に限らず、エディター、テストコード、プロファイリングなどMarkAsGarbageで破棄したくなるタイミングはちらほら出てきますから、油断なりません。特にエディタ上でAssetをForce Deleteしたときや、レベルからアクターをDeleteしたときが危ないです。そのアセットへのハード参照はnullptrにセットされますが、Slateでキャプチャしたりすると、タイミングによっては怪しいことになります。ワイルドな参照はダングリングポインタになります。
if(IsValid(Pointer))
{
Pointer->DoSomething();
}
パフォーマンス稼ぎのためにIsValid()を外したくなるときがありますが、クラッシュすると時間を奪われるので個人的にお勧めしてません。
Object->MarkAsGarbage(); // どこかでゴミマークされたとする
//...
// まだ GCが走っていない状態では
// operator bool や == nullptr は
// MarkAsGarbageなオブジェクトに対して trueを返してしまう
if(Object != nullptr){} // 非nullptrだからゴミだけどアクセスしちゃうよ
if(Object){} // 非nullptrだからゴミだけどアクセスしちゃうよ
Hard Object Pointerの生成
NewObjectで作ります。
UObject* Object = NewObject<UObject>();
作りたての状態では、参照チェインに含まれていないので、どこかのUPROPERTYに参照させておかねばなりません。
UObject* Object = NewObject<UObject>();
this->Pointer = Object;
もしくは RootSet に明示的に登録するかです。
UObject* Object = NewObject<UObject>();
Object->AddToRoot();
Hard Object Pointerの破棄
マネージドなので破棄はGC回収を期待します。参照を外すときは素直にnullptrをセットします。GC回収を遅延させないように明示的にnullptr代入をすることが大事です。親(Outer)からthisへの参照が残っていたとしてもthisからPointerへの参照を明示的に外しておけばUObjectが1つ早期に回収されることが期待されます。
void ReleaseReference()
{
Pointer = nullptr;
}
自前で破棄したいときはMarkAsGarbage()か ConditionalBeginDestroy()を使います。
MarkAsGarbageは到達可能性に関わらず、次のGCタイミングで回収されます。通常通りのライフサイクルを通るので安心です。
void ReleaseReference()
{
// 推奨
if(IsValid(Pointer)
{
Pointer->RemoveFromRoot(); //root setから除いておかないとMarkAsGarbageできないよ
Pointer->MarkAsGarbage();
Pointer = nullptr;
}
// 超難しい
if(IsValid(Pointer)
{
Pointer->ConditionalBeginDestroy(); // BeginDestroyを呼ぶ
Pointer = nullptr;
}
}
ConditionalBeginDestroyはすぐさまBeginDestroyを呼び出そうとするので危険です。ライフサイクルを守らないので、UEのプロじゃない限り使わない方がいいです。
※ AActorと UActorComponentの破棄は別(後述)
Hard Object Pointerの注意点
アセットを消すとnullptrになる
UPROPERTY() UObject* が何らかのアセットを参照している場合、エディタ上でそのアセットを Force Deleteした場合 nullptrにセットされます。そのため、アセットを参照したい場合はnullptrになり得ることを前提に実装しなければなりません。Slateや UEditorSubsystem とかエディタ周りでアセット参照を握る場合は気を付けましょう。
TObjectPtr
UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<UObject> Pointer;
}
UObject派生型を指す基本のポインタです。参照を保持して参照チェインによるGCを妨げたい場合はUPROPERTY() TObjectPtrにするべきです。TObjectPtrはハード参照です。強い参照ではありません。ハード参照は参照を保持しますが、所有はしません。参照を絶対所有したい場合は TStrongObjectPtrを使います。逆にGCを妨げたくない場合は TWeakObjectPtrを使います。
TObjectPtr は生のアドレスではなく内部でオブジェクトへのハンドルとして持っています。パッケージビルドされた段階で生ポインタに変換されます。sizeof(TObjectPtr<T>)は生ポと同じです。なので値渡しでOKです。
TObjectPtr は Incremental Garbage Collection に対応しています。
ユーザーコード側で全てのハードオブジェクトポインタを TObjectPtrに置き換えられるなら Incremental GCを利用することができます。
また、TObjectPtrはCook時に遅延ロードに対応しているため、Cook時間の面において有利です。
TObjectPtrの使い方
TObjectPtrのデリファレンス
T*をデリファレンスしたいときは GetValid()を使って生ポにします。ifスコープの利用が賢明です。これは何回もoperator ->やoperator *を利用するのは無駄だからです。ResolveObjectHandleの呼び出しもその都度行われます。
// GetValid<T>は IsValidなら T* を,さもなければnullptrを返すtemplate関数
if(T* Ptr = GetValid(Pointer))
{
Ptr->DoSomething();
Ptr->DoFooBar();
//何回も operator->を呼び出すのは無駄です
// Pointer->DoSomething();
// Pointer->DoFooBar();
}
operator boolを利用してnullptr比較が可能です。
ただし、T*が生存していることに意味がある局面ではIsValidを使う方が賢明です。
if(Pointer){ /* pointerは非nullptr だが Garbageかはわからない*/ }
if(IsValid(Pointer)){ /* pointerは非nullptr かつ Garbageでない*/ }
TObjectPtrの生成
implicit に生ポから変換できます。operator=や普通にコンストラクタを使えばよいです。
UObject* RawPtr = NewObject<UObject>();
TObjectPtr<UObject> Obj0 = RawPtr;
TObjectPtr<UObject> Obj1(RawPtr); //生ポをはめることもできる
TObjectPtr<UObject> Obj2(nullptr); //明示的なnullptrコンストラクタもあるよ
TObjectPtr<UObject> Obj3(); //デフォルトコンストラクタはnullptr
普通はAActor等のコンストラクタ内でセットするか、エディタのDetailsビューからセットすると思います。
AMyActor::AMyActor()
{
Pointer = CreateDefaultSubObject<T>(TEXT("Hogehoge"));
}
TObjectPtrの破棄
nullptr設定するだけです。これはoperator=(TYPE_OF_NULLPTR)がちゃんと実装されているからです。
void ReleaseReference()
{
Pointer = nullptr;
}
Pointerが指すオブジェクトのGC回収を早めたいならnullptrをセットするのもありです。
TObjectPtrのキャスト
生ポインタと違って、TObjectPtrはテンプレートクラスです。なので型情報を保持しています。
そのためCastを型安全かつconstexprに行えます。つまりコンパイル時に間違ったキャストを弾いてくれるのです。すばら。
is-a関係であるかに興味がある局面ではIsA<T>を使用し、Castして利用したい場合はCast<T>を使用します。Cast<T>は TObjectPtr用のtemplate実装が存在するため型安全です。
// .h
UPROPERYT() TObjectPtr<UMyBase> Pointer{};
// .cpp
void Main()
{
// TObjectPtr<UMyBase>からTObjectPtr<UObject>へのアップキャスト
// 静的な型チェックは operator= でやってくれている
TObjectPtr<UObject> BasePtr = Pointer;
// その型の派生型であることに意味があるときはIsA<T>()
if(BasePtr.IsA<UMyObject>())
{
/*ログ出すときとか.型タグ使うときとか*/
}
// もっぱらコレ
if(UMyObject* Obj = Cast<UMyObject>(BasePtr))
{
Obj->DoSometing();
}
}
TObjectPtrの関数の引数・戻り値利用
TObjectPtrは生ポ、ハードオブジェクトポインタよりも価値が高いので、このままreturnしてよいです。生ポと TObjectPtrのサイズは同じなので TObjectPtrをreturnして問題ありません。生ポはスコープを超えて使うべきでないので、returnしないようにしましょう。
// .h
UPROPERYT() TObjectPtr<UMyBase> Pointer{};
// C++ Nativeの世界は生ポ滅ぶべし
TObjectPtr<UObject> UseObject_ForNative(TObjectPtr<UObject> Other)
{
return Pointer;
}
UFUNCTIONではTObjectPtrは使えないのでしゃあなしで生ポにします。
// UFunctionはしょうがないので生ポにする
UFUNCTION(BlueprintCallable)
UObject* UseObject_ForBP(UObject* Other) const
{
// なんか使う
// ...
return Pointer;
}
UFUNCTIONとそれ以外でいちいち関数分けてられない、というのはごもっともなので、ほどほどにバランスをとってください。
TObjectPtrの注意点
ハード参照はインスタンス生成時にアセットロードしてしまう
ハードオブジェクトポインタおよび、TObjectPtrはハード参照です。ハード参照はAssetを参照した場合、最初のクラスインスタンスが生成されたタイミング[1]で一緒にロードされます。
そのため大量のアセットをTObjectPtrで参照してしまうと、大きなスパイクが発生したり、ロード時間が伸びる問題が発生します。
これを回避するには TSoftObjectPtrを使用します。
インスタンス参照はTObjectPtr, アセット参照はTSoftObjectPtrを使うと覚えておくとよいでしょう。
TSoftObjectPtr
UPROPERTY() TSoftObjectPtr<UObject> Pointer;
ソフトオブジェクトポインタはアセットへのハード参照をしないポインタです。実行時に自動ではロードしません。中身はFSoftObjectPathとFWeakObjectPtrのペアです。つまりアセットへのパスとロードしたアセットへの弱参照を保持しています。
ただのFStringと異なりリダイレクタやエディタ統合に対応しています。型Tを持っているため、特定のアセットしか設定できないようになっています。Detailsビューがちゃんと対応されており、ハード参照とほぼ変わらずにアセットを設定できます。
TSoftObjectPtrの使い方
TSoftObjectPtrの生成
普通はUPROPERTY(EditDefaultsOnly)にしてエディターから設定します。
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> Mesh;
C++でハードコードするときはパスを指定します。
void Main()
{
TSoftObjectPtr<UStaticMesh> Mesh = FSoftObjectPath("/Game/Path/To/Mesh");
}
TSoftObjectPtrの破棄
パスと弱参照しかもっていないので別に破棄する必要はありませんが、Resetで同じインスタンスを使いまわせます。
void Main()
{
TSoftObjectPtr<UStaticMesh> Mesh = FSoftObjectPath("/Game/Path/To/Mesh");
Mesh.Reset();
}
TSoftObjectPtrのロード
同期ロードはメンバメソッドを直接叩きます。ロード済みのインスタンス参照は自身で保持する必要があります。TSoftObjectPtrはロードしたものへの弱参照しかもっていないので、GC回収されちゃいます。
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> Mesh;
UPROPERTY()
TObjectPtr<UStaticMesh> LoadedMesh;
void Main()
{
TSoftObjectPtr<UStaticMesh> MeshAsset = FSoftObjectPath("/Game/Path/To/Mesh");
UStaticMesh* Mesh = MeshAsset.LoadSynchronous();
// ここでGCされるとMeshは回収されてしまう
// なので UPRRPERTY()なLoadedMeshに保持しておく
LoadedMesh = Mesh;
}
TSoftObjectPtrの非同期ロード
非同期ロードは公式ドキュメントの通りです。
アセットの非同期ロード
FStreamableManager を使って RequestAsyncLoadを呼びます。ロード完了のコールバックで受け取ります。
早くコルーチンかTFuture対応来てくれぇ!
TSoftObjectPtrの死活チェック
3種類あります。これは3状態を識別するものです。それぞれの状態は排他です。
-
IsNull()- 有効なパスを指していない -
IsPending()- 有効なパスを指しているが、ロードしていない、もしくはロード中、もしくはGC回収済み -
IsValid()- 有効なパスを指しており、ロード済み
有効なパスを指していないがロード済みという状態はありえません。
TSoftObjectPtr Ptr = FSoftObjectPath("Game/Path/To/Asset");
if(Ptr.IsNull()){} // パスが間違っている or パスが空文字
if(Ptr.IsPending(){} // パスは合っている. 未ロード or ロード中
if(Ptr.IsValid()){} // ロード完了
UPROPERTY()なSoftObjectPtrをちゃんとEditorから設定できているならばIsPending()となります。IsValid()は内部のWeakObjectPtrが活きている間はtrueを返すということなので、将来的にGC回収されたらIsPending()へと戻ります。ResetWeakPtr()でIsValid()からIsPending()へ明示的に戻せますが、普通そんなことしないでしょう。
if(Ptr.IsValid())
{
Ptr.ResetWeakPtr(); //内部の弱参照をResetしてIsPendingに戻す.
check(Ptr.IsPending());
//もいっかいロードしたら別インスタンスが得られる
UObject* Loaded2 = Ptr.LoadSynchronus();
}
IsValid()は内部でdynamic_castを用いており重めです。ロード完了したかどうかはロード済みのアセットの有無で確認した方が賢明でしょう。
UPROEPRTY() TSoftObjectPath<UObject> SoftObjectPtr;
UPROPERTY() TObjectPtr<UObject> Loaded;
void BeginPlay()
{
this->Loaded = SoftObjectPtr.LoadSynchronus();
if(SoftObjectPtr.IsValid()){} // これは少し重め
if(IsValid(Loaded)){} // ロード済みかどうかはロード済みObjectの死活チェックで良いよね
}
TWeakObjectPtr
UObjectのGCを妨げることがない弱参照です。
UPROPERTY()
TWeakObjectPtr<UObject> WeakObject;
中身はObjectのIndexとシリアルNoを持つ8byteの値型です。T* 自体は持っておらずポインタではないのですが、ポインタとして振る舞います。
GCを妨げないということで、有れば使い、なかったら使わないというようなニーズを満たすときに重宝します。
TWeakObjectPtrの使い方
TWeakObjectPtrの生成
UObject*や TObjectPtr、TStrongObjectPtrから暗黙的に変換できます。
TObjectPtr<UObject> Object = NewObject<UObject>();
TWeakObjectPtr<UObject> Weak = Object;
明示的に作りたい場合は MakeWeakObjectPtr<T>を使います。
生ポもTObjectPtrも両方対応しています。
TObjectPtr<UObject> Object = NewObject<UObject>();
UObject* RawPtr = NewObject<UObject>();
TWeakObjectPtr<UObject> WeakFromTObj = MakeWeakObjectPtr(Object);
TWeakObjectPtr<UObject> WeakFromRaw = MakeWeakObjectPtr(RawPtr);
TWeakObjectPtrの破棄
Resetを使うか、nullptrをセットします。
TObjectPtr<UObject> Object = NewObject<UObject>();
TWeakObjectPtr<UObject> Weak = Object;
Weak.Reset();
Weak = nullptr;
TWeakObjectPtrの死活チェック
活きているかどうかに興味がある局面においてはIsValid()を使用します。
if(Weak.IsValid())
{
UE_LOG(LogTemp, Display, TEXT("まだ活きている"));
}
else
{
UE_LOG(LogTemp, Display, TEXT("もう死んだか、最初からnullptrか、Staleした"));
}
IsValid には引数が2個ついています。bEvenIfPendingKill と bThreadSafeTestです。
bEvenIfPendingKill=trueはガベージマーク済みなゴミオブジェクトを指していてもtrueを返します。この段階では有効なアドレスを指しているため、デリファレンスしてもアクセス違反にはなりません。よくわからないならfalseにしてください。
bThreadSafeTest=trueはUObjectのルートからの到達可能性チェックを行わずGObjectArray[i]がnullptrかどうかだけをチェックします。GCのマークフェーズの間でもnullptrかチェックしたいときに使います。
どちらも普通のユーザーは使いません。
constexpr bool bEvenIfPendingKill = true;
constexpr bool bThreadSafeTest = true;
if(Weak.IsValid(bEvenIfPendingKill, bThreadSafeTest))
{
// ゴミかもしれないが、メモリ領域は活きている
}
ワーカースレッドから行える最強の死活チェックはPinです。
if(TStringObjectPtr Strong = Weak.Pin())
{
// スレッドセーフにまだ活きていることが確実
}
else
{
// すでに死んでいる
}
TWeakObjectPtrのデリファレンス
弱参照なのでいつ死ぬかわかりません。そのため使用する度にチェックする必要があります。
基本的には ifスコープで PinもしくはGetを使います。
ゲームスレッドで利用する場合は Getで問題ありません。
if(UObject* Ptr = Weak.Get())
{
Ptr->DoSomething();
}
Getでは中身が活きているかどうかをチェックした上で、有効なときのみ有効なアドレスを返し、死んでいる場合はnullptrを返します。
Get(bEvenIfPendingKill)ではガベージマーク済みなオブジェクトであっても有効なアドレスを返します。通常使いません。
TObjectPtr<UObject> Object = NewObject<UObject>();
TWeakObjectPtr<UObject> Weak = Object;
Object->MarkAsGarbage();
if(UObject* Ptr = Weak.Get(/*bEvenIfPendingKill*/true))
{
Ptr->DoSomething(); //まだGCされてないからぎりぎりセーフ
}
Pinを使うと弱参照から強参照に変換してGCを確実に妨げることができます。Pinはスレッドセーフです。
内部では FGCScopeGuardというclassを用いてGCとの排他制御を行っています。もしPin止めが間に合わなかったときはnullptrが帰ります。
// Slateの実行コンテキスト等において使う
if(TStrongObjectPtr<UObject> Ptr = Weak.Pin())
{
Ptr->DoSometing(); //絶対GCされてない
}
Pin(bEvenIfPendingKill)ではガベージマーク済みなオブジェクトであっても有効なアドレスを返します。
こちらは強参照であるため例えガベージマーク済みであっても、強参照が活きている限りGCを妨げます。通常使いません。
Pinは スレッドセーフだがT*はスレッドセーフではない
WeakObjectPtr::Pinはスレッドセーフです。GCに対してLockを取りつつ確実にIsValidな TStrongObjectPtrを取得できます。
ですが、肝心のT*に関してはそれがスレッドセーフかどうかは実装によります。ほとんどのUObject派生型はスレッドセーフじゃありません。
if(T* Ptr = Weak.Pin())
{
Ptr->DoSometing_ConCurrent(); //自前でスレッドセーフな関数を実装するべし
int Value = Ptr->GetValue_ConCurrent(); //std::atomicやCriticalSection等でスレッドセーフにするべし
}
Slateの世界はUObject管理外なので、ラムダキャプチャしたUObjectがGCされてしまわないようにWeakObjectPtrで参照を引き回したり、StrongObjectPtrで確実に所有権を握る必要がでてきます。Unreal Editor拡張を実装するにあたってSlateは避けて通れず、またUObjectも触らないわけにはいかないので、マネージドポインタを触る場合は特に気を付ける必要があります。
TStrongObjectPtr
マジの強いオブジェクト参照です。GCを必ず妨げます。RefCountedフラグを立てて、明示的な参照カウント方式に切り替えます。
たとえUnreachableフラグが立っている到達不可能なオブジェクトであってもGCを妨げます。
ゲーム内ではTObjectPtrで十分なので登場頻度は低いと思います。
TWeakObjectPtrのPinから得ることが大半でしょう。
もっぱら Editor拡張で使われます。
典型例はアセットのコンバートやバリデーション等です。処理中のアセットがGCされたり削除されると例外だすので、事前に強い参照を保持してから望むというものです。
TStrongObjectPtrの使い方
TStrongObjectPtrの生成
WeakObjectPtr<T>::Pin から貰います。もしくは明示的にコンストラクタを呼びます。
TObjectPtr<UObject> Obj = NewObject<UObject>();
TWeakObjectPtr<UObject> Weak = Obj;
TStrongObjectPtr<UObject> StrongFromObject(Obj);
TStrongObjectPtr<UObject> StrongFromWeak = Weak.Pin();
TStrongObjectPtrの破棄
Reset()を呼び出すか, nullptrをセットします。
参照カウント式なので確実に破棄しましょう。デストラクタで自動的にResetされるためで一時変数などは大丈夫でしょう。
メンバ変数に持つ場合は寿命が伸びる可能性があるので適切なライフスコープで破棄してください。
TStrongObjectPtr<UObject> Strong = Weak.Pin();
Strong.Reset(); // 参照カウントを減らし、内部ポインタをnullptrにする
Strong = nullptr; // 参照カウントを減らし、内部ポインタをnullptrにする
全てのTStrongObjectPtrから解放されたオブジェクトは従来のマークアンドスイープ方式でGCされます。
TStrongObjectPtrのデリファレンス
Get()で取得します。強参照であるので、非nullptrであるのならば確実に活きています。
そのためoperator->や operator*を直接使っても問題ありません。覚えることを減らすためほかのポインタ同様Getを使えばいいと思います。
TStrongObjectPtr<UObject> Strong = Weak.Pin();
if(UObject* Ptr = Strong.Get())
{
Ptr->DoSomething();
}
TStrongObjectPtrの死活チェック
IsValid()でチェックします。強参照であるので、非nullptrであるのならば確実に活きています。
Pin止めが間に合わなかったときはnullptrが返るので必ず一度はチェックしましょう。
チェック済みのStringObjectPtrは ずっと非nullptrなので以降のチェックは省けます。
TStrongObjectPtr<UObject> Strong = Weak.Pin();
if(Strong.IsValid())
{
// ピン止めが間に合った
}
else
{
// ピン止めが間に合わなかった or Weakが最初からnullptrだった
}
TStrongObjectPtr更に詳しく
内部的に UObject::AddRef/ReleaseRef()を呼び出します。これにより、EInternalObjectFlags::RefCountedフラグが立ちます。
このフラグは EInternalObjectFlags_GarbageCollectionKeepFlags に含まれているため、GCのスイープフェーズで回収されません。
Wild Pointer (Raw Pointer)
UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()
UObject* Pointer {nullptr}; // UPROPERTY()がない!
}
UPROPERTY()がついていないため、リフレクションシステムから辿ることができない野生のポインタです。このポインタが指すオブジェクトはIsValidなのかGC回収済みなのか全くわかりません。
Wild Pointer の使い方
ありません。使ってはいけません。ダンリングポインタになるのでクラッシュするか、最悪生動きします。
Wild Pointer の死活チェック
できません。IsValid()ではチェックできません。IsValidLowLevel()でもチェックできません。
IsValidLowLevelは 活きているかのチェックではなく、ダングリングポインタの恐れがあるかのチェックに使用します。デバッグ用です。誤判定する可能性があるため、デリファレンスしてアクセスしてはいけません。
if(!IsValidLowLevel(Object))
{
// ログを出すなどの為に使用する
UE_LOG(LogTemp, Error, TEXT("ダングリングポインタかもしれない!!!!"));
// これはダメ↓ GetNameの呼び出しでクラッシュする可能性が高い
// UE_LOG(LogTemp, Error, TEXT("%s"), Obj->GetName());
}
Wild Pointer 何故ダメか
このポインタを使ったときの最悪のシナリオとしては、GCに回収された無効オブジェクトが再利用されて有効な別のUObject派生型へと転生して使用されることです。
低い可能性ではありますが、同一アドレスに別のUObjectが生まれる可能性があるのです。
例:
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
void BeginPlay()
{
WildPrimitive = NewObject<PrimitiveComponent>();
// このスコープ内においてのみ活きていることが保障できる
}
void Tick()
{
// この時点で WildPrimitiveはもう死んでいる可能性がある
// ...
if(IsValid(WildPrimitive)) // チェックできてない意味のないチェック
{
}
}
void EndPlay( ... )
{
if(IsValid(WildPrimitive)) // チェックできてない意味のないチェック
{
WildPrimitive->MarkAsGarbage();// クラッシュの危険
WildPrimitive = nullptr;
}
}
UPrimitiveComponent* WildPrimitive {nullptr};
}
この例ではWildPrimitiveにPrimitiveComponentを差しました。通常 PrimitiveComponent型であることを期待するはずです。プログラム上でも合っています。
ただし、GCされた場合、UPROPERTY()ではないのでnullptrに書き戻されることもありません。相変わらずポインタは無効領域を差しています。
このとき、デリファレンスすると割り当てられたメモリ領域外にアクセスして未定義動作を起こします。運がよければクラッシュして問題が顕在化します。
運が悪いと、別のNewObject<T>が実行され、低い確率でたまたま同アドレスに新たなTが割り当てられたとします。TもまたUObject派生型であるというせいで、UObjectBase::InternalIndexなどにはギリギリアクセスできてしまいます。そのためIsValidLowLevelなどは転生したTに対してtrueとなるときがあります。が、当然自身はPrimitiveComponentであると思い込んでいるので、そのsizeでアクセスするし、vtableもそれだと思い込んでいます。謎領域をreadしながら生動きする可能性があります。デバッグビルドだとアクセス違反例外が出ると思います。最適化により例外チェック機構が無効化されていると出ないこともあります。
いずれにせよ、生動きにより問題が顕在化しない、もしくは深刻化する可能性が大いにあります。
生ポ絶対ダメ!
つづく
予想通り書くことが多かったので後編に続きます。
-
Class Default Objectのときはロードされません ↩︎
Discussion