はじめに
スマートポインタとは
突然ですが、スマートポインタとは何でしょうか?
我らの ChatGPT に尋ねてみると以下の回答が返ってきました。
スマートポインタとは、「自動でメモリ管理をしてくれるポインタ」のことです。
ChatGPT
C++などで使われ、プログラマが delete を書かなくても、オブジェクトの寿命に応じて自動でメモリを解放してくれます。
まさにその通りなのですが、言葉だけでは少し分かりづらいので具体例を見ていきましょう!
一般的に C++ で new などを使ってオブジェクトを生成、つまりメモリ領域を確保した場合はそのオブジェクトの使用後に delete などを使ってそのオブジェクトを破棄(メモリを解放)する必要があります。
// new を使ってオブジェクトを生成する
SampleClass* SampleClassPtr = new SampleClass();
//生成したオブジェクトを使用する
SampleClassPtr->SampleFunction();
// delete を使ってオブジェクトを破棄する
delete SampleClassPtr;
new を使ってオブジェクトを生成したときは、メモリ領域の中でもヒープ領域という部分が確保されるのですが、このコードのように new などを使用せずに変数を宣言したり、初期化したりした場合はヒープ領域ではなく、スタック領域という部分にメモリが割り当てられ、そのスコープを抜けると自動でメモリが解放されます。
int32 Num = 1;
そして、生成したオブジェクトが UObject の場合は UE 独自のガベージコレクションにより、その UObject がどこからも参照されなくなると自動でその UObject を破棄してくれるので、デストラクタなどで手動でその UObject を破棄する必要がなくなります。
//コンストラクタ
ASampleActor::ASampleActor()
{
// UObject を生成し、その UObject のポインタをメンバ変数に代入する
SampleObjectPtr = NewObject<UObject>();
}
//デストラクタ
ASampleActor::~ASampleActor()
{
//通常はメンバ変数のポインタの指し示すオブジェクトを破棄する処理を記述する必要があるが、
// UObject はガベージコレクションによって自動で破棄されるため、
//そのような処理を記述する必要は無い
}
このように UObject はガベージコレクションによって管理されていて、ガベージコレクトのタイミングはプロジェクト設定で指定することもできますが、UEngine::ForceGarbageCollection() などを呼び出して任意のタイミングでガベージコレクトさせることもできます。
この UE のガベージコレクションについては当ブログの以下の記事が参考になるかもしれません。
そして、基本的にヒープ領域を確保したオブジェクトのライフタイムを明確に制御するために使用するものがスマートポインタです。
このコードでは Hoge() のスコープを抜けると FSampleStruct 型のオブジェクトが自動で破棄されます。
void ASampleActor::Hoge()
{
// UObject ではないオブジェクトを生成し、そのオブジェクトのポインタを TSharedPtr として保持する
TSharedPtr<FSampleStruct> SampleStructPtr = MakeShared<FSampleStruct>();
//生成したオブジェクトを使用する
SampleStructPtr->SampleFunction();
}
//変数のスコープを抜けると自動でオブジェクトが破棄される
また、UObject が有効かどうかを確認するときはこのコードのように IsValid(UObject*) で確認します。
// UObject
UObject* SampleObjectPtr = NewObject<UObject>();
// IsValid(UObject*) で確認する
if (IsValid(SampleObjectPtr))
{
}
通常のポインタの場合は nullptr と比較して確認するのが一般的ですが、
//生ポインタ
SampleClass* SampleClassPtr = new SampleClass();
// nullptr と比較して確認する
if(SampleClassPtr != nullptr)
{
}
UE のスマートポインタの場合は IsValid() で確認するのが一般的です。
UObject の有効性を確認するときに使用した IsValid() は引数に UObject のポインタを渡す感じでしたが、スマートポインタの場合はそのスマートポインタのメンバ関数を呼び出す感じで使用します。
// UE のスマートポインタ
TSharedPtr<FSampleStruct> SampleStructPtr = MakeShared<FSampleStruct>();
// IsValid() で確認する
if(SampleStructPtr.IsValid())
{
}
スマートポインタを使用すべき場合と使用しなくてもいい場合
ここまででスマートポインタがどのようなものか、なんとなくは分かりましたが、具体的にスマートポインタを使った方がいい場合とそうでない場合の違いは何でしょうか?
以下にその例を書いてみました。
スマートポインタを使用すべき場合
まずはスマートポインタを使った方がいい場合を見ていきましょう。
例えば以下の場合が考えられます。
- とある関数の中で new などを使ってヒープ領域を確保したオブジェクトが関数のスコープを抜けても破棄されずに生存し続けてほしい場合
- 1つのオブジェクトを複数の場所で共有し、そのオブジェクトがどこからも参照されなくなったときに自動でそのオブジェクトを破棄してほしい場合
- 所有権の移動や共有を明確にしてダングリングポインタや二重解放などを事前に防ぎたい場合
スマートポインタを使用しなくてもいい場合
次はスマートポインタを使用しなくてもいい場合の例を見ていきましょう。
例えば以下のものが挙げられます。
- new などを使用することなくオブジェクトを生成した場合(ヒープ領域ではなくスタック領域を確保した場合)
- int や float などのプリミティブ型や非常に単純な構造体などの軽量な値を扱う場合
- new や delete などを使用して既にオブジェクトのライフタイムを明確に制御してある場合
UObject 以外で使用可能なスマートポインタ
では早速、各スマートポインタの特徴や使い方を見ていきましょう!
まずは、UObject を継承していないクラスや構造体に対して使えるスマートポインタから見ていきます。
TUniquePtr
TUniquePtr は所有権を持つ唯一のスマートポインタです。
ポインタをコピーしようとすると「error C2280: ‘TUniquePtr>::TUniquePtr(const TUniquePtr> &)’: attempting to reference a deleted function」といったコンパイルエラーが発生しますが、MoveTemp() を使用することで所有権を渡すことができます。
所有権を渡した後は、元の TUniquePtr は無効(nullptr)になり、有効なポインタが同時に複数存在しない(他の場所から参照されていない)ことが保証されます。
TUniquePtr<FSampleStruct> UniquePtr1 = MakeUnique<FSampleStruct>();
//他の TUniquePtr をコピーして渡そうとするとコンパイルエラーが発生する
TUniquePtr<FSampleStruct> UniquePtr2;// = UniquePtr1;
// MoveTemp() を使って所有権を渡すことでポインタを受け取ることができる
UniquePtr2 = MoveTemp(UniquePtr1);
//所有権を渡した後、元の TUniquePtr は無効になる
if (!UniquePtr1.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("UniquePtr1 is not valid"));
}
//所有権を渡した後、受取先の TUniquePtr は有効になる
if (UniquePtr2.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("UniquePtr2 is valid"));
}
TSharedPtr
次は TSharedPtr です。
TSharedPtr はポインタをコピーして共有することができ、そのオブジェクトを共有している数(参照カウント)が 0 になるまでオブジェクトを生存させ続け、参照カウントが 0 になる(そのオブジェクトがどこからも参照されなくなる)と自動でそのオブジェクトを破棄してくれます。
TSharedPtr<FSampleStruct> SharedPtr1 = MakeShared<FSampleStruct>();
// TSharedPtr はコピーして共有することができる
TSharedPtr<FSampleStruct> SharedPtr2 = SharedPtr1;
// GetSharedReferenceCount() で参照カウントを確認できる
//ここでは 2 と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
//ラムダ式のキャプチャでも参照カウントは増える
auto Callback = [SharedPtr2]()
{
//参照カウントが 0 になるまでオブジェクトは破棄されないため、
//ここでも SharedPtr2 は有効
if (SharedPtr2.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("SharedPtr2 is valid"));
}
};
//ここでは 3 と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
循環参照の例 1
TSharedPtr はそのオブジェクトのライフタイムに影響を与えられる存在が複数いるので、循環参照という状態に陥るリスクがあります。
具体例を見ていきましょう。
このコードでは TSharedPtr は正常に機能し、TSharedPtr のスコープを抜けて参照カウントが 0 になるとオブジェクトを自動的に破棄してくれます。
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
// TWeakPtr はオブジェクトのライフタイムに影響を与えない
TWeakPtr<FSampleStruct> WeakPtr = SharedPtr.ToWeakPtr();
auto Callback = [WeakPtr]()
{
//このラムダ式が実行される頃には TSharedPtr のスコープを抜けているため、
//この条件の結果は false になる
if (WeakPtr.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakPtr is valid"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("WeakPtr is not valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
しかし、以下のコードのように構造体のメンバに自分自身の TSharedPtr を持たせると循環参照が発生し、TSharedPtr のスコープを抜けても永遠に参照カウントが 0 にならず、オブジェクトが破棄されなくなってしまいます。
USTRUCT()
struct SAMPLE_API FSampleStruct
{
GENERATED_BODY()
public:
TSharedPtr<FSampleStruct> MyselfSharedPtr; //追加
};
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
//循環参照を引き起こさせる
SharedPtr->MyselfSharedPtr = SharedPtr;
TWeakPtr<FSampleStruct> WeakPtr = SharedPtr.ToWeakPtr();
auto Callback = [WeakPtr]()
{
//循環参照によって永遠に参照カウントが 0 にならないため、
//この条件の結果は true になる
if (WeakPtr.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakPtr is valid"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("WeakPtr is not valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
我らのいらすとやでこの状況を再現するとこのようになります。
真顔から発せられる攻撃的な言葉ほど怖いものはありませんね。

循環参照の例 2
他にも循環参照が発生してしまう場合があります。
例えば、以下のコードの場合でも循環参照が発生し、オブジェクトが永遠に破棄されない状態になってしまいます。
USTRUCT()
struct SAMPLE_API FSampleStructA
{
GENERATED_BODY()
public:
TSharedPtr<FSampleStructB> SharedPtrB;
};
USTRUCT()
struct SAMPLE_API FSampleStructB
{
GENERATED_BODY()
public:
TSharedPtr<FSampleStructA> ASharedPtrA;
};
//オブジェクトを生成してそのポインタを TSharedPtr として保持する
TSharedPtr<FSampleStructA> LocalSharedPtrA = MakeShared<FSampleStructA>();
TSharedPtr<FSampleStructB> LocalSharedPtrB = MakeShared<FSampleStructB>();
//各オブジェクトのポインタをお互いに TSharedPtr で保持させる
//(循環参照が発生する)
LocalSharedPtrA->SharedPtrB = LocalSharedPtrB;
LocalSharedPtrB->SharedPtrA = LocalSharedPtrA;
//各オブジェクトのポインタを TWeakPtr として保持する
//(各オブジェクトのライフタイムに影響は無い)
TWeakPtr<FSampleStructA> WeakPtrA = LocalSharedPtrA.ToWeakPtr();
TWeakPtr<FSampleStructB> WeakPtrB = LocalSharedPtrB.ToWeakPtr();
auto Callback = [WeakPtrA, WeakPtrB]()
{
//生成した FSampleStructA は破棄されず、ここでも有効
if (WeakPtrA.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakPtrA is valid"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("WeakPtrA is not valid"));
}
//生成した FSampleStructB も破棄されず、ここでも有効
if (WeakPtrB.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakPtrB is valid"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("WeakPtrB is not valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
こちらも我らのいらすとやで再現すると、このような感じになります。
お二人とも幸せそうで何よりです。

TSharedRef
次は TSharedRef です。
TSharedRef の機能は TSharedPtr のそれとほぼ同じですが、TSharedPtr とは違って TSharedRef には nullptr を代入することができず、常に有効なポインタが格納されています。
なので、生成直後のオブジェクトや破棄されることの無いオブジェクトに対して使用可能なスマートポインタであり、IsValid() は用意されていません。
// TSharedPtr には nullptr を代入できる
TSharedPtr<FSampleStruct> SharedPtr = nullptr;
// TSharedRef に nullptr を代入しようとするとコンパイルエラーが発生する
// TSharedRef<FSampleStruct> SharedRef = nullptr;
// TSharedRef では IsValid() が定義されていないため、このコードではコンパイルエラーが発生する
if(SharedRef.IsValid())
{
}
TWeakPtr
次は TWeakPtr です。
TWeakPtr は TSharedPtr からポインタをコピーして受け取ることができますが、TSharedPtr とは違って所有権を共有しているわけではないので、TWeakPtr が TSharedPtr からポインタをコピーして受け取ったとしても参照カウントは増えません!
TSharedPtr<FSampleStruct> SharedPtr1 = MakeShared<FSampleStruct>();
//ここでは 1 と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
// TSharedPtr にコピーすると参照カウントは増える
TSharedPtr<FSampleStruct> SharedPtr2 = SharedPtr1;
//ここでは 2 と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
// TWeakPtr にコピーしても参照カウントは増えない
TWeakPtr<FSampleStruct> WeakPtr = SharedPtr1.ToWeakPtr();
//ここでは 3 ではなく 2 と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
このように TWeakPtr はオブジェクトのライフタイムに影響を与えないので、オブジェクトのライフタイムに関与したくない場合や、循環参照を事前に防ぎたい場合に使います。
USTRUCT()
struct SAMPLE_API FSampleStruct
{
GENERATED_BODY()
public:
TWeakPtr<FSampleStruct> MyselfWeakPtr; //追加
};
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
// MyselfWeakPtr 変数が TSharedPtr の場合はここで循環参照が発生するが、
//今回、MyselfWeakPtr 変数は TWeakPtr であるため、循環参照は発生しない
SharedPtr->MyselfWeakPtr = SharedPtr.ToWeakPtr();
TWeakPtr<FSampleStruct> WeakPtr = SharedPtr.ToWeakPtr();
auto Callback = [WeakPtr]()
{
// TWeakPtr はオブジェクトのライフタイムに影響を与えないため、
//この条件の結果は true になる
if (!WeakPtr.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakPtr is not valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
UObject で使用可能なスマートポインタ
次は UObject を継承したクラスに対して使用可能なスマートポインタを見ていきましょう。
TObjectPtr
まずは TObjectPtr です。
TObjectPtr は UE5 から新たに導入されたスマートポインタで、UObject に対して使用できます。
挙動は生ポインタとほぼ同じですが、UPROPERTY() を付ける場合においては生ポインタよりも TObjectPtr を使用することが推奨されています。
UPROPERTY()
TObjectPtr<UObject> SampleObjectPtr;
TWeakObjectPtr
次は TWeakObjectPtr です。
これは名前からも分かるように TWeakPtr と同様にオブジェクトのライフタイムに影響を与えないスマートポインタです。
具体例を見ていきましょう。
まずは UPROPERTY() を付けずに TObjectPtr 型と TWeakObjectPtr 型のメンバ変数を宣言します。
UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
GENERATED_BODY()
public:
ASampleActor();
protected:
virtual void BeginPlay() override;
private:
TObjectPtr<UObject> ObjectPtr; // UPROPERTY() 無しの TObjectPtr
TWeakObjectPtr<UObject> WeakObjectPtr; // UPROPERTY() 無しの TWeakObjectPtr
void CheckObjectPtr() const; // TObjectPtr の状態を確認する
void CheckWeakObjectPtr() const; // TWeakObjectPtr の状態を確認する
public:
virtual void Tick(float DeltaTime) override;
};
次に BeginPlay() でUObject を生成してそのポインタを各メンバ変数に代入し、一定時間後に各メンバ変数の状態を確認します。
ASampleActor::ASampleActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void ASampleActor::BeginPlay()
{
Super::BeginPlay();
//スポーンさせたアクタをメンバ変数に保持する
ObjectPtr = NewObject<UObject>();;
WeakObjectPtr = ObjectPtr;
auto GarbageCollectLambda = []()
{
if (GEngine)
{
//ガベージコレクトする
GEngine->ForceGarbageCollection(true);
}
};
// 1 秒後にガベージコレクトする
FTimerHandle GarbageCollectTimerHandle;
GetWorld()->GetTimerManager().SetTimer(GarbageCollectTimerHandle, GarbageCollectLambda, 1.f, false);
// 2 秒後に TObjectPtr の状態を確認する
FTimerHandle ObjectPtrTimerHandle;
GetWorld()->GetTimerManager().SetTimer(ObjectPtrTimerHandle, this, &ASampleActor::CheckObjectPtr, 2.f, false);
// 3 秒後に TWeakObjectPtr の状態を確認する
FTimerHandle WeakObjectPtrTimerHandle;
GetWorld()->GetTimerManager().SetTimer(WeakObjectPtrTimerHandle, this, &ASampleActor::CheckWeakObjectPtr, 3.f, false);
}
void ASampleActor::CheckObjectPtr() const
{
if (!IsValid(ObjectPtr))
{
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is not valid"));
return;
}
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is valid"));
if (ObjectPtr == nullptr)
{
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is nullptr"));
return;
}
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is not nullptr"));
if (!ObjectPtr->IsValidLowLevel())
{
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is not valid low level"));
return;
}
UE_LOG(LogTemp, Log, TEXT("ObjectPtr is valid low level"));
}
void ASampleActor::CheckWeakObjectPtr() const
{
if (!WeakObjectPtr.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is not valid"));
return;
}
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is valid"));
if (WeakObjectPtr == nullptr)
{
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is nullptr"));
return;
}
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is not nullptr"));
if (!WeakObjectPtr->IsValidLowLevel())
{
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is not valid low level"));
return;
}
UE_LOG(LogTemp, Log, TEXT("WeakObjectPtr is valid low level"));
}
void ASampleActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
少しコードが長くなってしまいましたが、このコードではログに以下のように表示されます。
ObjectPtr is valid
ObjectPtr is not nullptr
ObjectPtr is not valid low level
WeakObjectPtr is not valid
これはつまり、UObject の生成直後にそのポインタを UPROPERTY() の付いていないメンバ変数に代入したので、その UObject がどこからも参照されていないという状態になり、ガベージコレクタによって破棄されてしまいました。
なので TObjectPtr はダングリングポインタになってしまいましたが、TWeakObjectPtr にはしっかりと nullptr が代入されています。
この UObject に安全にアクセスする(ダングリングポインタを回避する)には上記のコードのように TWeakObjectPtr を使用してもいいのですが、元から TObjectPtr に UPROPERTY() を付けることによって「その UObject は参照されている」ということをガベージコレクタに伝えて、ガベージコレクトのタイミングで破棄されるのを事前に防ぐことができます。
UPROPERTY()
TObjectPtr<UObject> ObjectPtr;
TWeakObjectPtr<UObject> WeakObjectPtr;
ObjectPtr is valid
ObjectPtr is not nullptr
ObjectPtr is valid low level
WeakObjectPtr is valid
WeakObjectPtr is not nullptr
WeakObjectPtr is valid low level
逆に TObjectPtr には UPROPERTY() を付けずに、TWeakObjectPtr に UPROPERTY() を付けてもその UObject は参照されていることにならないので、その UObject はガベージコレクタによって破棄されて TObjectPtr はダングリングポインタになってしまいます。
TObjectPtr<UObject> ObjectPtr;
UPROPERTY()
TWeakObjectPtr<UObject> WeakObjectPtr;
ObjectPtr is valid
ObjectPtr is not nullptr
ObjectPtr is not valid low level
WeakObjectPtr is not valid
これらの特徴からもわかるように TWeakObjectPtr は UObject のライフタイムに影響を与える(UObject を強く参照する)ことなく、その UObject に安全にアクセスしたいときに使います。
我らのいらすとやでわかりやすく表現するとこのようになります。
トイレットペーパーという名のオブジェクトが無くなっちゃうのは構わないけど、無くなったことに気付かないのがダングリングポインタで、無くなったことにしっかり気付いて焦ることができるのが TWeakObjectPtr です。

その他
主要なスマートポインタの紹介は以上です。
ここからはスマートポインタを使うにあたって覚えておいた方がいいことを紹介します。
MakeShared() と MakeShareable() の違い
基本的に TSharedPtr や TSharedRef を作成するときは MakeShared() か MakeShareable() のどちらかを使います。
それぞれの特徴を見ていきましょう。
MakeShared()
まずは MakeShared() です。
基本的に MakeShared() は何も無い状態から TSharedPtr を作成したいときに使用します。
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
MakeShareable()
次は MakeShareable() です。
MakeShareable() は既にオブジェクトが存在し、そのオブジェクトのポインタを TSharedPtr に変換(ラップ)したいときに使用します。
// TSharedPtr を作成する前から存在するオブジェクトのポインタ
FSampleStruct* SampleStructPtr = new FSampleStruct();
//既に存在するオブジェクトのポインタを TSharedPtr にラップする
TSharedPtr<FSampleStruct> SharedPtr = MakeShareable(SampleStructPtr);
基本的な使い分けはこんな感じなのですが、エンジンのソースコードを読んでいると MakeShareable() の引数の中で new を使ってオブジェクトを生成しているものをよくみかけるのでこの点に関しては調査中です。
TSharedPtr と TSharedRef の相互変換
次は TSharedPtr と TSharedRef の相互変換の方法を紹介します。
TSharedRef → TSharedPtr
まずは TSharedRef から TSharedPtr への変換です。
TSharedPtr では nullptr も許容されるので、そのまま暗黙的に変換することができます。
TSharedRef<FSampleStruct> SharedRef = MakeShared<FSampleStruct>();
//そのまま暗黙的に変換できる
TSharedPtr<FSampleStruct> SharedPtr = SharedRef;
TSharedPtr → TSharedRef
次は TSharedPtr から TSharedRef への変換についてです。
TSharedPtr では nullptr が許容されていますが、TSharedRef では nullptr が許容されていないので、TSharedPtr から TSharedRef に変換するときは IsValid() で TSharedPtr が有効か確認した後に ToSharedRef() で変換する必要があります。
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
// TSharedPtr が有効か確認
if (SharedPtr.IsValid())
{
// ToSharedRef() で TSharedRef に変換する
TSharedRef<FSampleStruct> SharedRef = SharedPtr.ToSharedRef();
}
TSharedPtr と TSharedRef のキャスト
次は TSharedPtr と TSharedRef をキャストする方法を紹介します。
StaticCastSharedPtr() / StaticCastSharedRef()
まずは StaticCastSharedPtr() と StaticCastSharedRef() です。
TSharedPtr と TSharedRef のキャストというと、大抵の場合はこの2つを使います。
具体例を見ていきましょう。
まずは以下の2つのクラスを定義します。
//親クラス
class FParentClass
{
};
//子クラス
class FChildClass : public FParentClass
{
public:
void ChildClassFunction()
{
};
};
以下のコードのように StaticCastSharedPtr() を使用することで TSharedPtr を、StaticCastSharedRef() を使用することで TSharedRef をダウンキャストすることができます。
// FChildClass を生成して FParentClass として取得する
TSharedPtr<FParentClass> ParentClassSharedPtr = MakeShared<FChildClass>();
// FParentClass として保持している FChildClass を FChildClass にダウンキャストする
TSharedPtr<FChildClass> ChildClassSharedPtr = StaticCastSharedPtr<FChildClass>(ParentClassSharedPtr);
//正常に動作する
ChildClassSharedPtr->ChildClassFunction();
しかし、StaticCastSharedPtr() と StaticCastSharedRef() はダウンキャストに失敗しても nullptr にならないので、このコードでは未定義動作を引き起こします。
// FParentClass を生成して FParentClass として取得する
TSharedPtr<FParentClass> ParentClassSharedPtr = MakeShared<FParentClass>();
//生成した FParentClass を FChildClass にダウンキャストする
//実際はダウンキャストできていない
TSharedPtr<FChildClass> ChildClassSharedPtr = StaticCastSharedPtr<FChildClass>(ParentClassSharedPtr);
// IsValid() でも true が返ってきてしまう
if (ChildClassSharedPtr.IsValid())
{
//未定義動作を引き起こす
ChildClassSharedPtr->ChildClassFunction();
}
ConstCastSharedPtr() / ConstCastSharedRef()
次は ConstCastSharedPtr() と ConstCastSharedRef() です。
ConstCastSharedPtr() を使用することで、const の付いた(編集できない)TSharedPtr を編集可能(mutable)な TSharedPtr に変換することができます。
// const の付いた TSharedPtr を作成する
TSharedPtr<const FSampleStruct> ConstSharedPtr = MakeShared<const FSampleStruct>();
//「Error C3892 : 'ConstSharedPtr': you cannot assign to a variable that is const」というコンパイルエラーが発生する
ConstSharedPtr->Num = 1;
// const な TSharedPtr から、編集可能な TSharedPtr に変換する
TSharedPtr<FSampleStruct> SharedPtr = ConstCastSharedPtr<FSampleStruct>(ConstSharedPtr);
//コンパイルエラーは発生しない
SharedPtr->Num = 1;
また、TSharedRef の場合は ConstCastSharedRef() を使用します。
// const の付いた TSharedRef を作成する
TSharedRef<const FSampleStruct> ConstSharedRef = MakeShared<const FSampleStruct>();
//「Error C3892 : 'ConstSharedRef': you cannot assign to a variable that is const」というコンパイルエラーが発生する
ConstSharedRef->Num = 1;
// const な TSharedRef から、編集可能な TSharedRef に変換する
TSharedRef<FSampleStruct> SharedRef = ConstCastSharedRef<FSampleStruct>(ConstSharedRef);
//コンパイルエラーは発生しない
SharedRef->Num = 1;
TSharedFromThis
次は TSharedFromThis について説明します。
これは簡単に説明すると、そのクラスや構造体のオブジェクトが自分自身を TSharedPtr 化するときに使用するテンプレートクラスです。
TSharedFromThis を使用した場合と使用しなかった場合ではかなり大きな違いが生まれてしまうので具体例を交えて確認してみましょう。
TSharedFromThis を使用しなかった場合
例えば、とあるオブジェクトが自分自身の TSharedPtr を作成するときに、このコードのように MakeShareable(this) という方法を用いたとします。
class SAMPLE_API FSampleClass
{
public:
TSharedPtr<FSampleClass> AsSharedByIncorrectWay()
{
return MakeShareable(this);
};
};
// MakeShared() で TSharedPtr を作成し、もう1つの TSharedPtr にコピーする
TSharedPtr<FSampleClass> CorrectSharedPtr1 = MakeShared<FSampleClass>();
TSharedPtr<FSampleClass> CorrectSharedPtr2 = CorrectSharedPtr1;
//正しい方法で作成した TSharedPtr の参照カウントはここでは 2
UE_LOG(LogTemp, Log, TEXT("%d"), CorrectSharedPtr1.GetSharedReferenceCount());
//同じオブジェクトの TSharedPtr を正しくない方法で作成する
TSharedPtr<FSampleClass> IncorrectSharedPtr = CorrectSharedPtr1->AsSharedByIncorrectWay();
//正しくない方法で作成した TSharedPtr の参照カウントはここでは 1 であり、
//既存の参照カウントとは別の独立した参照カウントになってしまっている
UE_LOG(LogTemp, Log, TEXT("%d"), IncorrectSharedPtr.GetSharedReferenceCount());
//正しい方法で作成した TSharedPtr の参照カウントは変わらず、
//ここでも 2 のまま
UE_LOG(LogTemp, Log, TEXT("%d"), CorrectSharedPtr1.GetSharedReferenceCount());
//正しくない方法で作成した TSharedPtr をキャプチャする
auto Callback = [IncorrectSharedPtr]()
{
//正しくない方法で作成した TSharedPtr は有効という判定になるが、
//実際はダングリングポインタ
if (IncorrectSharedPtr.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("IncorrectSharedPtr is valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
この方法でオブジェクト自身を TSharedPtr 化するとダングリングポインタが発生し、この例ではゲーム終了時にメモリの二重開放が発生してクラッシュしてしまいます。

TSharedFromThis を使用した場合
次は TSharedFromThis を使用した場合を考えます。
TSharedFromThis を使用するときはこのコードのようにそのクラスに TSharedFromThis を継承させます。
// TSharedFromThis を継承させる
class SAMPLE_API FSampleClass : public TSharedFromThis<FSampleClass>
{
};
以下のコードのように TSharedFromThis::AsShared() で自分自身の TSharedPtr を作成すると、ダングリングポインタもメモリの二重開放も事前に防ぐことができるので、この例ではゲーム終了時にクラッシュすることはありません。
// MakeShared() で TSharedPtr を作成し、もう1つの TSharedPtr にコピーする
TSharedPtr<FSampleClass> SharedPtr1 = MakeShared<FSampleClass>();
TSharedPtr<FSampleClass> SharedPtr2 = SharedPtr1;
// MakeShared() で作成した TSharedPtr の参照カウントはここでは 2
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
//同じオブジェクトの TSharedPtr を AsShared() で作成する
TSharedPtr<FSampleClass> SharedPtr3 = SharedPtr1->AsShared();
// AsShared() で作成した TSharedPtr の参照カウントはここでは 3 であり、
//既存の参照カウントがしっかり反映されている
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr3.GetSharedReferenceCount());
// MakeShared() で作成した TSharedPtr の参照カウントもここでは 3
UE_LOG(LogTemp, Log, TEXT("%d"), SharedPtr1.GetSharedReferenceCount());
// AsShared() で作成した TSharedPtr をキャプチャする
auto Callback = [SharedPtr3]()
{
// AsShared() で作成した TSharedPtr はここでもしっかり有効で、ダングリングポインタではない
if (SharedPtr3.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("SharedPtr3 is valid"));
}
};
// 3 秒後にラムダ式を実行する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, Callback, 3.f, false);
また、TSharedFromThis を継承したクラスを更に継承した子クラスが自分自身の TSharedPtr を子クラスの型で取得するときは TSharedFromThis::AsShared() ではなく、TSharedFromThis::SharedThis() を使用します。
class SAMPLE_API FSampleClass : public TSharedFromThis<FSampleClass>
{
};
class SAMPLE_API FSampleChildClass : public FSampleClass
{
public:
TSharedPtr<FSampleChildClass> GetAsSharedPtr()
{
// TSharedFromThis::AsShared() では親クラスの型(FSampleClass)で返ってきてしまうため、
// TSharedFromThis::SharedThis() を使用する
return SharedThis(this);
}
};
ESPMode::ThreadSafe と ESPMode::NotThreadSafe
次は ESPMode::ThreadSafe と ESPMode::NotThreadSafe について説明します。
TSharedPtr 型の変数を宣言するときにテンプレート引数で ESPMode::ThreadSafe を指定するとスレッドセーフに、ESPMode::NotThreadSafe を指定すると非スレッドセーフにすることができます。
//スレッドセーフ
TSharedPtr<FSampleStruct, ESPMode::ThreadSafe> ThreadSafeSharedPtr;
//非スレッドセーフ
TSharedPtr<FSampleStruct, ESPMode::NotThreadSafe> NotThreadSafeSharedPtr;
スレッドセーフと非スレッドセーフの違いを実際の挙動を通して確認してみましょう。
このコードではスレッドセーフな TSharedPtr と非スレッドセーフな TSharedPtr を作成して、それらの参照カウントをゲームスレッドとゲームスレッド以外のスレッドで同時に操作し、参照カウントの操作終了後にゲームスレッドで各 TSharedPtr の参照カウントを確認しています。
//スレッドセーフな TSharedPtr を作成する
TSharedPtr<FSampleStruct, ESPMode::ThreadSafe> ThreadSafeSharedPtr = MakeShared<FSampleStruct, ESPMode::ThreadSafe>();
//非スレッドセーフな TSharedPtr を作成する
TSharedPtr<FSampleStruct, ESPMode::NotThreadSafe> NotThreadSafeSharedPtr = MakeShared<FSampleStruct, ESPMode::NotThreadSafe>();
//ゲームスレッド以外のスレッドで実行される処理
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ThreadSafeSharedPtr, NotThreadSafeSharedPtr]()
{
TArray<TSharedPtr<FSampleStruct, ESPMode::ThreadSafe>> ThreadSafeSharedPtrArray;
TArray<TSharedPtr<FSampleStruct, ESPMode::NotThreadSafe>> NotThreadSafeSharedPtrArray;
for (int32 i = 0; i < 10000; i++)
{
//ひたすら参照カウントを増やす
ThreadSafeSharedPtrArray.Add(ThreadSafeSharedPtr);
NotThreadSafeSharedPtrArray.Add(NotThreadSafeSharedPtr);
}
});
//ゲームスレッドで実行される処理
{
TArray<TSharedPtr<FSampleStruct, ESPMode::ThreadSafe>> ThreadSafeSharedPtrArray;
TArray<TSharedPtr<FSampleStruct, ESPMode::NotThreadSafe>> NotThreadSafeSharedPtrArray;
for (int32 i = 0; i < 10000; i++)
{
//ひたすら参照カウントを増やす
ThreadSafeSharedPtrArray.Add(ThreadSafeSharedPtr);
NotThreadSafeSharedPtrArray.Add(NotThreadSafeSharedPtr);
}
}
//複数スレッドでの参照カウントの操作が終わっている 3 秒後に各 TSharedPtr の参照カウントを確認する
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, [ThreadSafeSharedPtr, NotThreadSafeSharedPtr]()
{
UE_LOG(LogTemp, Log, TEXT("ThreadSafeSharedPtr: %d"), ThreadSafeSharedPtr.GetSharedReferenceCount());
UE_LOG(LogTemp, Log, TEXT("NotThreadSafeSharedPtr: %d"), NotThreadSafeSharedPtr.GetSharedReferenceCount());
},
3.f, false);
このとき、スレッドセーフな TSharedPtr の参照カウントは 1 という正常な値が表示されますが、非スレッドセーフな TSharedPtr には毎回異なる巨大な値が表示されます。
ThreadSafeSharedPtr: 1
NotThreadSafeSharedPtr: 3604536
ThreadSafeSharedPtr: 1
NotThreadSafeSharedPtr: 1777315584
このようにスレッドセーフな TSharedPtr を使用することで複数スレッドから同時に正しく参照カウントを操作できるようになりますが、公式ドキュメントや UE のソースコードに書かれているように複数スレッドからアクセスされないことが保証されている場合は非スレッドセーフな TSharedPtr を使用した方がパフォーマンスが向上します。
If you know your pointer will never be accessed by more than one thread, you can get better performance by avoiding the thread-safe versions.
Unreal Smart Pointer Library

TSharedPtr はデフォルトでスレッドセーフなのか
公式ドキュメントでは以下のように、スマートポインタはデフォルトでは 1 つのスレッドからのみアクセスでき、複数のスレッドからアクセスする場合はスレッドセーフなテンプレートクラスを使用すべきだというふうに書かれています。
By default, Smart Pointers are only safe to access on a single thread. If you need multiple threads to have access, use the thread-safe versions of Smart Pointer classes:
Unreal Smart Pointer Library
先ほど、スレッドセーフな TSharedPtr を作成するときにこの公式ドキュメントの記述をもとにしてテンプレート引数で ESPMode::ThreadSafe を指定しましたが、実際はわざわざテンプレート引数でそのように指定しなくても TSharedPtr はデフォルトでスレッドセーフです。
//スレッドセーフ
TSharedPtr<FSampleStruct> ThreadSafeSharedPtr;
UE の GitHub リポジトリの履歴を遡って調べてみると 2021 年 4 月 26 日に「TSharedPtr is now thread-safe by default.」というコミットがされており、このコミットを境に TSharedPtr はデフォルトでスレッドセーフに変わったのかもしれません。
(コミット ID「7814fec1d186abdf8ba0be8b006180174a883eb6」)
具体的な UE のバージョンでいうと 5.0.0-early-access-2 でのデフォルトは ESPMode::Fast、5.0.0-preview-1 でのデフォルトは ESPMode::ThreadSafe になっているように見えます。


5.0.0-early-access-2 時点での Engine/Source/Runtime/Core/Public/Templates/SharedPointerInternals.h をより詳しく見てみると、ESPMode::Fast は FORCE_THREADSAFE_SHAREDPTRS というマクロの値によってスレッドセーフにするか、非スレッドセーフにするかを切り替えているようです。
/**
* ESPMode is used select between either 'fast' or 'thread safe' shared pointer types.
* This is only used by templates at compile time to generate one code path or another.
*/
enum class ESPMode
{
/** Forced to be not thread-safe. */
NotThreadSafe = 0,
/**
* Fast, doesn't ever use atomic interlocks.
* Some code requires that all shared pointers are thread-safe.
* It's better to change it here, instead of replacing ESPMode::Fast to ESPMode::ThreadSafe throughout the code.
*/
Fast = FORCE_THREADSAFE_SHAREDPTRS ? 1 : 0,
/** Conditionally thread-safe, never spin locks, but slower */
ThreadSafe = 1
};
この辺りに関しては Epic Games Japan の鈴木様の「CharacterMovementをマルチスレッド化してみよう」という記事で少し説明されているので、そちらも見ていただけると良いかと思います。
肝心の方法はTarget.csを変更して
CharacterMovementをマルチスレッド化してみようPLATFORM_WEAKLY_CONSISTENT_MEMORY 1
のマクロを設定するか、以下の様にエンジンを直接変更します。(どちらにせよリビルドが必要です)
ちなみに 5.2.0-preview-1 以降では Engine/Source/Runtime/Core/Public/Templates/SharedPointerFwd.h に ESPMode の定義などが書かれており、「By default, thread safety features are turned on.」とも書かれています。
より詳しく見たい方でまだ GitHub の @EpicGames organization に参加していない方は「GitHub で Unreal Engine のソースコードにアクセス」という公式ドキュメントを参考にして @EpicGames organization に参加しましょう!
最後に
参考記事
- UE5 C++でのPROPERTY変数のポインタの扱いについて(TObjectPtr)
- Incremental Garbage Collection
- UE5:Unreal Engineのポインタについてまとめた 中編
- Why should I replace raw pointers with TObjectPtr?
- [UE4] GC(ガベージコレクション)の基本
- UObjectの動作原理
- Unreal スマート ポインタ ライブラリ
- 共有の参照
- [UE5 C++] UPROPERTYとGCについて整理
- CharacterMovementをマルチスレッド化してみよう