ポインタ化した構造体メンバのGC解放を防ぐ

UnrealEngine

はじめに

※確認UEバージョン:5.0.3

ポインタとして扱っている構造体のUObjectメンバ変数がGCで解放されないようにする手順を解説します。

構造体ポインタとして扱いたいが、GCを防ぐ目的でUObject継承クラス化したくない方はぜひ最後までご覧ください!

構造体のポインタはUPROPERTY化できない

下記のように、ポインタで構造体を扱うと構造体ポインタにUPROPERTYは付けられないのでエラーが出てしまいます。

USTRUCT()
struct FMyStruct {
   GENERATED_BODY()

   UPROPERTY(Transient)
   TObjectPtr<UObject> MyHogeObject;
};

UCLASS(Blueprintable)
class MYTESTPROJECT_API UMyObject : public UObject {
   GENERATED_BODY()

public:
   // エラー内容:Inappropriate '*' on variable of type 'FMyStruct', cannot have an exposed pointer to this type. [UnrealHeaderTool Error]
   // 訳:FMyStruct' 型の変数に不適切な '*' があり、この型への公開ポインタを持つことができません。
   // UPROPERTY(Transient)
   // FMyStruct* MyStruct;
};

そのためUPROPERTYではないポインタとして扱う必要があり、構造体内のUObjectは参照できずGCで消されてしまいます。

以下の項目で対応方法をいくつか紹介します。

方法1:構造体ポインタを持っているUObjectクラス内でGCリファレンスを追加する

構造体ポインタを所持しているUObjectクラスにAddReferencedObjects関数を追加して、構造体をリファレンスオブジェクトとして登録する方法です。

手順

UObjectを継承したクラス宣言内で下記関数を宣言してください。

// Begin UObject Interface
static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector);
// End UObject Interface

この関数はクラス情報のUProperty登録にないオブジェクトを手動で登録できる関数になります。

virtualではなく、static関数になっていますが宣言するだけで関数呼び出しがありますので、問題ありません。

関数定義は以下のようにします。

void UMyObject::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector)
{
   const UMyObject* pThisObject = CastChecked<UMyObject>(InThis);

   if(pThisObject->MyStruct != nullptr)
   {
      const UScriptStruct* pStructClass = FMyStruct::StaticStruct();
      Collector.AddReferencedObjects(pStructClass, pThisObject->MyStruct);
   }
   
   Super::AddReferencedObjects(InThis, Collector);
}

手順だけだとわかりにくいので、下記で解説していきます。

const UMyObject* pThisObject = CastChecked<UMyObject>(InThis);

UObject*のInThisを自クラス型にキャストして、pThisObject に格納しています。

この関数がstatic関数なので、直接メンバ変数を記述してもコンパイルエラーになるためです。

また、Castではなく、CastCheckedにしている理由は、InThisが自クラスオブジェクトか継承クラス以外の型を渡されることがないからです。(nullだった場合はCastChecked内でアサートが出ます。そのためこれ以降の行ではpThisObjectのnullチェックをしていません。)

if(pThisObject->MyStruct != nullptr)

MyStructのnullチェックです。

nullを渡してもAddReferencedObjects関数内で読み取ろうとしてエラーが出てしまうため追加しています。

Collector.AddReferencedObjects(pStructClass, pThisObject->MyStruct);

構造体ポインタ内のPropertyをリファレンスオブジェクトとして登録します。

AddReferencedObjectsはたくさんのオーバーロード候補があるのですが、今回は下記を使用しています。

void AddReferencedObjects(
  const class UScriptStruct*& ScriptStruct, 
  void* StructMemory, 
  const UObject* ReferencingObject = nullptr, 
  const FProperty* ReferencingProperty = nullptr);

第1引数には構造体のStaticStruct()を、第2引数には構造体ポインタを入れてください。

構造体ポインタとStaticStructの型は同じにしてください。

例えば、第1引数をFMyObject::StaticStruct()にして、第2引数をFMyObjectを継承したFMyObject2をポインタとして指定すると、FMyObject2内で宣言されたオブジェクトのみGCで消されてしまいます。

継承された構造体もリファレンスできるようにしたい場合は、構造体ポインタだけでなく、構造体のUScriptStruct型も同時に持たせるようにすることで、解決できます。

方法2:構造体ポインタ型を独自に作成する

構造体ポインタの代わりに独自に作成したUSTRUCTの型を持たせて、作成した構造体内で構造体ポインタを管理する方法です。

手順

まず、専用のポインタをもつUSTURCT構造体を作成します。

USTRUCT()
struct FMyStructPtr {
   GENERATED_BODY()
public:
   FMyStruct* Pointer = nullptr;

public:
   void AddStructReferencedObjects(class FReferenceCollector& Collector);
};

template<>
struct TStructOpsTypeTraits<FMyStructPtr> : public TStructOpsTypeTraitsBase2<FMyStructPtr>
{
   enum
   {
      WithAddStructReferencedObjects = true,
   };
};

作成した構造体にAddStructReferencedObjects関数を追加したので、処理を書き込みます。

void FMyStructPtr::AddStructReferencedObjects(FReferenceCollector& Collector)
{
   const UScriptStruct* Struct = FMyStruct::StaticStruct();
   if (Struct != nullptr && Pointer != nullptr)
   {
      Collector.AddReferencedObjects(Struct, Pointer);
   }
}

構造体ポインタ型をメンバ変数宣言していた箇所を、作成した構造体型にしたうえでUPROPERTYにします。

UPROPERTY(Transient)
FMyStructPtr MyStructPtr;

手順だけだとわかりにくいので、下記で解説していきます。

TStructOpsTypeTraits<>

FMyStructPtrの下で宣言しているTStructOpsTypeTraitsは、USTRUCTの型特性を設定できるテンプレート構造体になります。

TStructOpsTypeTraitsBase2 を継承していて、TStructOpsTypeTraitsBase2 の宣言を確認するとデフォルトの型特性フラグ情報を確認することができます。

今回、WithAddStructReferencedObjectsをtrueにしていますが、このフラグをtrueにすることでGC回収時にAddStructReferencedObjectsが呼ばれるようになり、UPROPERTYになっていないUObjectのGCを防ぐことができます。

詳しくは下記をご確認ください。

AddStructReferencedObjectsの処理については、方法1とほぼ同じですので、解説は省略します。

方法3:プラグイン「StructUtils」を使用する

UEに標準で入っているプラグイン StructUtls を使用する方法です。

StructUtls内のInstancedStruct構造体を使用することで、方法2と同じような形でGCを防ぐことができます。

さらに、GCを防ぐだけでなくUEエディタで値を編集・アセットに構造体のデータを保存することができるようです。

StructUtls はベータ版です。正式版ではないので、不具合や(バージョンの互換性はサポートされていますが)将来的に大きな仕様変更・クラス名や関数名の変更が発生する可能性があります。

StructUtlsを使用するメリットよりも上記可能性のデメリットが上回っていると思われた方は、方法1,2をおすすめします。

ちなみに筆者は方法3で対応しました。

手順

プラグイン「StructUtls」を有効にします。

このプラグインはデフォルトで有効になっていませんので、UE側で有効にする必要があります。

UEメインウィンドウから[編集]タブを開き、プラグインウィンドウを出してください。

プラグイン検索で「Struct」と入力すると下のほうにStruct Utlsプラグインが表示されます。

左側にあるチェックボックスにチェックを入れてください。

チェックをすると再起動を促されるので、再起動しましょう。

UE側でプラグインを有効にせず、プログラムを組んでしまった場合起動できなくなってしまいます。

そのため、UE側で有効にするのではなく手動で有効にしましょう。

.uproject内のプラグイン枠に下記を追加してください。

,// すでに別のプラグインが追加されている場合は「,」を入れてください。
		{
			"Name": "StructUtils",
			"Enabled": true
		}

追加後、保存することでエディタを起動できるようになります。

プラグイン有効後、自プロジェクトで使用するためにBuild.csを編集します。

プロジェクト名.Build.csを開いてください。

開いたら、 PublicDependencyModuleNames に”StructUtls”を追加してください。

PrivateDependencyModuleNamesの追加でも問題ありません。

以上の作業で、自プロジェクトでStructUtlsが使用できるようになりました。

構造体ポインタ型をメンバ変数宣言していた箇所を、FInstancedStruct型にしたうえでUPROPERTYにします。( “InstancedStruct.h”をインクルードしてください。)

	UPROPERTY(Transient)
	FInstancedStruct 	MyStructPtr;

FInstancedStructの扱いについて以降の項目で解説していきます。

FInstancedStruct に構造体のポインタを設定

ポインタを設定する方法は3つあります。

引数付きコンストラクタで構造体を生成したい場合は、InitializeAs 関数か Make 関数を使用する必要があります。

コンストラクタで設定する方法

UMyTestObject::UMyTestObject()
	: MyStructPtr(FMyStruct::StaticStruct())
{
}

InitializeAs 関数を使用する方法

void UMyTestObject::Init()
{
	MyStructPtr.InitializeAs(FInstancedStruct::StaticStruct());

	// 引数付きコンストラクタで構造体を生成する場合は
	//MyStructPtr.InitializeAs<FInstancedStruct>(/* ここにコンストラクタ引数を入れてください。 */);
}

Make 関数を使用する方法

Make関数はstaticなのでUObjectのコンストラクタでもよぶことも可能です。

UMyTestObject::UMyTestObject()
	: MyStructPtr(FInstancedStruct::Make(FInstancedStruct::StaticStruct()))
	// 引数付きコンストラクタで構造体を生成する場合は
	//: MyStructPtr(FInstancedStruct::Make<FInstancedStruct>(/* ここにコンストラクタ引数を入れてください。 */))
{
}

FInstancedStruct 内の構造体データを取得

constの有無、参照かポインタ型かで呼び出す関数が異なります。

	// const参照型を取得
	const FMyStruct& ConstantMyStructRef = MyStructPtr.Get<FMyStruct>();

	// 非const参照型を取得
	FMyStruct& MyStructRef = MyStructPtr.GetMutable<FMyStruct>();

	// constポインタ型を取得
	const FMyStruct* ConstantMyStructPointer = MyStructPtr.GetPtr<FMyStruct>();

	// 非constポインタ型を取得
	FMyStruct* MyStructPointer = MyStructPtr.GetMutablePtr<FMyStruct>();

FMyStructかFMyStructを継承した構造体が事前に設定されていない場合はcheckに引っかかってクラッシュしますので、ご注意ください。

まとめ

いかがでしたでしょうか。

筆者は方法3で対応しましたが、方法1・2にもメリットはありますので、ケーズバイケースで対応方法を検討していくことをお勧めします。(構造体をポインタで持たせるケースは少ないですが…)

最後までご覧いただき、ありがとうございました!

タイトルとURLをコピーしました