はじめに
スマートポインタの概要
通常、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 のガベージコレクションについては以下の記事が参考になる。
そして、基本的にヒープ領域を確保したオブジェクトのライフタイムを明確に制御するために使用するものがスマートポインタである。
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()」で確認するのが一般的である。
(一部「IsValid()」が定義されていないスマートポインタもある)
//スマートポインタ
TSharedPtr<FSampleStruct> SampleStructPtr = MakeShared<FSampleStruct>();
//「{スマートポインタ}.IsValid()」で確認する
if(SampleStructPtr.IsValid())
{
}
スマートポインタを使用すべき場合と使用しなくても良い場合
スマートポインタを使用すべき場合
- とある関数内で new 等を用いてヒープ領域を確保したオブジェクトが関数のスコープを抜けても破棄されずに生存し続けて欲しい場合
- 1つのオブジェクトを複数の場所で共有し、そのオブジェクトがどこからも参照されなくなった時に自動でそのオブジェクトを破棄して欲しい場合
- 所有権の移動や共有を明確にしてダングリングポインタや二重解放などを事前に防ぎたい場合
スマートポインタを使用しなくても良い場合
- new 等を用いる事なくオブジェクトを生成した場合(ヒープ領域ではなくスタック領域を確保した場合)
- int や float 等のプリミティブ型や非常に単純な構造体などの軽量な値を扱う場合
- new や delete 等を用いて既にオブジェクトのライフタイムを明確に制御してある場合
UObject 以外で使用可能なスマートポインタ
TUniquePtr
所有権を持つ唯一のスマートポインタであり、ポインタをコピーしようとすると「error C2280: ‘TUniquePtr>::TUniquePtr(const TUniquePtr> &)’: attempting to reference a deleted function」というコンパイルエラーが発生するが、「MoveTemp()」 を使用する事で所有権を移す事が出来る。
所有権を渡した後、元のポインタは無効になり、有効なポインタが同時に複数存在しない(他の場所から参照されていない)事が保証される。
TUniquePtr<FSampleStruct> UniquePtr1 = MakeUnique<FSampleStruct>();
//他の TUniquePtr をコピーして渡そうとするとコンパイルエラーが発生する
TUniquePtr<FSampleStruct> UniquePtr2;// = UniquePtr1;
//「MoveTemp()」を使用して所有権を渡す事でポインタを受け取る事が出来る
UniquePtr2 = MoveTemp(UniquePtr1);
//所有権を渡した後、元のポインタは無効になる
if (!UniquePtr1.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("UniquePtr1 is not valid"));
}
//所有権を渡した後、受取先のポインタは有効になる
if (UniquePtr2.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("UniquePtr2 is valid"));
}
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 になるまでオブジェクトは破棄されないため、ここでも有効
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 のスコープを抜けて参照カウントが 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
TSharedPtr と機能はほぼ同じだが、TSharedPtr とは違って TSharedRef には nullptr を代入する事が出来ず、常に有効なポインタが格納されているため、生成直後のオブジェクトや破棄される事の無いオブジェクトに対して使用可能なスマートポインタである。
故に「IsValid()」は用意されていない。
// TSharedPtr には nullptr を代入できる
TSharedPtr<FSampleStruct> SharedPtr = nullptr;
// TSharedRef に nullptr を代入しようとするとコンパイルエラーが発生する
TSharedRef<FSampleStruct> SharedRef = nullptr;
// TSharedRef に「IsValid()」の定義は無いため、このコードではコンパイルエラーが発生する
if(SharedRef.IsValid())
{
}
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 で使用可能なスマートポインタ
TObjectPtr
UE5 から新たに導入されたスマートポインタであり、UObject に対して使用できる。
挙動は生ポインタとほぼ同じだが、「UPROPERTY()」においては生ポインタよりも TObjectPtr を使用する事が推奨されている。
UPROPERTY()
TObjectPtr<UObject> SampleObjectPtr;
TWeakObjectPtr
以下のコードのように「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;
};
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 に安全にアクセスしたい場合に使用する。

その他
「MakeShared()」と「MakeShareable()」の違い
MakeShared()
主に「MakeShared()」は何も無い状態から TSharedPtr を作成したい時に使用する。
TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();
MakeShareable()
主に「MakeShareable()」は既にオブジェクトが存在し、そのオブジェクトのポインタを TSharedPtr に変換(ラップ)したい時に使用する。
// TSharedPtr を作成する前から存在するオブジェクトのポインタ
FSampleStruct* SampleStructPtr = new FSampleStruct();
//既に存在するオブジェクトのポインタを TSharedPtr にラップする
TSharedPtr<FSampleStruct> SharedPtr = MakeShareable(SampleStructPtr);
TSharedPtr と TSharedRef の相互変換
TSharedRef → TSharedPtr
TSharedPtr では nullptr も許容されるため、そのまま暗黙的に変換できる。
TSharedRef<FSampleStruct> SharedRef = MakeShared<FSampleStruct>();
//そのまま暗黙的に変換できる
TSharedPtr<FSampleStruct> SharedPtr = SharedRef;
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 におけるキャスト
StaticCastSharedPtr / StaticCastSharedRef
以下の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()」を使用する事で、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 を作成する時、以下のコードのように「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);
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()」では親クラスの型で返ってきてしまうため、
//「TSharedFromThis::SharedThis()」を使用する
return SharedThis(this);
}
};
「ESPMode::ThreadSafe」と「ESPMode::NotThreadSafe」
概要
以下のコードのように TSharedPtr 型の変数を宣言する時にテンプレート引数で「ESPMode::ThreadSafe」を指定するとスレッドセーフに、「ESPMode::NotThreadSafe」を指定するとその TSharedPtr 型の変数を非スレッドセーフにする事が出来る。
//スレッドセーフ
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」に参加しましょう!
最後に
記事中の主な用語
ポインタ
変数の値はコンピュータのメモリに保存される。
C++ポインタまとめ
アドレスは値が格納されているメモリの位置を表す。
ポインタ変数、またはポインタとは、アドレスを保持する変数のこと。
メモリ領域の確保
プログラムさんが「メモリのここの部分は今から僕が使うからね。他の人は使わないでね」と場所取りをすることです。
メモリ領域の確保とは
メモリの解放
プログラムさんが「メモリのここの部分は今から僕が使うからね。他の人は使わないでね」と場所取りしていたメモリ(の一部)を「ここはもう使わないから他の人が使っていいよ!」と、誰でも使える状態に戻すことです。
メモリの解放とは
スコープ
「範囲」をカッコ付けて言った表現。
スコープとは
あるいはプログラミングの世界で登場する用語で「おまえの名前が通じるのは、ここまでだ!」な影響範囲のことです。
循環参照
「その指示に従うと、ずっとたらい回しにされますよ!」な状態のこと。
循環参照とは
もう少し具体的に書くとAには「Bを見ろ」と書いてあって、Bには「Aを見ろ」と書いてあるように、いつまでたってもゴールに辿りつけないような指示になっていることです。
マルチスレッド
処理の開始から終了まで線を引いたときに、枝分かれの発生する処理のこと。
マルチスレッドとは
つまり並行処理が発生するプログラムのことです。
スレッドセーフ
「マルチスレッドでも(並行処理が発生するようになっていても)大丈夫だよ~」のこと。
スレッドセーフとは
もう少し具体的に書くと「マルチスレッドな環境で、複数のスレッドが同時に実行したり同じデータを扱ったりしても、ぶっ壊れないよ~」のことです。
ヒープ領域
動的に確保と解放を繰り返せるメモリ領域のことです。
ヒープ領域とは?スタック領域との違いや具体的な管理方法を解説!
メモリの二重解放
new などで確保したメモリ領域(ヒープ領域)を2回 delete などで解放することを言う.
メモリの二重解放回避テク
ダングリングポインタ
プログラムの実行中にポインタが不正なメモリ領域を指している状態。
ダングリング・ポインタ(dangling pointer)
ガベージコレクション(GC)
プログラムで使っていないメモリを解放する機能のことです。
ガベージコレクションとは
UObject クラス
UObjectは、UE4においてnewやdeleteを置き換える概念と考えると分かりやすいかもしれません。 UE4で定義される多くのクラスはUObjectを継承し、直接または間接にNewObject関数で生成します。
【UE4】UObjectのPendingKillについて
一方、UObjecctの解放は必ずGCによって行われます。プログラマは自分でUObjectを解放することはできません。その代わりに、ポインタにUPROPERTYを付ける事によりオブジェクトを参照中であることをGCに伝えることになります。UObjectへの全ての参照が無くなるといずれGCにより解放されます。
ライフタイム
「生存期間」という意味だ。プログラムでは、ライフタイムには2種類ある。「値のライフタイム」と「参照のライフタイム」だ。値のライフタイムはその値のスコープが切れるまでで、参照のライフタイムは値への参照が使われる期間になる。
Rustの革新性を支える「ライフタイム」、メモリー関連の脆弱性を防ぐ
プリミティブ型
そのプログラミング言語に最初から用意されている変数の型のうち、基本的な型っぽいやつ
プリミティブ型【変数の型】とは
ラッパー
プログラミングの分野では、既存のコード(関数、クラス、ライブラリなど)を包んで、別の形で提供することがあります。この手法を「ラップする」とか「ラッピング(wrapping)」といい、ラップして作成した新しいクラスや関数を「ラッパー(wrapper)」といいます。
「ラッパー」・「ラップする」・「ラッピング」の意味
参考記事
- 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をマルチスレッド化してみよう