📝

[UE5 C++] UPROPERTYとGCについて整理

2025/02/28に公開

1. はじめに

Unreal Engineにはガベージコレクタがあります。

UEが自動で不要なオブジェクトをメモリから破棄してくれるという便利で有難い仕組みです。ですが、UE上でコードを書く際にはこの辺りをしっかりと把握していないと、ロジック外での意図しないエラーが発生してしまいます。

GCに意図せず回収されないように 「UObjectはUPROPERTYマクロを使用して保持しておけばよい!」 というのは分かっていますが、細かいシチュエーションで「これって大丈夫だったっけ」となることがあります。個人的には構造体が絡んだ時に多かった気がします。C++ではないUE仕様の部分はなんかブラックボックス感がありさらっとドキュメントを読んだだけだと理解が甘いなと感じています。

「これなんだっけ」となるのは理解が浅いということであり、そのまま書くのは怖いですね!ちゃんと理解をせずに扱うのが怖かったので、これを機に自分の理解があやしい部分の整理をしておこうと思った次第です。

まず各機能について改めてドキュメントを確認し、その上で個人的な不明点を挙げ、確認していきたいと思います。基本的には根拠の裏どりが面倒なので公式ドキュメントのみを参考にします。分からない部分はエンジンのコードを読んだり、動作チェックしたりで確認していきます。

2. まずはこの周辺の機能についてドキュメント確認

■ 2.1. ガベージコレクタについて確認する

まずは本題のガベージコレクタについてです。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/unreal-object-handling-in-unreal-engine?application_version=5.5#ガベージコレクション

Unreal は、参照されなくなった、または破棄と明示的にフラグ付けされた UObjects を定期的にクリーンアップするガーベジ コレクション スキームを実装しています。どの UObjects がまだ使用中であるか、どのオブジェクトが孤立しているかを判断するための参照グラフをビルドします。このグラフの元は、「root set」と指定されている一連の UObjects です。どの UObject でもルートセットに追加することができます。ガーベジ コレクションが実施されると、既知の UObject 参照のツリーを「root set」から開始して検索することで、参照されているすべての UObjects を追跡することができます。参照されていない UObjects、つまりツリー検索で見つからなかったものは、不要とみなされ、取り除かれます。

なんとなくは分かりますが、私の読解力が弱いのか、ルートセット周りがいまいち日本語訳がよく分からないので英語の方で確認しておきます。

The engine builds a reference graph to determine which UObjects are still in use and which ones are orphaned. At the root of this graph is a set of UObjects designated as the "root set". Any UObject can be added to the root set.

「まだ使われているUObject」と「親がなく孤立しているUObject」を特定するために、エンジンは参照グラフを作成します。このグラフの根はUObjectの集合です。UObjectの集合は the "root set" と名付けます。 どんなUObjectでも the root set に加えることが可能です。

修飾とか省いて見ると root is a set で「グラフの根」自体が「集合1つ」のようです。そして以降はこれを「ルートセット」と言いますよ、とのことです。また list ではなく set なので順序は重要ではないですね。構造は木構造のようですが、根の集合ではなく、集合が根...英語は苦手なのでいまいちピンと来ません。

木構造の定義をWikiで見てみると

ノードは高々1個の親ノードを持つ。

とあり、木構造は親ノードが複数は存在しないはずです。高々というのは数学で使われる用語で node \leqq 1 です。「 root set 」という名前からして根の集合であり、「木構造がたくさんあり、その根の参照を保持してますよー」という意味でしょうか。ひとまずそう理解しておきます。

こんな感じでしょうか?

ルートセットを念頭に、内容をまとめます。
エンジンは「UObjectの木構造」を構築しており、その木構造の根を「ルートセット」として保持しているそうです。この木構造に含まれている限りは「使用されている」と判断されGC対象にならないようです。おそらくDestroyする際にはこの木構造から外され、木構造から独立した部分木はGCに回収されるのだと思われます。

とは言え、この木構造は内部的な実装のデータ構造の話であり、意識したことはないかと思います。私たちが明示的に木構造の操作をして、UObjectを追加するようなこともありません。(アタッチしたり親子関係を構築はしますが)

ドキュメントには

通常はガーベジ コレクションの対象から外したいオブジェクトに対する UPROPERTY 参照を保持しておく必要があります。多くの場合、アクタはとそのコンポーネントは、この例外です。アクタは通常、ルートセットにリンクバックするオブジェクトによって参照されるからです。

と書いてあります。私たちは UPROPERTY を使うことでGCに回収されるのを回避をします。 UPROPERTY マクロを付けることでエンジンがリフレクション操作ができるようになるとのことなので、明記はされていませんがUObject内の UPROPERTY を辿ることで木構造として検索しているのでしょうか。

アクターに関しては、このドキュメントの内容だけでは詳細までは分かりませんが、どこかでルートセットと繋がっており、私たちが UPROPERTY で参照せずともGCに回収されないようですね。レベルに配置されているアクターが勝手に消えていったら困るので別途管理されているのは納得できます。

◆ 正しく生成し、ちゃんとUnrealEngineに管理してもらう

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/objects-in-unreal-engine

UObject では new 演算子を使用すべきではありません。すべての UObject のメモリは Unreal Engine によって管理されていて、ガベージ コレクションが行われます。new または delete 演算子を使用してメモリを手動で管理すると、メモリ内のデータが破損することがあります。

試したことないですが、C++のclassなのでそのまま new することも可能かもしれません。エラーを出されそうな気もしますが。しかし、注意書きにあるように、UObjectを適切な生成方法で生成しなければUnrealEngineには認知してもらえず、適切にガベージコレクタにも回収してもらえません。UObjectであれば問答無用でUnrealEngineに管理してもらえるわけではないようです。生成方法には注意です。

加えて、「new/deleteで手動管理すると、メモリ内のデータが破損するかも」とも書かれています。ここで言うメモリとは、そのオブジェクト内のメモリを指しているのか、ゲーム全体のメモリを指しているのか、なぜ壊れるのかがいまいち不明瞭なのでどちらかよく分かりませんが不穏な雰囲気があります。やめましょう。

ガベージコレクションについては以上です。

まとめると
UPROPERTY マクロを辿って木構造を構築する。ガベージコレクタは定期的な検査で木構造に属さないUObjectを発見し、解放する。 UPROPERTY を使わずにポインタで保持するだけだと木構造に追加されずにGCのタイミングで破棄されてしまう。アクタは例外でレベルに配置されている限りはGCされない。」
といった感じでしょうか。

■ 2.2. UPROPERTYについて確認する

既に「 UPROPERTY マクロを使ってGCに回収されるのを防ぐ」という話はしてきました。改めてドキュメントも確認しておきます。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/unreal-object-handling-in-unreal-engine

によると

クラス、プロパティ、関数を適切なマクロでマーク付けすると、それぞれ UClasses、UProperties、UFunctions になります。その結果、Unreal Engine がこれらにアクセスできるようになり、たくさんの内部処理機能を実装することができます。

とのことです。

コードは次のように書きます。

UPROPERTY()
TObjectPtr<UMyObject> MyObject;

TObjectPtr<> については後ほど述べます。普通に使っている分にはただのポインタと同じ振る舞いのようです。ひとまずスルーしてください。

UPROPERTY といったマクロを使うことで、 Unreal Engine がそれらにアクセスすることができるようになるそうです。「このクラスにはどんな変数があるのか」といったプログラム自体の情報を読み取ったりできるいわゆるリフレクションですね。C#などにはありますが、C++にはないようです。代わりに Unreal Engine では独自のリフレクションシステムを実装しているそうです。

Unreal Engine のリフレクションシステムに関するドキュメントは以下になります。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/reflection-system-in-unreal-engine

これによると

UCLASS マクロを使用して、UObject から派生したクラスにタグ付けすると、UObject の処理システム が、その派生クラスを認識できるようになります。

UFUNCTION マクロと UPROPERTY マクロは、UE に新しいクラス、関数、変数を認識させます。これらのマクロは、エンジンによってガベージ コレクションされます。

とのことです。

エンジンに変数を認識させ、ガベージコレクションシステムで扱えるようにするようです。

「これらのマクロは、エンジンによってガベージ コレクションされます。」!?

...この文章を読んでいて「UPROPERTYマクロはエンジンにGCされちゃうの!?」と最初思いましたが、さきほどのGCの説明にもあったように

ガーベジ コレクションが実施されると、既知の UObject 参照のツリーを「root set」から開始して検索することで、参照されているすべての UObjects を追跡することができます。

と書いてあり、ガベージコレクタによるチェック自体を「ガベージコレクションする」というのですね。チェックをして「破棄をすること」を「ガベージコレクションする」だと勘違いしていました。

ここまでで分かった UPROPERTY の機能は

  • Unreal Engine のリフレクションシステムに変数を認識してもらえる
  • 認識してもらうことでGC管理され、参照が生きていれば残り、参照が途絶えればGCして適切に消してくれる

です。

そして UPROPERTY マクロには「GCして適切に消してくれる」という部分に関して、重要な点があります。

◆ UPROPERTYはGC後にnullptrにしてくれる

その重要な点は

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/unreal-object-handling-in-unreal-engine#参照の自動更新

AActor または UActorComponent が破棄されるか、プレイから取り除かれると、それに対するすべての参照でリフレクション システムで可視のものは (UProperty ポインタおよび TArray など Unreal Engine コンテナ クラスに保存されているポインタ) 自動的に null になります。これは、ダングリング ポインタが残らないようにし、

この機能は、UPROPERTY とマーク付けされているか、Unreal Engine のコンテナ クラスに格納されている UActorComponent または AActor 参照にのみ適用されることを理解することが重要です。Raw ポインタに格納されているオブジェクト参照は、Unreal Engine 側では未知のものであり、自動的に null にはならず、ガーベジ コレクションも妨げません。

という部分です。自動で UPROPERTYマクロの付いたポインタをnullにしてくれます。

なお、UObjectではなく AActorUActorComponent についてのみ書かれています。一応調べてはみたのですが、UObjectは基本的にGCによる破棄のみで明示的な破棄がなさそうでしょうか...?

UObjectの場合はそもそも UPROPERTY を付けて参照している限りはGCされないという話でした。明示的な破棄がないとなると削除したい場合は全部の参照を自分で外さないといけません。大変ですね...!そんなときには TWeakObjectPtr というのを使います。これは「 所有をせず GC回収されるが、ダングリングポインタは回避したい」という場合には有効です。所有権を持つクラスだけが UPROPERTY で保持していれば、その所有権を持つクラスが参照を破棄すればGCされることになります。

なので、nullptrになって初めて破棄を検知するというのは作りが悪いよ!ってことでしょうか。クラス関係がCompositeであれば、所有すべきで参照があるうちは破棄されるべきではないですし、所持しないなら TWeakObjectPtr を使うべきですね。

Unreal は、参照されなくなった、または破棄と明示的にフラグ付けされた UObjects を定期的にクリーンアップするガーベジ コレクション スキームを実装しています。

とあるので、フラグ設定をすれば回収してもらえそうですが、ポーリング式の動作なら即時と言うわけではないかもしれません。あまり「破棄したらnullptrになる」挙動を前提としたロジックは良くなさそうです。あくまでダングリングポインタを防いでくれるだけと考えるべきです。

少し話がそれたので戻ります。
ここでする「 UPROPERTY はGC後にnullptrにしてくれる」という話は、UObjectではなくActor/UActorComponentの話になります。

UPROPERTY「UObjectをGC管理の木構造に追加し、GCに回収されるのを防ぐ」 というものでした。念のためですが「必ずしも UPROPERTY に入れていれば nullptr ではないことを保証できる」というものではありません。アクタは好きにDestroyできますしね。明示的に削除されたならば UPROPERTY に保持していても消えてしまうのは当然です。

重要なのは、削除後に「 UPROPERTY を付けたポインタが nullptr になる」 という点です。
「え、当り前じゃないの?削除されたんだから...」と思うかもしれませんが、ポインタはただアドレスの場所を示しているだけです。そこが削除され不正なアドレスになっていようが、ただアドレスを指すだけです。しかし、 UPROPERTY で保持しておくことで、 「UnrealEngineがnullptrにちゃんとしてくれる」 ようになります。

便利ですね。こういった不正なメモリ領域をアクセスしてしまうようなポインタを ダングリングポインタ と呼びます。このダングリングポインタは防がないとバグになってしまいます。何と言っても不正なメモリだと分からず、知る術もなくアクセスしてしまうので恐ろしいです。

C++であればスマートポインタを使うことでダングリングポインタを防ぐことも可能です。

ただ、 Unreal Engine では独自のガベージコレクタを実装しているため、C++のスマートポインタでは防げません。(pending killといった破棄途中という状態などがあり、まだC++のオブジェクト自体はあれど、UEのオブジェクトとしてアクセスし操作しようものならばエラーを引き起こしてしまいかねません)

代わりに、この UPROPERTY を使うことでダングリングポインタを回避することが可能です。アクタが破棄された後はnullptrを設定してくれるそうです。

「最初はちゃんと動いているのに、途中でなぜか不正なアクセスエラーが頻発してしまいます...」という方はこの辺りを見直してみるとよさそうです。 UPROPERTY をつけ忘れた生のポインタを使ってしまっていたら注意です。GCシステムの木構造に属さないUObjectはGCに回収されてしまいます。

◆ TObjectPtr について

さきほど保留した TObjectPtr について軽く調べておきます。

UPROPERTY()
TObjectPtr<UMyObject> MyObject;

ドキュメントは以下です。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/unreal-engine-5-migration-guide#c++オブジェクトポインタのプロパティ

Unreal Engine 独自の特殊な型のようです。エディタではアクセストラッキングなど何か裏でやっており、非エディタではただのポインタとして振舞うようです。ドキュメントから詳細は謎ですが、こっちを使って欲しいようです。記述量は増えてしまうので、もう少し恩恵について詳しく教えて欲しいですね。

軽くエンジンのコードを覗いてみます。

ObjectPtr.h
FORCEINLINE operator T* () const { return Get(); }
ObjectPtr.h
FORCEINLINE T* Get() const { return ObjectPtr_Private::Get<T>(ObjectPtr); }
ObjectPtr.h
FORCEINLINE UObject* Get() const
{
	return UE::CoreUObject::Private::ResolveObjectHandle(Handle);
}

使う側は気にせず通常のポインタとして使えますが、裏では独自の関数を噛ませてポインタをどっからか取得していそうですね。

これも検証しておきたいですね。これ経由でしかダングリングポインタの検出ができないとかあったりしたら嫌ですね...。そう書いてないので大丈夫だとは思いますが...。

■ 2.3. UObjectについて確認する

続いてUObjectについて確認しておきます。一番最初に抑えておくべき根幹な気もしますが、本題がガベージコレクタであり、まずはガベージコレクタ、次にそれに付随する機能について認識を固めていく、という方針で進めています。

ドキュメントを見ていきます。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/objects-in-unreal-engine

Unreal Engine にはゲーム オブジェクトを扱うための堅牢なシステムがあります。Unreal でのオブジェクトの基本クラスは UObject です。 UCLASS マクロを使用して、UObject から派生したクラスにタグ付けすると、UObject を扱うシステムが その派生クラスを認識できるようになります。

UObjectを継承し、 UCLASS マクロを付けることで Unreal Engine が認識し扱えるようになるようです。

UObjects の作成は、ランタイム時の NewObject メソッド、またはコンストラクタでの CreateDefaultSubobject メソッドでのみ行われるべきです。

既に少し触れましたが、C++の new は使わないようにしましょう。 NewObject<class>() 等で生成する必要があります。

UObject によって提供される機能

このシステムを使用するのは必須ではなく、使用するのが適切ではない場合もありますが、使用することで次のような多くのメリットがあります。

  • ガベージ コレクション
  • 参照の更新
  • ...

上記のメリットのほとんどは、UObject と同じリフレクションとシリアル化の機能がある UStruct にも当てはまります。UStruct は値の型と見なされているので、ガベージ コレクションは行われません。これらの各システムの詳細については、「Unreal でのオブジェクト処理」のドキュメントを参照してください。

こちらも既に触れましたね。ただ USTRUCT についても少し触れられています。この辺りについて後ほど検証したいです。

◆ UObjectの破棄

引き続きUObjectに関するドキュメントを見ていきます。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/objects-in-unreal-engine#オブジェクトを破棄する

オブジェクトを破棄する

オブジェクトの破棄は、オブジェクトが参照されなくなったときに、ガベージ コレクション システムによって自動的に処理されます。

  • PendingKill() が無効になっていれば、MarkGarbage() はそのオブジェクトを破棄する必要があることをオブジェクトのオーナーに通知しますが、そのオブジェクトへのすべての参照が解放されるまで、そのオブジェクト自体はガベージ コレクションの対象になりません。
  • アクタの場合、アクタに対して Destroy() が呼び出されて、そのアクタがレベルから取り除かれても、そのアクタへのすべての参照が解放されるまで、そのアクタはガベージ コレクションの対象になりません。

特に新しい情報はありませんが、GCによって破棄されることが書いてあります。

ただ、アクタについては少しひっかかります...。

そのオブジェクトへのすべての参照が解放されるまで、そのオブジェクト自体はガベージ コレクションの対象になりません。

の部分です。

Destroyでレベルから除去されても、参照が残っていたらGCに回収されない...?「そのアクタへのすべての参照が解放されるまで」とは具体的にどういう時なのでしょうか...。どっかで参照し続けていたら破棄マーク状態でずっとメモリに残るのでしょうか...。メモリリークしてしまわないのでしょうか...。そもそも破棄後はnullptrにしてくれると書いてあったような...。

これまで見てきたドキュメント内容と齟齬があるような気もします。

参照の自動更新

AActor または UActorComponent が破棄されるか、プレイから取り除かれると、それに対するすべての参照でリフレクション システムで可視のものは (UProperty ポインタおよび TArray など Unreal Engine コンテナ クラスに保存されているポインタ) 自動的に null になります。これは、ダングリング ポインタが残らないようにし、

のはずです。

あるいは、ここで言う「そのアクタへのすべての参照」は UPROPERTY ポインタではない別の話なのでしょうか...。

言葉足らずで記述が不親切な気がします...。整合性を保つように行間を読むならば、

  1. アクタがDestroyされレベルからは除去
  2. まだそのアクタを参照しているポインタがあるので GC対象にならない
  3. 「UnrealEngineの参照の自動更新機能」により、そのアクタ参照箇所はnullにされる
  4. 結果、そのアクタへのすべての参照が外れる
  5. その「破棄済みアクタ」への参照が全て外れたので、ここでやっとGC対象になる

ということでしょうか。参照の自動更新機能とガベージコレクタの仕様も関係もよく分からないので、合っているかは分かりませんが。ドキュメントの表題・セクションの文脈の話しかしていない、と言われればそうなのですが、なんか誤解を招きそうな不親切な文章な気がやっぱりします。せめて、「そのアクタへのすべての参照が解放されるまで、そのアクタはガベージコレクションの対象になりません。参照の自動更新によりすべての参照がnullにされた後にガベージコレクションの対象になります。」と書いて欲しいです。他のドキュメントへのリンクもなしに、前提で書かれても「え、自分で参照はちゃんと外さないといけないの!?」となってしまう気がします。

上記の話は「ただ行間を読んでいるだけ」で推測なので実際は分かりません。ひとまずドキュメントからは深掘りしようがないので、この解釈で納得しておきます。間違っていたらご指摘頂けると嬉しいです。

次を見ていきます。

弱いポインタは、参照しているオブジェクトに対してガベージ コレクションが行われるかどうかに影響を及ぼしません。

弱いポインタ( TWeakObjectPtr )は既に触れましたが保留していました。リンクがされているのでドキュメントを見てみましょう。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/weak-pointers-in-unreal-engine

弱いポインタ はオブジェクトに対する弱い参照を格納します。弱いポインタは 共有のポインタ や 共有の参照 とは異なり、参照するオブジェクトの破棄を防止しません。

なお、示されているのが TWeakPtr で、 TWeakObjectPtr については触れられていませんね。 TWeakObjectPtr についてしっかり解説しているドキュメントは見つけられませんでした。表題は Weak Pointers であり、だいたい同じかと思います。

「GCの木に追加されず、破棄後にnullptrにしてくれる機能はあり」 という感じでしょうか?

■ 2.4. UStructについて確認する

少し前に出てきたのでこれについてもドキュメントを確認しておきます。 Unreal Engine では UStruct は値型とみなすためGCしないと書いてありました。UObjectのように NewObject() メソッドでGC管理のヒープに作成するような使用方法はなさそうです。もしスタックではなくヒープに作成するならば、UEのスマートポインタを使うのかなと思います。

ドキュメントは以下です。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/structs-in-unreal-engine

構造体 は、メンバーのプロパティを整理および操作する際に便利なデータ構造です。Unreal Engine のリフレクション システムは、構造体を UStruct として認識しますが、構造体は UObject エコシステムの一部ではないため、UClasses 内で使用することはできません。

  • UStruct は、同じデータ レイアウトを使用する UObject よりも高速に作成できます。
  • UStruct は UProperty をサポートしますが、ガベージ コレクション システムによって管理されておらず、UFunction の機能も使用できません。
  • 主な用途はプロパティを整理・操作すること
  • Unreal Engine のリフレクションシステムが認識すること
  • UPEROPERTY をサポートすること
  • ガベージコレクションシステムによる管理はされないこと

とのことです。

構造体は UObject エコシステムの一部ではないため、UClasses 内で使用することはできません。

この意味が分かりません。使用とは何を指しているのでしょうか。普通に UCLASS の中でメンバにできるはずです。何かリフレクションシステムでの制約があるという意味でしょうか?定義は UCLASS 内では宣言できないのでそのことでしょうか?これもドキュメントからもう読み取れることはないので謎のままとします。知っていたら教えて頂けるとありがたいです。

重要なのは UPROPERTY をサポートするという点です。

USTRUCT構造体の中の UPROPERTY メンバ変数については

  • GCの回避
  • 破棄後はnullptrにしてくれる(アクタなど)

をサポートしてくれているという認識でいいのでしょうか。

ベスト プラクティスとヒント

以下は、UStruct を使用するときに覚えておくと役立つヒントです。

  1. UStruct は、Unreal Engine のスマート ポインタとガベージ コレクション システムを使用して、ガベージ コレクションによる UObjects の削除を防ぐことができます。

と書いてあります。GCの回避については記述がありました。明記はされていませんが、 UPROPERTY をサポートと書いてあるので、破棄後にnullptrにしてくれる機能などちゃんと動くはずです。

ただ、 「 UClasses 内では使用することができません」という文言が気になります。 USTRUCT の主な用途はプロパティの整理とありました。つまり、 UCLASS マクロの付いたUObject内のプロパティを構造体にまとめたりすることだと思います。その際にちゃんと UPEROPERTY の効力が USTRUCT 内であっても変わらず機能するという挙動を期待したいです。後ほど確認したいと思います。

3. 個人的な不明点について

ドキュメントに沿ってなので少しまとまりなく、長めになりましたが、ガベージコレクタ周辺の話を読んできました。

まずは分かったことを整理します。

  • UObjectは生成したらGC対象になる
  • 勝手にGCされないように UPROPERTY マクロを付けて保持する
  • USTRUCT内でも UPEROPERTY マクロが機能する
  • マクロによりUObjectをエンジンが認識し、GCルートセットの木に関連付けられる
  • TWeakObjectPtr は所有せずGC対象にはなるが、ダングリングポインタを回避できる

続いて、個人的にちゃんと確認しておきたいことが以下です。

  1. TWeakObjectPtrUPEROPERTY マクロはいらないのか
  2. 構造体での UPROPERTY の使い方
  3. TObjectPtr 、もしかして UPROPERTY マクロ、いらない...?

■ 3.1. TWeakObjectPtrUPEROPERTY マクロはいらないのか

UPROPERTY マクロでポインタを保持すると

  • GCの回避
  • 破棄後はnullptrにしてくれる

とのことでした。

そして TWeakObjectPtr は所有はしない、つまり「GCの回避」はしないときに使うと書いてあります。UPROPERTY マクロを辿ることでGCする対象を特定するとのことだったので、 UPROPERTY マクロは必要なさそうな気もします。ですが、付けないとエンジンにそもそも認識してもらえず「破棄後はnullptrにしてくれる」が機能しないとかだったりするのでしょうか?

無いとは思いますが、逆に UPEROPRTY を付けてしまうことでGC木に追加されてしまうことはないでしょうか?

ただ、 UPEROPERTY を付けるのかに関しては記述を見つけられなかったのでこれらはよく分かりません。必要がなければ書いてないと思うので必要がないということでしょうか。

エンジンのコードを見に行ってみます。

WeakObjectPtrTemplates.h
/**
 * TWeakObjectPtr is the templated version of the generic FWeakObjectPtr
 * 訳: TWeakObjectPtr は、ジェネリック FWeakObjectPtr のテンプレート バージョンです
 */
template<class T, class TWeakObjectPtrBase>
struct TWeakObjectPtr : private TWeakObjectPtrBase
{
WeakObjectPtrTemplates.h
	FORCEINLINE T* Get(bool bEvenIfPendingKill) const
	{
		return (T*)TWeakObjectPtrBase::Get(bEvenIfPendingKill);
	}

	FORCEINLINE T& operator*() const
	{
		return *Get();
	}

	FORCEINLINE T* operator->() const
	{
		return Get();
	}

	FORCEINLINE bool IsValid(bool bEvenIfPendingKill, bool bThreadsafeTest = false) const
	{
		return TWeakObjectPtrBase::IsValid(bEvenIfPendingKill, bThreadsafeTest);
	}

summaryに FWeakObjectPtr のテンプレートとあるので、 FWeakObjectPtr の方を見に行きます。

WeakObjectPtr.cpp
UObject* FWeakObjectPtr::Get(bool bEvenIfGarbage) const
{
	return Internal_Get(bEvenIfGarbage);
}

bool FWeakObjectPtr::IsValid(bool bEvenIfGarbage, bool bThreadsafeTest) const
{
	// This is the external function, so we just pass through to the internal inlined method.
	return Internal_IsValid(bEvenIfGarbage, bThreadsafeTest);
}

Internal_XXX 関数が別途あるので見てみます。

WeakObjectPtr.cpp
	FORCEINLINE_DEBUGGABLE bool Internal_IsValid(bool bEvenIfGarbage, bool bThreadsafeTest) const
	{
		FUObjectItem* const ObjectItem = Internal_GetObjectItem();
		if (bThreadsafeTest)
		{
			return (ObjectItem != nullptr);
		}
		else
		{
			return (ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfGarbage);
		}
	}

	FORCEINLINE_DEBUGGABLE UObject* Internal_Get(bool bEvenIfGarbage) const
	{
		FUObjectItem* const ObjectItem = Internal_GetObjectItem();
		return ((ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfGarbage)) ? (UObject*)ObjectItem->Object : nullptr;
	}

どちらも Internal_GetObjectItem() で取得していますね。見てみます。

WeakObjectPtr.cpp
	FORCEINLINE FUObjectItem* Internal_GetObjectItem() const
	{
		using namespace UE::Core::Private;

		if (ObjectSerialNumber == 0)
		{
#if UE_WEAKOBJECTPTR_ZEROINIT_FIX
			checkSlow(ObjectIndex == InvalidWeakObjectIndex); // otherwise this is a corrupted weak pointer
#else
			checkSlow(ObjectIndex == 0 || ObjectIndex == -1); // otherwise this is a corrupted weak pointer
#endif

			return nullptr;
		}

		if (ObjectIndex < 0)
		{
			return nullptr;
		}
		FUObjectItem* const ObjectItem = GUObjectArray.IndexToObject(ObjectIndex);
		if (!ObjectItem)
		{
			return nullptr;
		}
		if (!SerialNumbersMatch(ObjectItem))
		{
			return nullptr;
		}
		return ObjectItem;
	}

GUObjectArray.IndexToObject(ObjectIndex); でどこかからか取得しています。見に行ってみます。

UObjectHash.cpp
// Global UObject array instance
FUObjectArray GUObjectArray;

実体は GUObject.Array というところにあるようです。WeakPointerは自身のIndexだけ持ち、グローバルな配列のようなオブジェクトにアクセスしているだけのようですね。

ObjectIndex はおそらく TWeakObjectPtr 作成時にUObjectの持っている値をもらっているのでしょう。確認しておきます。

WeakObjectPtr.cpp
void FWeakObjectPtr::operator=(const class UObject *Object)
{
	if (Object // && UObjectInitialized() we might need this at some point, but it is a speed hit we would prefer to avoid
		)
	{
		ObjectIndex = GUObjectArray.ObjectToIndex((UObjectBase*)Object);
		ObjectSerialNumber = GUObjectArray.AllocateSerialNumber(ObjectIndex);
		checkSlow(SerialNumbersMatch());
	}
	else
	{
		Reset();
	}
}

していますね。

どうやら、 Unreal Engine が TWeakObjectPtr にnullptrを律儀にセットしにいくのではなく、 TWeakObjectPtr がグローバルな所へ確認をしに行くみたいですね。ならば、特に UPROPERTY は必要ないように思います。

UPROPERTY マクロでリフレクションを使って機能しているのではないなら、 ただの構造体でも TWeakObjectPtr なら使えそうですね。

余談

コードを見ているとこんなのがありました。

WeakObjectPtrTemplates.h
// This is explicitly not added to avoid resolving weak pointers too often - use Get() once in a function.
// (訳: これは、弱いポインタが頻繁に解決されるのを避けるために明示的に追加されていません。関数内で Get() を 1 回使用してください。)
explicit operator bool() const = delete;

つまり、

サンプル
// そのままチェック
if (WeakObjectPtr)
{
    const FVector Location = WeakObjectPtr->GetActorLocation();
}

// どっからかもらってチェック
if (TWeakObjectPtr<AActor> Actor = GetActorForSample())
{
    const FVector Location = Actor->GetActorLocation();
}

のようなことはできないようになっています。コメントに書いてある通り頻繁な使用を避けるために意図的に消しており、 Get() を1度だけ使って、有効なポインタを取り出し、以降はそれを使えとのことです。

Get() で取り出した後はnullptrかどうか確認すればいいのでしょうか? TWeakObjectPtr::GetのAPI Referenceで戻り値を確認してみましょう。

Remarks

Dereference the weak pointer nullptr if this object is gone or the weak pointer is explicitly null, otherwise a valid uobject pointer
このオブジェクトが存在しないか、弱いポインタが明示的にNULLである場合は弱いポインタnullptrを、そうでない場合は有効なuobjectポインタを参照する。

UObjectが無効な場合はnullptrが返ってくるそうです。なので以下のように書くとよいと思われます。

サンプル
TWeakObjectPtr<AActor> ActorWeakPtr = GetActorForSample();

// このスコープでしか使わないのでそのまま生ポインタに取得
AActor* Actor = ActorWeakPtr.Get();

// ない場合はnullptrになっている
if (Actor != nullptr)
{
    const FVector Location = Actor->GetActorLocation();
}

という風に書くべきとのことです。

ちなみに

if (ActorWeakPtr.IsValid())
{
    const FVector Location = ActorWeakPtr->GetActorLocation();
}
if (ActorWeakPtr.IsValid())
{
    AActor* Actor = ActorWeakPtr.Get();
    const FVector Location = Actor->GetActorLocation();
}

も内部的にWeakPointerの有効チェックが入っており、 IsValid() が無駄ですね。 Get() した方がよさそうです。Tickじゃない箇所で負荷が気にならず、変数のスコープを限定したいのでifの中でGetしたい、とかならありでしょうか?

■ 3.2. 構造体での UPROPERTY の使い方

少しデータと処理を手軽にまとめたいときに構造体でまとめることがあるかと思います。 USTRUCT の主な使用目的でも「主な用途はプロパティを整理・操作すること」と書いてありましたね。よく使うので構造体での UPROPERTY の使い方について確認したいと思います。

USTRUCTUPROPERTY をサポートしているとのことでした。 つまり USTRUCT マクロを付けていないただの構造体では UPROPERTY はサポート外で機能しません。Rider IDEを使っていると警告して教えてくれます。

また、 USTRUCT マクロのない構造体はそもそも UPROPERTY マクロを付けられません。

そして気になっているのが UPROPERTY マクロを USTRUCT 構造体自体のメンバ変数につけるべきかどうかです。Riderは特にどちらもエラーや警告を出していません。ですが、リフレクションを使って木構造として辿っていくなら UPROPERTY は必要そうな気がします。

また、UPROPERTYをサポートとあるので当然だとは思いますがGC回避だけでなく nullptr を破棄後に入れてくれることを確認します。

■ 3.3. TObjectPtr 、もしかして UPROPERTY マクロ、いらない...?

Riderが何かと教えてくれるので、逆に警告出さないと「え、いいの?」となってしまいます...。生のポインタの場合は UPROPERTY マクロを付けていないと警告が出るのですが、 TObjectPtr には警告が実は出ません。

Riderだって最初から全部のケースをサポートしてくれるわけではないので、 UPROPERTY がいらないなんてことはないとは思います。そのうちこのケースも警告をだしてくれるようになるかもしれません。

ただ、 TObjectPtrTWeakObjectPtr と同様に自分からグローバルな所に見に行ってる可能性もあるのかな...とも思ったのですが、 TObjectPtr のコードを覗いてみるとどうやら FObjecthandle という構造体を持っており、その中は UPTRINT PointerOrRef; というポインタをそのまま持っていそうな感じです。なのでたぶんですが、自分から解決しにいくわけではないように感じます。となるとやはり UPROPERTY マクロを付けなければ機能しないと見ていいのでしょうか。

リフレクションを使ったUnreal Engine 側の処理はさすがに手軽にコードを追いにくそうなので挙動だけ確認してみたいと思います。

ないとは思いますが、念のため。

4. 不明点の検証をする

■ 4.1. TWeakObjectPtrUPEROPERTY マクロはいらないのか

UPROPERTYには「GC回避」「破棄後にnullptrにしてくれる」がありました。 TWeakObjectPtr は所有しないので「GC回避」はされませんが「破棄後にnullptrにしてくれる」があるとのことです。

先ほどの調査ではエンジンがnullptrにしてくれるのではなく、 TWeakObjectPtr が自ら参照を取りに行くので、 UPROPERTY がなくても機能するという予想を立てました。

ここでは念のため、「GCされ破棄された後にちゃんと検知しダングリングポインタを回避できるか」を確認しておきます。

パターンとしては次の3つを確認してみます。それぞれでnullptrになり、不正な残骸メモリにアクセスしなければOKです。

TWeakObjectPtr
// UPROPERTYなしで、ダングリングポインタを回避できるか
TWeakObjectPtr<UMyObject> MyObjectWeakPtr;
UPROPERTY付のTWeakObjectPtr
// UPROPERTY付けた場合に、GC回避をしてしまわないか. GCされてnullptrになっていれば想定通り.
UPROPERTY()
TWeakObjectPtr<UMyObject> MyObjectWeakPtrWithUProperty;
構造体の中のTWeakObjectPtr
struct FData
{
	TWeakObjectPtr<UMyObject> MyObject;
};
// 構造体に入っていても、ダングリングポインタを回避できるか
FData Data;

ダングリングポインタが発生することの確認として UPROPERTY なしの生ポインタも用意しておきます。これはGCされ破棄されているのに検知ができずアクセスができてしまう例とします。

// UPROPERTYがないのでGCされ、不正なポインタとなるはず
UMyObject* MyObjectPtr;

全体のコードです。まずはUObjectを用意します。

MyObject.h
#pragma once
#include "CoreMinimal.h"
#include "MyObject.generated.h"
class UMyObject;

UCLASS()
class UPROPERTYTEST_API UMyObject : public UObject
{
	GENERATED_BODY()
public:
	UMyObject(): Value(FMath::Rand()) { }
	int Value;
};

続いて、これを生成し TWeakObjectPtr に保存するアクターを用意します。また確認用のメソッドをいくつか用意しています。

MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
class UMyObject;

UCLASS()
class UPROPERTYTEST_API AMyActor : public AActor
{
	GENERATED_BODY()
protected:
	UFUNCTION(BlueprintCallable)
	void GenerateMyObject();
	UFUNCTION(BlueprintCallable)
	void CheckWeakPtr();
	UFUNCTION(BlueprintCallable)
	void CheckWeakPtrWithUProperty();
	UFUNCTION(BlueprintCallable)
	void CheckWeakPtrInStruct();
	UFUNCTION(BlueprintCallable)
	void CheckPtr();

private:
	// UPROPERTYなしで、ダングリングポインタを回避できるか
	TWeakObjectPtr<UMyObject> MyObjectWeakPtr;

	// UPROPERTY付けた場合に、GC回避をしてしまわないか. GCされてnullptrになっていれば想定通り.
	UPROPERTY()
	TWeakObjectPtr<UMyObject> MyObjectWeakPtrWithUProperty;

	struct FData
	{
		TWeakObjectPtr<UMyObject> MyObject;
	};
	// 構造体に入っていても、ダングリングポインタを回避できるか
	FData Data;

	// UPROPERTYがないのでGCに回収され、不正なポインタとなるはず
	UMyObject* MyObjectPtr;
};
MyActor.cpp
#include "MyActor.h"
#include "MyObject.h"

void AMyActor::GenerateMyObject()
{
	GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Blue, TEXT("MyObject created"));
	UMyObject* MyObject = NewObject<UMyObject>();
	MyObjectWeakPtr = MyObject;
	MyObjectPtr = MyObject;
	Data.MyObject = MyObject;

	UMyObject* MyObject2 = NewObject<UMyObject>();
	MyObjectWeakPtrWithUProperty = MyObject2;
}

void AMyActor::CheckWeakPtr()
{
	UMyObject* MyObject = MyObjectWeakPtr.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("TWeakObjectPtr is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("TWeakObjectPtr is nullptr."));
	}
}

void AMyActor::CheckWeakPtrWithUProperty()
{
	UMyObject* MyObject = MyObjectWeakPtrWithUProperty.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("[UPROPERTY] TWeakObjectPtr is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("[UPROPERTY] TWeakObjectPtr is nullptr."));
	}
}

void AMyActor::CheckWeakPtrInStruct()
{
	UMyObject* MyObject = Data.MyObject.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("Data.MyObject is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("Data.MyObject is nullptr."));
	}
}

void AMyActor::CheckPtr()
{
	if (IsValid(MyObjectPtr))
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("MyObjectPtr is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("MyObjectPtr is nullptr."));
	}
}

以下の関数をBPとかから呼び出して検証してみます。
GCはコンソールコマンドから Obj GC とすることで強制的に起こさせて確認します。

  • AMyActor::GenerateMyObject()
    • UObjectを生成。各確認用のプロパティに格納。
  • AMyActor::CheckWeakPtr()
    • TWeakObjectPtr に格納したUObjectを検証
    • GC後はnullptrになり、ダングリングポインタを回避できることを期待
  • AMyActor::CheckWeakPtrInStruct()
    • ただの構造体の中の TWeakObjectPtr に格納したUObjectを検証
    • リフレクションではなく自らチェックしにいくのでこれでも機能することを期待
  • AMyActor::CheckPtr()
    • UPROPERTY も付けない生ポインタに格納したUObjectを検証(Part1)
    • GC後もnullptrにならずに、ダングリングポインタが発生することを期待
    • 複雑なデータ構造ではないのでエラーにはならずアクセスできそう。でも不正なメモリ。
  • AMyActor::AccessPtr()
    • UPROPERTY も付けない生ポインタに格納したUObjectを検証(Part2)
    • アクセスエラーにはならなさそうだけど一応ダングリングポインタにアクセスしてみる

以下がログ表示です。

期待通りの動作になりました。GC後はちゃんとnullptrになり、消えていることを検知できています。また TWeakObjectPtrUPROPERTY を付けてもGC回避されたりもなさそうです。

ダングリングポインタが発生する例として用意した生ポインタではGCによる破棄を検知できずに IsValid が通ってしまっています。ただのintしか持たないので特にアクセスエラーは発生せずにそのままアクセスできてしまうかと思いますが、本来は不正なメモリのはずなのでエラーが起きうる状態です。他のオブジェクトが生成されたりするとそのメモリ領域が上書きされ、別の変な値がでてきてしまったりするのではないかと思います。

どうやら TWeakObjectPtrUPROPERTY マクロなしで機能していそう です。

■ 4.2. 構造体での UPROPERTY の使い方

USTRUCT 構造体は UPROPERTY をサポートしているとのことでした。そしてその構造体自体のメンバ変数を用意するときに UPROPERTY マクロは必要なのか確認したいと思います。

GCの仕組みとしては UPROPERTY をリフレクションで取得して、木検索を行いGC対象の検知をするとのことだったので、おそらく UPROPERTY は必要だと思います。ただ、よく指摘してくれるRiderが指摘をしてくれないので念のため確認をしてみたいと思います。

さきほどと同じ要領でUObjectを作成・保持し、GCを強制作動させます。 UPROPERTY なしの構造体メンバ変数を用意し、予想通りであればGCに回収されてしまうはずです。ただ、GCに回収されてもダングリングポインタ検知がそもそもできないため、nullptrにならず検証ができません。そこで TWeakObjectPtr にも入れておくことでダングリングポインタ検知したいと思います。( TWeakObjectPtr は所有しないのでGCを妨げない)

UObjectじゃなくてActorでやればクラッシュしてくれるかもですが、今回は「UObjectが意図せずGCされてしまう」という文脈で確認しているのでUObjectで進めます。

UObjectは先ほどと同じなのでコードは割愛します。以下が確認用コードです。

MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
class UMyObject;

USTRUCT()
struct FData
{
	GENERATED_BODY()

	UPROPERTY()
	TObjectPtr<UMyObject> MyObject;
};

UCLASS()
class UPROPERTYTEST_API AMyActor : public AActor
{
	GENERATED_BODY()
protected:
	UFUNCTION(BlueprintCallable)
	void GenerateMyObject();
	UFUNCTION(BlueprintCallable)
	void CheckPtrInStruct();
	UFUNCTION(BlueprintCallable)
	void CheckWeakPtr();

private:
	// UPROPERTYなし。想定通りならGC回避できない。加えてダングリングポインタが発生してしまう。
	FData Data;

	// ダングリングポインタになっているかの確認. nullptrになっていればGCに回収されたことが確認できる.
	TWeakObjectPtr<UMyObject> MyObjectWeakPtr;
};
MyActor.cpp
#include "MyActor.h"
#include "MyObject.h"


void AMyActor::GenerateMyObject()
{
	GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Blue, TEXT("MyObject created"));
	UMyObject* MyObject = NewObject<UMyObject>();
	Data.MyObject = MyObject;
	MyObjectWeakPtr = Data.MyObject;
}

void AMyActor::CheckPtrInStruct()
{
	if (IsValid(Data.MyObject))
	{
		const FString Message = FString::Printf(TEXT("Data.MyObject is valid: %i"), Data.MyObject->Value);
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, *Message);
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("Data.MyObject is nullptr."));
	}
}

void AMyActor::CheckWeakPtr()
{
	UMyObject* MyObject = MyObjectWeakPtr.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("TWeakObjectPtr is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("TWeakObjectPtr is nullptr."));
	}
}

確認してみます。

GCされちゃってますね...!! 予想通りです。

では、ちゃんと構造体メンバ変数にも UPROPERTY を付けて、GC回避ができるか確認します。

変更前
// UPROPERTYなし。想定通りならGC回避できない。加えてダングリングポインタが発生してしまう。
FData Data;
// ダングリングポインタになっているかの確認. nullptrになっていれば、GCに回収されたことが確認できる.
TWeakObjectPtr<UMyObject> MyObjectWeakPtr;

変更後
// UPROPERTYを付ける。想定通りならGC回避できる。
UPROPERTY()
FData Data;
// ダングリングポインタになっているかの確認. nullptrになっていなければ、GCされていないことが確認できる。
TWeakObjectPtr<UMyObject> MyObjectWeakPtr;

とします。

確認します。ちゃんと TWeakObjectPtr も有効のままで、GCされていないのが確認できました。

やはり、 USTRUCT 構造体のメンバ変数にも UPEROPERTY マクロは必須 のようです。C++ではなくUE仕様については見落としてしまって UPROPERTY 付け忘れちゃうということもなくはないので気負付けたいです。Riderも指摘してくれるようになると嬉しいですね。(十分助かっていますが!)

■ 4.3. TObjectPtr 、もしかして UPROPERTY マクロ、いらない...?

...確認するまでもなく必要でしょう。
Riderが全部指摘してくれると思うのは甘えですね!ですが一応確認します。

MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
class UMyObject;

UCLASS()
class UPROPERTYTEST_API AMyActor : public AActor
{
	GENERATED_BODY()
protected:
	UFUNCTION(BlueprintCallable)
	void GenerateMyObject();
	UFUNCTION(BlueprintCallable)
	void  CheckMyObject1();
	UFUNCTION(BlueprintCallable)
	void CheckMyObject2();

private:
	// GCされない
	UPROPERTY()
	TObjectPtr<UMyObject> MyObject1;
	// GCされるはず
	TObjectPtr<UMyObject> MyObject2;

	// ダングリングポインタ確認用
	TWeakObjectPtr<UMyObject> MyObject1WeakPtr;
	TWeakObjectPtr<UMyObject> MyObject2WeakPtr;
};
MyActor.cpp
#include "MyActor.h"
#include "MyObject.h"

void AMyActor::GenerateMyObject()
{
	GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Blue, TEXT("MyObject created"));
	MyObject1 = NewObject<UMyObject>();
	MyObject2 = NewObject<UMyObject>();
	MyObject1WeakPtr = MyObject1;
	MyObject2WeakPtr = MyObject2;
}

void AMyActor::CheckMyObject1()
{
	// GCされてるかは WeakPtr だけでチェック
	UMyObject* MyObject = MyObject1WeakPtr.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("MyObject1 is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("MyObject1 is invalid."));
	}
}

void AMyActor::CheckMyObject2()
{
	UMyObject* MyObject = MyObject2WeakPtr.Get();
	if (MyObject != nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Green, TEXT("MyObject2 is valid."));
	}
	else
	{
		GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("MyObject2 is invalid."));
	}
}

確認します。想定通りです。当然ですね!

TObjectPtr だろうが UPROPERTY を付けていなければGCされます!されないとはドキュメントのどこにも書いてないですし、GCの検知方法を考えればそれはそうです。

生ポインタではしてくれるRiderの指摘がなかったこと、もしかしてTWeakObjectPtrみたいな自らチェックしに行く機構があったりしない?...ということで確認しておきました。

5. おわりに

まだUEに慣れず、時折 UPROPERTY のGC挙動に不安を覚えることがあったため、一度GCについてドキュメントをしっかりと確認してみました。

  • UPROPERTY をもとにルートセットという木構造を構築し、GC時にルートセットの木に属さないUObjectを検知し破棄している
  • TWeakObjectPtr の中身について

あたりは新しく知れたことだったので改めて確認してみてよかったと思います。

あとは、 USTRUCT の中のUObjectメンバ変数には UPROPERTY は付けてると思いますが、 USTRUCT構造体のメンバ変数にちゃんと UPROPERTY 付けていたかな...というのがすごく不安になってきました。構造体をそのままメンバ変数に作るだけなら別途ヒープに確保するわけではないので、GCを意識しておらずなんか付け忘れてる気がしてきました...。
(直近、 USTRUCT にUObjectを所有させてるコードあまり書いてなかった気もするので大丈夫そうな気がしますが以後機会があれば気を付けたいです)

自分の理解の整理も兼ねて技術ブログに書いてみましたが、既に有用なブログがたくさんあるかと思うのでそれらも見てもらえればと思います。何か勘違いが本記事にあれば教えて頂けると喜びます。

Discussion