【UE】Unreal C++ のスマートポインタについて

Unreal Engine

はじめに

押さえておきたい用語

ポインタ

変数の値はコンピュータのメモリに保存される。
アドレスは値が格納されているメモリの位置を表す。
ポインタ変数、またはポインタとは、アドレスを保持する変数のこと。

C++ポインタまとめ

メモリ領域の確保

プログラムさんが「メモリのここの部分は今から僕が使うからね。他の人は使わないでね」と場所取りをすることです。

メモリ領域の確保とは

メモリの解放

プログラムさんが「メモリのここの部分は今から僕が使うからね。他の人は使わないでね」と場所取りしていたメモリ(の一部)を「ここはもう使わないから他の人が使っていいよ!」と、誰でも使える状態に戻すことです。

メモリの解放とは

派生クラス

クラスの継承の話で登場するクラスのひとつであり新しく作られた方(継承先)のクラスのこと

派生クラスとは

スコープ

「範囲」をカッコ付けて言った表現。
あるいはプログラミングの世界で登場する用語で「おまえの名前が通じるのは、ここまでだ!」な影響範囲のことです。

スコープとは

循環参照

「その指示に従うと、ずっとたらい回しにされますよ!」な状態のこと。
もう少し具体的に書くとAには「Bを見ろ」と書いてあって、Bには「Aを見ろ」と書いてあるように、いつまでたってもゴールに辿りつけないような指示になっていることです。

循環参照とは

マルチスレッド

処理の開始から終了まで線を引いたときに、枝分かれの発生する処理のこと。
つまり並行処理が発生するプログラムのことです。

マルチスレッドとは

スレッドセーフ

「マルチスレッドでも(並行処理が発生するようになっていても)大丈夫だよ~」のこと。
もう少し具体的に書くと「マルチスレッドな環境で、複数のスレッドが同時に実行したり同じデータを扱ったりしても、ぶっ壊れないよ~」のことです。

スレッドセーフとは

ヒープ領域

動的に確保と解放を繰り返せるメモリ領域のことです。

ヒープ領域とは?スタック領域との違いや具体的な管理方法を解説!

メモリの二重解放

new などで確保したメモリ領域(ヒープ領域)を2回 delete などで解放することを言う.

メモリの二重解放回避テク

ダングリングポインタ

プログラムの実行中にポインタが不正なメモリ領域を指している状態。

ダングリング・ポインタ(dangling pointer)

ガベージコレクション(GC)

プログラムで使っていないメモリを解放する機能のことです。

ガベージコレクションとは

UObject クラス

UObjectは、UE4においてnewやdeleteを置き換える概念と考えると分かりやすいかもしれません。 UE4で定義される多くのクラスはUObjectを継承し、直接または間接にNewObject関数で生成します。
一方、UObjecctの解放は必ずGCによって行われます。プログラマは自分でUObjectを解放することはできません。その代わりに、ポインタにUPROPERTYを付ける事によりオブジェクトを参照中であることをGCに伝えることになります。UObjectへの全ての参照が無くなるといずれGCにより解放されます。

【UE4】UObjectのPendingKillについて

インスタンス

オブジェクト指向の話で出てくる用語のひとつで「実際に作ったもの」に相当するものが「インスタンス」です。

インスタンスとは

ライフタイム

「生存期間」という意味だ。プログラムでは、ライフタイムには2種類ある。「値のライフタイム」と「参照のライフタイム」だ。値のライフタイムはその値のスコープが切れるまでで、参照のライフタイムは値への参照が使われる期間になる。

Rustの革新性を支える「ライフタイム」、メモリー関連の脆弱性を防ぐ

プリミティブ型

そのプログラミング言語に最初から用意されている変数の型のうち、基本的な型っぽいやつ

プリミティブ型【変数の型】とは

ラッパー

プログラミングの分野では、既存のコード(関数、クラス、ライブラリなど)を包んで、別の形で提供することがあります。この手法を「ラップする」とか「ラッピング(wrapping)」といい、ラップして作成した新しいクラスや関数を「ラッパー(wrapper)」といいます。

「ラッパー」・「ラップする」・「ラッピング」の意味

スマートポインタの概要

通常、C++ で new 等を用いてオブジェクトを生成し、メモリ領域(ヒープ領域)を確保した際、そのオブジェクトの使用後に delete 等を用いてメモリを解放する必要がある。

// new を用いてオブジェクトを生成し、変数を初期化してメモリ領域(ヒープ領域)を確保する
SampleClass* SampleClassPtr = new SampleClass();

//生成したオブジェクトを使用する
SampleClassPtr->SampleFunction();

// delete を用いてオブジェクトを破棄(メモリを解放)する
delete SampleClassPtr;

new 等を用いてオブジェクトを生成し、メモリ領域を確保した際はメモリ領域の中でもヒープ領域が確保されるが、以下のコードのように new 等を用いずに変数を宣言、初期化した場合はヒープ領域ではなく、スタック領域にメモリが割り当てられ、スコープを抜けると自動でメモリが解放される。

int32 Num = 1;

そして生成したオブジェクトが UObject クラスの場合は UE 独自のガベージコレクションにより、そのオブジェクトがどこからも参照されなくなると自動でオブジェクトが破棄(メモリが解放)されるため、デストラクタ等で手動でオブジェクトを破棄(メモリを解放)する必要が無い。
(当然、UObject クラスの派生クラスもガベージコレクションの対象)

//コンストラクタ
ASampleActor::ASampleActor()
{
    // UObject クラスを生成し、そのオブジェクトのポインタをメンバ変数に代入する
    SampleObjectPtr = NewObject<UObject>();
}

//デストラクタ
ASampleActor::~ASampleActor()
{
    //通常はメンバ変数のポインタの指し示すオブジェクトを破棄(メモリを解放)する処理を記述する必要があるが、UObject クラスのオブジェクトはガベージコレクションにより自動で破棄(メモリが解放)されるため、手動でオブジェクトを破棄(メモリを解放)する処理を記述する必要が無い
}

前述のように UObject クラスのオブジェクトはガベージコレクションにより管理されているが、ガベージコレクトのタイミングは不定であり、「UEngine::ForceGarbageCollection()」等を呼び出して任意のタイミングでガベージココレクトさせる事も出来る。

そして基本的にメモリ領域(ヒープ領域)を確保したオブジェクトのライフタイムを明確に制御するために使用するものがスマートポインタである。

void ASampleActor::Hoge()
{
    // UObject クラスではないオブジェクトを生成し、そのオブジェクトのポインタを TSharedPtr として保持する
    TSharedPtr<FSampleStruct> SampleStructPtr = MakeShared<FSampleStruct>();

    //生成したオブジェクトを使用する
    SampleStructPtr->SampleFunction();
}
//変数のスコープを抜けると自動でオブジェクトが破棄(メモリが解放)される

また、通常のポインタは nullptr がどうかを確認する事で有効なポインタかをチェックするのが一般的だが、

//生ポインタ
SampleClass* SampleClassPtr = new SampleClass();

//生ポインタの場合はポインタが nullptr かどうかを比較して有効性を確認する
//(UObject クラスの場合は「IsValid(UObject*)」で確認する)
if(SampleClassPtr != nullptr) SampleClassPtr->SampleFunction();

UE のスマートポインタの場合は「IsValid()」を使用してポインタが有効かをチェックするのが一般的である。

//スマートポインタ
TSharedPtr<FSampleStruct> SampleStructPtr = MakeShared<FSampleStruct>();

//スマートポインタの場合は「IsValid()」で確認する
if(SampleStructPtr.IsValid()) SampleStructPtr->SampleFunction();

スマートポインタを使用すべき場合と使用しなくても良い場合

スマートポインタを使用すべき場合

  • とある関数内で 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

ポインタをコピーして共有する事ができ、対象のオブジェクトがどこからも参照されなくなると自動でオブジェクトを破棄(メモリを解放)するが、循環参照を引き起こす可能性があり、その場合は全ての参照が無くなってもオブジェクトは破棄(メモリは解放)されない。

TSharedPtr<FSampleStruct> SharedPtr1 = MakeShared<FSampleStruct>();

// TSharedPtr 同士はポインタをコピーして渡す事が出来る
TSharedPtr<FSampleStruct> SharedPtr2 = SharedPtr1;
TSharedPtr<FSampleStruct> SharedPtr3 = SharedPtr2;

//ユニークな所有権を渡している訳ではないため、受け渡し後でも元のポインタは有効
if (SharedPtr1.IsValid()) UE_LOG(LogTemp, Log, TEXT("SharedPtr1 is valid!"));

//「GetSharedReferenceCount()」でオブジェクトを共有している数(参照カウント)を確認でき、この例では「3」と表示される
UE_LOG(LogTemp, Log, TEXT("The shared reference count is %d"), SharedPtr1.GetSharedReferenceCount());

TSharedRef

TSharedPtr と機能はほぼ同じだが、TSharedPtr とは違って TSharedRef には nullptr を代入する事が出来ず、常に有効なポインタが格納されているため、破棄(メモリが解放)される事の無いオブジェクトに対して使用可能なスマートポインタであり、「IsValid()」が用意されていない。

// TSharedPtr には nullptr を代入できる
TSharedPtr<FSampleStruct> SharedPtr = nullptr;

// TSharedRef に nullptr を代入しようとするとコンパイルエラーが発生する
TSharedRef<FSampleStruct> SharedRef = nullptr;

// TSharedRef は常に有効であるため、「IsValid()」を含んでいない
//以下のコードでは「IsValid()」が定義されていないというコンパイルエラーが発生する
if(SharedRef.IsValid())
{

}

TWeakPtr

TSharedPtr からポインタをコピーして受け取る事が可能だが、TSharedPtr とは違って共有可能な所有権を持たず、オブジェクトのライフタイムは受け取り元の TSharedPtr に依存するため、元の TSharedPtr の参照カウントが0になる(対象のオブジェクトがどこからも参照されなくなる)と、例え TWeakPtr の変数のスコープを抜けていなくても対象のオブジェクトは破棄(メモリが解放)されるため、主に循環参照を避けたり、オブジェクトのライフタイムに影響を与えたくない場合に使用する。

void ASampleActor::BeginPlay()
{
    Super::BeginPlay();

    //スコープが「BeginPlay()」内である TSharedPtr 型の変数を初期化する
    TSharedPtr<FSampleStruct> SharedPtr = MakeShared<FSampleStruct>();

    //クラスのメンバ変数(スコープが「BeginPlay()」ではない変数)である TWeakPtr 型の変数に TSharedPtr 型の変数の値をコピーして渡す
    WeakPtr = SharedPtr;

    //この時点ではまだ TSharedPtr 型の変数のスコープを抜けていないため TWeakPtr 型の変数も有効
    if (WeakPtr.IsValid()) UE_LOG(LogTemp, Log, TEXT("WeakPtr is valid!"));
}

void ASampleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (WeakPtr.IsValid())
    {
        // TWeakPtr 型の変数の際し示すオブジェクトにアクセスする際は「Pin()」で TWeakPtr から TSharedPtr に変換する必要がある
        WeakPtr.Pin()->SampleFunction();
    }
    else
    {
        // TWeakPtr はオブジェクトのライフタイムに影響を与えないため、TSharedPtr 型の変数のスコープを抜けた後(対象のオブジェクトがどこからも参照されなくなった後)は TWeakPtr 型の変数も無効になる
        // TSharedPtr はオブジェクトのライフタイムに影響を与えるため、この例の WeakPtr 変数が TWeakPtr 型ではなく TSharedPtr 型だった場合は「BeginPlay()」を抜けた「Tick()」内でも WeakPtr 変数は有効
        UE_LOG(LogTemp, Log, TEXT("WeakPtr is not valid!"));
    }
}

UObject クラスのオブジェクトに対して使用可能なスマートポインタ

TWeakObjectPtr

TWeakPtr と機能はほぼ同じで TWeakPtr では UObject クラスのオブジェクトを対象に出来なかったが、TWeakObjectPtr は UObject クラスのオブジェクトを対象にでき、対象のオブジェクトがガベージコレクションによって破棄(メモリが解放)されてもポインタが自動的に無効化されるため、安全に UObject クラスを参照する事が出来る。

UObject* SampleObject = NewObject<UObject>();

// TWeakPtr では UObject クラスのオブジェクトを対象に出来ないため、このコードではコンパイルエラーが発生する
TWeakPtr<UObject> WeakPtr = SampleObject;

// TWeakObjectPtr では UObject クラスのオブジェクトを対象に出来るため、このコードでもコンパイルエラーが発生しない
TWeakObjectPtr<UObject> WeakObjectPtr = SampleObject;

TStrongObjectPtr

このセクションは編集中です

TSoftObjectPtr

このセクションは編集中です

TObjectPtr

UE5 から新たに導入されたスマートポインタであり、生ポインタとほぼ同じ挙動をするが、「UPROPERTY()」においては生ポインタよりも TObjectPtr を使用する事が推奨されており、UObject クラスのオブジェクトに対して使用する事が出来る。

private:
    UPROPERTY()
    TObjectPtr<UObject> SampleObject;

その他

「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 も許容されるため、そのまま暗黙的に変換できる。

TSharedPtr<FSampleStruct> SharedPtr = SharedRef;

TSharedPtr → TSharedRef

TSharedPtr では nullptr が許容されているが、TSharedRef では nullptr が許容されていないため、TSharedPtr から TSharedRef に変換する際は一度「IsValid()」で TSharedPtr が有効か確認した後に「ToSharedRef()」で変換する必要がある。

// TSharedPtr が有効か確認
if(SharedPtr.IsValid())
{
    TSharedRef<FSampleStruct> SharedRef = SharedPtr.ToSharedRef();
}

TSharedPtr と TSharedRef におけるキャスト

StaticCastSharedPtr / StaticCastSharedRef

以下の2つのクラスが定義されている時、

//親クラス
class FParentClass
{
	
};

// FParentClass クラスの子クラス
class FChildClass : public FParentClass
{
public:
    void ChildClassFunction()
    {
        UE_LOG(LogTemp, Log, TEXT("Hello World"));
    };
};

以下のコードのように「StaticCastSharedPtr()」を使用する事で TSharedPtr をダウンキャスト出来る。
(TSharedRef の場合は「StaticCastSharedRef()」を使用する)

// FChildClass クラスを生成して FParentClass クラスとして取得する
TSharedPtr<FParentClass> ParentClassSharedPtr = MakeShared<FChildClass>();

// FParentClass クラスとして保持している FChildClass クラスを FChildClass クラスにダウンキャストする
TSharedPtr<FChildClass> ChildClassSharedPtr = StaticCastSharedPtr<FChildClass>(ParentClassSharedPtr);

//正常に動作する
ChildClassSharedPtr->ChildClassFunction();

しかし、「StaticCastSharedPtr()」と「StaticCastSharedRef()」はダウンキャストに失敗しても nullptr にならないため、以下のコードでは未定義動作を引き起こす。

// 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

このセクションは編集中です

最後に

参考記事

用語

スマートポインタ

その他

お問い合わせ

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