UE5:Unreal Engineのポインタについてまとめた 中編

2025/03/12に公開

はじめに

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{};
static member

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のプロじゃない限り使わない方がいいです。
AActorUActorComponentの破棄は別(後述)

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のサイズは同じなので TObjectPtrreturnして問題ありません。生ポはスコープを超えて使うべきでないので、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;

ソフトオブジェクトポインタはアセットへのハード参照をしないポインタです。実行時に自動ではロードしません。中身はFSoftObjectPathFWeakObjectPtrのペアです。つまりアセットへのパスとロードしたアセットへの弱参照を保持しています。

ただの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状態を識別するものです。それぞれの状態は排他です。

  1. IsNull() - 有効なパスを指していない
  2. IsPending() - 有効なパスを指しているが、ロードしていない、もしくはロード中、もしくはGC回収済み
  3. 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*TObjectPtrTStrongObjectPtrから暗黙的に変換できます。

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個ついています。bEvenIfPendingKillbThreadSafeTestです。

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を取りつつ確実にIsValidTStrongObjectPtrを取得できます。

ですが、肝心の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で十分なので登場頻度は低いと思います。
TWeakObjectPtrPinから得ることが大半でしょう。

もっぱら 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};
}

この例ではWildPrimitivePrimitiveComponentを差しました。通常 PrimitiveComponent型であることを期待するはずです。プログラム上でも合っています。
ただし、GCされた場合、UPROPERTY()ではないのでnullptrに書き戻されることもありません。相変わらずポインタは無効領域を差しています。
このとき、デリファレンスすると割り当てられたメモリ領域外にアクセスして未定義動作を起こします。運がよければクラッシュして問題が顕在化します。

運が悪いと、別のNewObject<T>が実行され、低い確率でたまたま同アドレスに新たなTが割り当てられたとします。TもまたUObject派生型であるというせいで、UObjectBase::InternalIndexなどにはギリギリアクセスできてしまいます。そのためIsValidLowLevelなどは転生したTに対してtrueとなるときがあります。が、当然自身はPrimitiveComponentであると思い込んでいるので、そのsizeでアクセスするし、vtableもそれだと思い込んでいます。謎領域をreadしながら生動きする可能性があります。デバッグビルドだとアクセス違反例外が出ると思います。最適化により例外チェック機構が無効化されていると出ないこともあります。
いずれにせよ、生動きにより問題が顕在化しない、もしくは深刻化する可能性が大いにあります。

生ポ絶対ダメ!

つづく

予想通り書くことが多かったので後編に続きます。

脚注
  1. Class Default Object のときはロードされません ↩︎

GitHubで編集を提案

Discussion