【UE】Unreal C++ の学習メモ(随時更新)

Unreal Engine

プリプロセッサディレクティブ

プリプロセッサは、ソースコードをコンパイラーに渡す前に実行される前処理です。

プリプロセッサ ディレクティブ | C言語

pragma once

通常、とあるヘッダーファイルを他の複数のファイルで include すると、それぞれのファイルでヘッダーファイルの内容が複数回展開され、同じ定義が複数見つかる事で重複定義エラーを引き起こす。

それを事前に防ぐするために使用されるのが「#pragma once」である。
ヘッダーファイルに「#pragma once」と記述すると、そのヘッダーファイルが 1 度だけインクルードされるようにコンパイラに指示する事が出来る。

B.h と C.h で「#include “A.h” 」と書く場合、A.h に「#pragma once」と記述すると A.h が複数回展開されなくなる。
(「#pragma once」は C++ 標準の機能ではなく、特定のコンパイラのみがサポートしている機能)

pragma region

以下のように記述する事でソースコード内の任意の範囲を折り畳む事が出来る。

#pragma region {表示名}
    //メンバ変数など
#pragma endregion

依存関係の解決

include

include は主に他のヘッダーファイルを現在のファイルに読み込み、その場所に展開するために使用し、以下のコードのように記述する。

#include "{フォルダ名}/{ファイル名}.h"

using

名前空間や型の別名の定義

「using {別名} = {型名};」と記述する事で名前空間や型の別名を定義する事が出来る。

以下のコードでは FString クラスを「str」という別名で使用できるようにしている。

using str = FString;// FString の別名を str と定義

void ASampleActor::Hoge()
{
    // FString ではなく str と書いてもコンパイルが通るようになる
    str Message = TEXT("Hello World");

    UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
}

名前空間名の省略

本来なら「{名前空間名}::{関数名}()」と記述しなくてはならない場合でも「using namespace {名前空間名};」を追加する事で「{関数名}()」のみの記述でもコンパイルエラーが発生しなくなるが、その名前空間に宣言されている変数や関数と同じ名前のものがスコープ内に存在する場合は注意が必要。

#pragma once

namespace SampleNamespace
{
    void SampleFunction()
    {

    }
}
#include "SampleNamespace.h"// using のみではなく include も必要

using namespace SampleNamespace;// using で名前空間をインポート

void ASampleActor::Hoge()
{
    //本来なら「SampleNamespace::SampleFunction()」と記述しなければコンパイルエラーが発生するが、関数名のみの記述でも using を使用する事でコンパイルエラーが発生しなくなる
    SampleFunction();
}

前方宣言

外部のヘッダーファイルに宣言してあるクラスや構造体などの名前のみが必要で、そのクラスや構造体などの詳細(メンバ等)を知る必要が無い場合は前方宣言を使用する。

include とは異なり、ヘッダーファイルを読み込む訳ではないためコンパイル時間を減らしたり、循環参照を避けたりする事が出来る。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SampleActor.generated.h"

//「#include "Camera/CameraComponent.h"」と記述してもコンパイルエラーを発生させないようにする事は出来るが、依存性が強くなったり、コンパイル時間が長くなったりする恐れがある
class UCameraComponent;

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    // UCameraComponent というクラス名のみが必要でメンバを知る必要は無い
    UCameraComponent* CameraComponent;
};

マクロ

UPROPERTY

アクセス権(全体)

プロパティ指定子レベルエディタでの閲覧レベルエディタでの編集ブループリントエディタ等での閲覧ブループリントエディタ等での編集
EditAnywhere
VisibleAnywhere不可不可
EditDefaultsOnly不可不可
VisibleDefaultsOnly不可不可不可
EditInstanceOnly不可不可
VisibleInstanceOnly不可不可不可
参考:UE4 よく使うUPROPERTYメモ

アクセス権(ブループリント)

BlueprintReadWrite ではブループリントから Get も Set も出来るが、

BlueprintReadOnly では Get しか出来なくなる。

private なメンバ変数に対するアクセス権

通常、private なメンバ変数に対して BlueprintReadWrite 等を付けると「BlueprintReadWrite should not be used on private members」というコンパイルエラーが発生するが、以下のコードのように AllowPrivateAccess を true に設定する事でコンパイルエラーが無くなり、ブループリントからでも private なメンバ変数にアクセス出来るようになる。

private:
    // AllowPrivateAccess が無く、BlueprintReadWrite のみではコンパイルエラーが発生する
    UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
    int32 Num;

AllowPrivateAccess を true に設定した private なメンバ変数に他の C++ クラスからアクセスしようとすると「’ASampleActor::Num’: cannot access private member declared in class ‘ASampleActor’」というコンパイルエラーが発生するが、他のクラスが C++ ではなくてブループリントの場合はアクセス出来てしまう。

TitleProperty

構造体を配列などで使用する際に以下のコードのように記述する事で、構造体の特定のメンバの値を配列の要素のタイトルにする事が出来る。

//構造体を定義する
USTRUCT(BlueprintType)
struct FSampleStruct
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    FString Name;
	
    UPROPERTY(EditAnywhere)
    int32 Num;
};
// TitleProperty には構造体のメンバの名前を設定する
UPROPERTY(EditAnywhere, meta = (TitleProperty = "Name"))
TArray<FSampleStruct> SampleStructs;

この例では FSampleStruct の Name という名前のメンバを配列の要素のタイトルに使用している。

FString 型以外にも float 型なども配列の要素のタイトルに使用できる。

存在しないメンバの名前を「TitleProperty」に設定したりした場合は「Invalid Title Property!」というエラーが表示される。

キーワード

static

static ローカル変数

一般的に static はメンバ変数やメンバ関数に対して使用する事が多いが、関数内のローカル変数に対しても使用できる。

通常のローカル変数はスコープを抜けるとメモリから解放され、関数が再び呼び出された際に再度初期化されるが、static を付けたローカル変数である static ローカル変数は、関数が最初に呼び出された際に一度のみ初期化され、その後は値を保持し続ける。

以下のコードでは通常のローカル変数を使用しており、毎フレーム「Hello World」と表示されるが、

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

    Hoge();
}

void ASampleActor::Hoge()
{
    //関数が呼び出される度に bHasLogged 変数が初期化される
    bool bHasLogged = false;

    if (!bHasLogged)
    {
        //毎フレーム表示される
        UE_LOG(LogTemp, Log, TEXT("Hello World"));

        //ここで true を代入しても次の関数の呼び出し時には初期化されてしまう
        bHasLogged = true;
    }
}

以下のコードでは static ローカル変数を使用しており、一度のみ「Hello World」と表示される。

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

    Hoge();
}

void ASampleActor::Hoge()
{
    //関数が最初に呼び出された時にのみ bHasLogged 変数が初期化される
    static bool bHasLogged = false;

    if (!bHasLogged)
    {
        //一度だけ表示される
        UE_LOG(LogTemp, Log, TEXT("Hello World"));

        //この関数の2回目以降の呼び出し時、bHasLogged 変数の値は true になる
        bHasLogged = true;
    }
}

static 関数と const メンバ関数

static 関数

「UFUNCTION(BlueprintPure)」を付けた static 関数はピュア関数として下の画像のような見た目になり、その関数を宣言しているクラスや構造体のインスタンスが無くても使用できる。
(クラスや構造体のインスタンスに依存しない)

//当然、BlueprintCallable 等でも OK
UFUNCTION(BlueprintPure)
static bool StaticFunction(); //前方に static を付ける
const メンバ関数

一方、関数名の後ろに const を付けた const メンバ関数は「UFUNCTION(BlueprintCallable)」であってもピュア関数のような見た目になるが、その関数を宣言しているクラスや構造体のインスタンスを Target に接続しないとコンパイルエラーが発生する。
(クラスや構造体のインスタンスに依存する)

UFUNCTION(BlueprintCallable)
bool ConstFunction() const; //後方に const を付ける
static 関数と const メンバ関数の違い

static 関数ではそのクラスや構造体の静的ではないメンバを読み取ろうとしたり、書き込もうとするとコンパイルエラーが発生するが、const メンバ関数ではそのクラスや構造体の静的ではないメンバを読み取る事が出来る。

// static 関数
bool ASampleActor::StaticFunction()
{
    // static 関数はそのクラスの静的ではないメンバを参照できないため、この例のようにメンバにアクセスしようとするとコンパイルエラーが発生する
    return MemberVariable;
}

// const メンバ関数
bool ASampleActor::ConstFunction() const
{
    //クラスや構造体の静的ではないメンバを読み取る事は出来るが、書き込む事は出来ないため、このコードではコンパイルエラーが発生する
    MemberVariable = true;

    // static 関数ではないため、クラスや構造体のメンバを読み取るだけではコンパイルエラーは発生しない
    return MemberVariable;
}

template

テンプレートの自作

関数テンプレート

通常、関数を宣言する際は以下のコードのように引数と戻り値の型を指定しておく必要がある。

//引数も戻り値も int32
int32 Add(int32 A, int32 B);

しかし、template を使用して以下のコードように関数テンプレートを宣言する事で、その関数の利用者が引数と戻り値にあらゆる型を指定できるようになる。

template<typename T>
T Add(T A, T B)
{
    return A + B;
}

上記の「Add()」を呼び出し、引数に整数型を指定すると当然、整数型どうしは加算できるためコンパイルエラーは発生しない。

int32 IntResult = Add<int32>(1, 2);

しかし、以下のコードのように加算演算子の定義されていない構造体などを引数に指定するとコンパイルエラーが発生する。

FSampleStruct SampleStruct1;
FSampleStruct SampleStruct2;

//「error C2678: binary '+': no operator found which takes a left-hand operand of type 'T' (or there is no acceptable conversion)」というコンパイルエラーが発生する
FSampleStruct StructResult = Add<FSampleStruct>(SampleStruct1, SampleStruct2);
クラステンプレート

以下のようにクラス自体をテンプレートにする事も可能。

基本的に TMap や TArray 等の「T」から始まるものは、ほぼクラステンプレート。

template<typename T>
class TSampleClassTemplate
{
public:
    T Value;
};

TEnumAsByte

以下のように1バイトを超えるサイズの列挙型を定義し、ブループリントからその列挙型を使用するとピンの色が緑色ではなくなり、「unsupported_enum_type: enum size is larger than a byte」というエラーが表示される。

UENUM()
enum class ESampleEnum : int32 //ここで列挙型のサイズを32ビット(4バイト)に設定
{
    A,
    B,
    C
};

対処法としては、列挙型を定義する際にその列挙型のサイズを int32(4バイト)から uint8(1バイト)に変更するか、「UENUM()」に BlueprintType を指定して、uint8 以外のサイズの列挙型を定義できなくする必要がある。

「UENUM()」に BlueprintType を指定してある状態で1バイト以外のサイズの列挙型を定義しようとすると「Invalid BlueprintType enum base – currently only uint8 supported」というコンパイルエラーが発生する。

UENUM(BlueprintType) //「UENUM()」に BlueprintType を指定するとそもそも1バイト以外のサイズの列挙型を定義できなくなる
enum class ESampleEnum : uint8 //サイズを8ビット(1バイト)に変更する
{
    A,
    B,
    C
};

列挙子が非常に多かったり、符号付き整数を使用したかったりと、どうしても列挙型自体のサイズを1バイトに変更できない場合はその列挙型を利用した変数を宣言する際に TEnumAsByte でラップし、その変数を1バイトに変換する事が出来る。

TEnumAsByte<ESampleEnum> SampleEnum;

一応、uint8 ではなく int8 を使用する事で列挙型のサイズを1バイトに抑えつつ符号付き整数を使用する事も可能。
(以下は UE5.3.2 の「Runtime/ImageWrapper/Public/IImageWrapper.h」の EImageFormat のコード)

enum class EImageFormat : int8
{
    /** Invalid or unrecognized format. */
    Invalid = -1,

    /** Portable Network Graphics. */
    PNG = 0,

    /** Joint Photographic Experts Group. */
    JPEG,

    /** Single channel JPEG. */
    GrayscaleJPEG,	

    /** Windows Bitmap. */
    BMP,

    /** Windows Icon resource. */
    ICO,

    /** OpenEXR (HDR) image file format. */
    EXR,

    /** Mac icon. */
    ICNS,
	
    /** Truevision TGA / TARGA */
    TGA,

    /** Hdr file from radiance using RGBE */
    HDR,

    /** Tag Image File Format files */
    TIFF,

    /** DirectDraw Surface */
    DDS,
};

TLazySingleton

通常のシングルトンはプログラム実行時にインスタンスを生成するが、そのインスタンスが必要になるまでインスタンスを生成せず、インスタンスが必要になったタイミングで初めてインスタンスを生成するという処理を遅延初期化という。

UE には遅延初期化と任意のタイミングでの破棄が可能なシングルトンを実現する template が用意されており、それが TLazySingleton である。

基本的に TLazySingleton は以下のコードのように使用する。

ポイントとしてはコンストラクタとデストラクが TLazySingleton からアクセス出来るように public にして、「TLazySingleton<{型名}>::Get()」や「TLazySingleton<{型名}>::TryGet()」、「TLazySingleton<{型名}>::TearDown()」等を使用した静的関数を定義する。

USTRUCT()
struct FSampleStruct
{
    GENERATED_BODY()

//コンストラクタとデストラクは public にして TLazySingleton からアクセス出来るようにする
public:
    //コンストラクタ
    FSampleStruct()
    {
        Guid = FGuid::NewGuid();
    }

    //デストラクタ
    ~FSampleStruct()
    {

    }

    // static を付けて静的関数にする
    static FSampleStruct& Get()
    {
        //オブジェクトへの参照を取得する(初回はオブジェクトを生成する)
        return TLazySingleton<FSampleStruct>::Get();
    }

    // static を付けて静的関数にする
    static FSampleStruct* TryGet()
    {
        //オブジェクトへのポインタを取得する(初回はオブジェクトを生成する)
        return TLazySingleton<FSampleStruct>::TryGet();
    }

    // static を付けて静的関数にする
    static void TearDown()
    {
        //オブジェクトを破棄する
        TLazySingleton<FSampleStruct>::TearDown();
    }

    FString GetGuidAsString()
    {
        return Guid.ToString();
    }

private:
    FGuid Guid;
};

この構造体の例の場合、以下のコードでは経過時間が3秒未満の時は同じ Guid を表示し続けるが、3秒以上経過すると「FSampleStruct is not valid!」と表示され続けるようになる。

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

    FTimerHandle TimerHandle;

    //3秒後に FSampleStruct 型のオブジェクトを破棄する
    GetWorld()->GetTimerManager().SetTimer(TimerHandle, []() { FSampleStruct::TearDown(); }, 3.0f, false);
}

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

    //経過時間が3秒未満の場合は有効なポインタが返ってくる
    if (FSampleStruct* SampleStructPtr = FSampleStruct::TryGet())
    {
        //同じ Guid が表示され続ける
        UE_LOG(LogTemp, Log, TEXT("%s"), SampleStructPtr->GetGuidAsString());
    }
    //3秒以上経過すると nullptr が返ってくる
    else
    {
        UE_LOG(LogTemp, Log, TEXT("FSampleStruct is not valid!"));
    }
}

friend

フレンドクラス

通常、とあるクラスの private なメンバに他のクラスからアクセスしようとすると「error C2248: cannot access private member declared in class ”」というコンパイルエラーが発生する。

以下のコードでは USampleObject クラスの private な「PrivateFunction()」に ASampleActor クラスからアクセスしようとしている。

UCLASS()
class SAMPLE_API USampleObject : public UObject
{
    GENERATED_BODY()

private:
    void PrivateFunction();
};
void ASampleActor::Hoge()
{
    USampleObject* SampleObject = NewObject<USampleObject>();

    //「error C2248: 'USampleObject::PrivateFunction': cannot access private member declared in class 'USampleObject'」というコンパイルエラーが発生する
    SampleObject->PrivateFunction();
}

しかし、private なメンバにアクセスされる側のクラスで friend を用いて他のクラスをフレンドクラスに設定するとそのクラスから private なメンバにアクセス出来るようになる。

#UCLASS()
class SAMPLE_API USampleObject : public UObject
{
    GENERATED_BODY()

private:
    void PrivateFunction();

    // ASampleActor クラスをフレンドクラスに設定する
    friend class ASampleActor;
};
void ASampleActor::Hoge()
{
    USampleObject* SampleObject = NewObject<USampleObject>();

    // ASampleActor クラスは USampleObject クラスのフレンドクラスであるため、コンパイルエラーは発生しない
    SampleObject->PrivateFunction();
}

フレンド関数

通常、どのクラスにも属さないフリー関数がとあるクラスの private なメンバにアクセスしようとすると「error C2248: cannot access private member declared in class ”」というコンパイルエラーが発生する。

以下の例では「AccessToSampleObjectPrivateVariable()」というフリー関数が USampleObject クラスの private な PrivateVariable 変数にアクセスしようとしている。

UCLASS()
class SAMPLE_API USampleObject : public UObject
{
    GENERATED_BODY()

private:
    FString PrivateVariable;

    void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject);
};
//どのクラスにも属さないフリー関数
void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject)
{
    //「error C2248: 'USampleObject::PrivateVariable': cannot access private member declared in class 'USampleObject'」というコンパイルエラーが発生する
    UE_LOG(LogTemp, Log, TEXT("%s"), *SampleObject->PrivateVariable);
}

しかし、フリー関数を宣言する際に friend を用いてそのクラスのフレンド関数に設定するとそのフリー関数から private なメンバにアクセス出来るようになる。

UCLASS()
class SAMPLE_API USampleObject : public UObject
{
    GENERATED_BODY()

private:
    FString PrivateVariable;

    //フリー関数「AccessToSampleObjectPrivateVariable()」をフレンド関数に設定する
    friend void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject);
};
//どのクラスにも属さないフリー関数
void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject)
{
    //「AccessToSampleObjectPrivateVariable()」は USampleObject クラスのフレンド関数であるため、コンパイルエラーは発生しない
    UE_LOG(LogTemp, Log, TEXT("%s"), *SampleObject->PrivateVariable);
}

extern

概要

以下のコードの SampleVariable 変数のようにソースファイルの関数の定義の外に変数を宣言または定義した場合、
(ローカル変数やメンバ変数ではなく、グローバル変数を宣言または定義した場合)

#include "SampleActor.h"

FString SampleVariable = TEXT("Hello World");

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

他のクラスのソースファイルで同じ名前のグローバル変数を宣言または定義すると「fatal error LNK1169: one or more multiply defined symbols found」というコンパイルエラーが発生する。

#include "SampleActor2.h"

// SampleActor.cpp でも SampleVariable 変数を宣言または定義しているのにも関わらず、SampleActor2.cpp でも同じ名前のグローバル変数を宣言または定義しようとしている
FString SampleVariable;

ASampleActor2::ASampleActor2()
{
    PrimaryActorTick.bCanEverTick = true;
}

しかし、今回の場合、どちらか片方に extern を使用してグローバル変数を extern 宣言すると他のソースファイルに宣言または定義されているグローバル変数を参照する事が出来る。

例えば、SampleActor.cpp で SampleVariable 変数が以下のように定義されている時、

#include "SampleActor.h"

FString SampleVariable = TEXT("Hello World");

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

SampleActor2.cpp で同じ名前のグローバル変数を extern 宣言すると元のグローバル変数(今回の例では SampleActor.cpp の SampleVariable 変数)の値を参照する事が出来るため、以下のコードでは「ASampleActor2::BeginPlay()」の実行時に「Hello World」とログに表示される。

#include "SampleActor2.h"

// extern を使用しないとコンパイルエラーが発生する
extern FString SampleVariable;

ASampleActor2::ASampleActor2()
{
    PrimaryActorTick.bCanEverTick = true;
}

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

    UE_LOG(LogTemp, Log, TEXT("%s"), *SampleVariable);
}

static との関係

グローバル変数の宣言時に static を付けると static グローバル変数となり、その static グローバル変数を宣言しているソースファイルからは参照できるが、他のソースファイルからは参照できなくなる。

#include "SampleActor.h"

static FString SampleVariable = TEXT("Hello World");

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

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

    //同じソースファイル内からは参照できる
    UE_LOG(LogTemp, Log, TEXT("%s"), *SampleVariable);
}
#include "SampleActor2.h"

//「fatal error LNK1120: 1 unresolved externals」というコンパイルエラーが発生する
extern FString SampleVariable;

ASampleActor2::ASampleActor2()
{
    PrimaryActorTick.bCanEverTick = true;
}

constexpr

const を用いて初期化した変数はプログラム実行時に値が確定するが、プログラム実行時ではなく、コンパイル時に値を確定させたい定数には constexpr を使用する。

例えば、以下のコードでは const を用いて変数を初期化しており、static でも const でも無い関数の値を代入しているが、コンパイルエラーは発生しない。

void ASampleActor::Hoge()
{
    const int32 ConstVariable = GetNum();
}

int32 ASampleActor::GetNum()
{
    return 1;
}

しかし、以下のコードのように constexpr を用いた変数を初期化する際に static でも const でも無い関数の値を代入しようとすると「error C2131: expression did not evaluate to a constant」というコンパイルエラーが発生する。

void ASampleActor::Hoge()
{
    constexpr int32 ConstVariable = GetNum();
}

int32 ASampleActor::GetNum()
{
    return 1;
}

そして、ヘッダーファイルでの「GetNum()」の宣言時に static を付けて「GetNum()」を static 関数にした場合や、

static int32 GetNum();

ヘッダーファイルとソースファイルで constexpr を付けて「GetNum()」を static 関数ではなく、constexpr 関数にした場合、

// .h
constexpr int32 GetNum();
// .cpp
constexpr int32 ASampleActor::GetNum()
{
    return 1;
}

ヘッダーファイルで static と constexpr を、ソースファイルで constexpr を付けて「GetNum()」を static かつ constexpr な関数にした場合も同じコンパイルエラーが発生する。

// .h
static constexpr int32 GetNum();
// .cpp
constexpr int32 ASampleActor::GetNum()
{
    return 1;
}

この場合、以下のコードのようにヘッダーファイルで関数の定義も記述して「GetNum()」を static かつ constexpr な inline 関数にする事でコンパイルエラーが発生しなくなる。

static constexpr int32 GetNum() { return 1; }

また、以下のコードのように constexpr を用いて宣言した変数に const ではない変数を代入して初期化しようとするとコンパイルエラーが発生するが、

void ASampleActor::Hoge()
{
    // const ではない変数
    int32 Num = 1;

    constexpr int32 ConstVariable = Num;
}

const を用いて初期化した変数を代入して初期化しようとするとコンパイルエラーが発生しなくなる。

void ASampleActor::Hoge()
{
    // const な変数
    const int32 Num = 1;

    constexpr int32 ConstVariable = Num;
}

explicit

通常、以下のコードのように引数が1つのみのコンストラクタを持つ構造体を定義した時、

USTRUCT()
struct FSampleStruct
{
    GENERATED_BODY()

public:
    int32 Num;

    FSampleStruct() : Num(0) {}

    //引数が1つのみのコンストラクタ
    FSampleStruct(int32 InNum) : Num(InNum) {}
};

関数の引数などでコンストラクタの引数の型の値をその構造体に暗黙的に変換する事が出来る。

// FSampleStruct を引数に渡す必要のある関数
void ASampleActor::Hoge(const FSampleStruct& SampleStruct)
{

}

void ASampleActor::Hoge2()
{
    //「Hoge()」の引数は FSampleStruct 型だが、コンストラクタの引数である整数型の値のみを渡す事で暗黙的に FSampleStruct に変換する事が出来る
    Hoge(1);

    //当然、明示的に FSampleStruct を生成して渡す事も出来る
    Hoge(FSampleStruct(1));

    //関数の引数でなくても暗黙的に変換できる
    FSampleStruct SampleStruct = 1;
}

このような暗黙的な変換が可能な場合、意図せず構造体が生成されたり、構造体のコンストラクタが実行されたりするため、構造体のコンストラクタを宣言する際に explicit を付ける事で暗黙的な型変換を行えなくする事が出来る。

USTRUCT()
struct FSampleStruct
{
    GENERATED_BODY()

public:
    int32 Num;

    FSampleStruct() : Num(0) {}

    //コンストラクタの宣言時に explicit を付けて暗黙的な型変換を行えなくする
    explicit FSampleStruct(int32 InNum) : Num(InNum) {}
};
void ASampleActor::Hoge(const FSampleStruct& SampleStruct)
{

}

void ASampleActor::Hoge2()
{
    //「error C2440: 'initializing': cannot convert from 'int' to 'FSampleStruct'」というコンパイルエラーが発生するようになる
    FSampleStruct SampleStruct = 1;

    //「error C2664: 'void ASampleActor::Hoge(const FSampleStruct &)': cannot convert argument 1 from 'int' to 'const FSampleStruct &'」というコンパイルエラーが発生するようになる
    Hoge(1);
}

new(placement new)

以下のコードのように構造体を定義し、

USTRUCT()
struct FSampleStruct
{
    GENERATED_BODY()

public:
    //コンストラクタ
    FSampleStruct()
    {
        UE_LOG(LogTemp, Log, TEXT("Construct"));
    }

    //デストラクタ
    ~FSampleStruct()
    {
        UE_LOG(LogTemp, Log, TEXT("Destruct"));
    }

    void SampleFunction()
    {
        UE_LOG(LogTemp, Log, TEXT("Hello World"));
    }
};

new を用いてその構造体のオブジェクトを生成した場合、メモリ領域(ヒープ領域)の確保と構造体のコンストラクタの呼び出しが同時に行われるが、

//構造体のコンストラクタが呼び出されるため、ログに「Construct」と表示される
FSampleStruct* SampleStruct = new FSampleStruct();

以下の記述方法の placement new を使用すると、確保済みの既存のメモリにオブジェクトを構築する事が出来る。

new({メモリのポインタ}) {型名}();

また、そのオブジェクトのコンストラクタは placement new でのオブジェクトの構築時に初めて呼び出される。

上記のような特徴から placement new はメモリを厳密に管理したい場合や、メモリの使用量の計測や制限などを行いたい場合、処理速度を上げたい場合などに使用する。

使用例

ヒープ領域を確保する場合

一般的にヒープ領域の確保のみを行う際は以下のコードように記述し、

malloc(sizeof({型名}));

メモリを解放する際は delete を使用せず、デストラクタを呼び出した後に「free()」を使用する。

// FSampleStruct のオブジェクトに必要なサイズのメモリをヒープ領域から確保する
void* Memory = malloc(sizeof(FSampleStruct));

//事前に確保したメモリのポインタを「new()」に渡し、そのメモリにオブジェクトを構築する
//このタイミングで構造体のコンストラクタが呼び出される
FSampleStruct* SampleStruct = new(Memory) FSampleStruct();

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

//手動で構造体のデストラクタを呼び出す
SampleStruct->~FSampleStruct();

// delete ではなく「free()」でメモリを解放する
free(Memory);
スタック領域を確保する場合

一般的にスタック領域の確保のみを行う際は以下のコードように記述し、

alignas({型名}) unsigned char {変数名}[sizeof({型名})];

メモリを解放する際、デストラクタの呼び出しは必要だが、delete や「free()」等は不要。

// FSampleStruct のオブジェクトに必要なサイズのメモリをスタック領域から確保する
alignas(FSampleStruct) unsigned char Memory[sizeof(FSampleStruct)];

//事前に確保したメモリのポインタを「new()」に渡し、そのメモリにオブジェクトを構築する
//このタイミングで構造体のコンストラクタが呼び出される
FSampleStruct* SampleStruct = new(Memory) FSampleStruct();

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

//手動で構造体のデストラクタを呼び出す
SampleStruct->~FSampleStruct();

unsigned

int 型の値を宣言する際に「unsigned」を付ける事でその変数に負の値を代入できないようにする事が出来る。

unsigned int Num = 1;

通常、32ビットの int 型で表せる値の範囲は -2,147,483,648~2,147,483,647 だが、「unsigned」を付ける事で負の数を表すのに使用していた分を全て正の数を表すために使用する事が出来るため、32ビットの符号なし整数型の表せる値の範囲は 0~4,294,967,295 となる。

演算子

デリファレンス演算子

「UE_LOG()」等で FString 型の変数の値を渡す際に以下のコードのように記述する事がある。

UE_LOG(LogTemp, Log, TEXT("%s"), *StringVariable);

エンジン利用者は Unreal C++ やブループリントでは基本的に TCHAR 型ではなく FString 型で文字列を管理する事が出来るが、「UE_LOG()」のように引数が FString 型ではなく TCHAR* 型のものが時々存在し、その場合はデリファレンス演算子(*)を使用する事で FString 型から TCHAR* 型に変換する事が出来る。

FString StringVariable = TEXT("Hello World");

//「error C2440: 'initializing': cannot convert from 'FString' to 'const TCHAR *'」というコンパイルエラーが発生する
const TCHAR* CharPtr = StringVariable;

// FString 型から TCHAR* 型に変換しているため、コンパイルエラーは発生しない
const TCHAR* CharPtr = *StringVariable;

パス結合演算子

FString 型の値どうしにパス結合演算子(/)を使用する事でパスを簡単に結合する事が出来る。

以下のいずれの場合においても Path 変数には「C:/Program Files/Epic Games/UE_5.3」という文字列が入る。

//結合部分にスラッシュは無い
FString Path = FString(TEXT("C:/Program Files/Epic Games")) / FString(TEXT("UE_5.3"));
//結合部分の前にスラッシュがある
FString Path = FString(TEXT("C:/Program Files/Epic Games/")) / FString(TEXT("UE_5.3"));
//結合部分の後ろにスラッシュがある
FString Path = FString(TEXT("C:/Program Files/Epic Games")) / FString(TEXT("/UE_5.3"));

ただし、以下のように結合部分の前にも後ろにもスラッシュがある場合は「C:/Program Files/Epic Games//UE_5.3」という文字列になってしまう。
(UE 5.3.2 で確認)

//結合部分の前にも後ろにもスラッシュがある
FString Path = FString(TEXT("C:/Program Files/Epic Games/")) / FString(TEXT("/UE_5.3"));

ポインタ

汎用ポインタ型

float* では float 型の値のポインタのみを格納でき、FSampleStruct* では FSampleStruct 型のオブジェクトのポインタのみを格納できるというように通常のポインタ型ではその型以外のポインタは格納できないが、汎用ポインタ型(void*)を使用する事であらゆる型のオブジェクトのポインタを格納できるようになる。

FSampleStruct SampleStruct;

//構造体のオブジェクトのポインタを取得する
FSampleStruct* SampleStructPtr = &SampleStruct;

// FSampleStruct* と float* では型が異なるため、コンパイルエラーが発生する
float* FloatPtr = SampleStructPtr;

//汎用ポインタ型ではあらゆる型のオブジェクトのポインタを格納する事が出来るため、コンパイルエラーは発生しない
void* VoidPtr = SampleStructPtr;

関数ポインタ

メンバ関数ポインタ

関数を変数のように扱いたい時は関数ポインタを使用し、クラスや構造体のインスタンスに依存するメンバ関数を関数ポインタで扱いたい場合(メンバ関数ポインタ)は以下の記述方法で初期化する。

{戻り値の型名}({クラス名}:: * {メンバ関数ポインタ名})({引数の型名1}, {引数の型名2}) = &{クラス名}::{メンバ関数名};

そして、メンバ関数ポインタで指し示しているメンバ関数を呼び出す際は以下のコードのように記述する。

//ポインタ経由でオブジェクトにアクセスする場合
({オブジェクトのポインタ}->*{メンバ関数ポインタ名})({引数の値1}, {引数の値2});

//オブジェクトに直接アクセスする場合
({オブジェクト}.*{メンバ関数ポインタ名})({引数の値1}, {引数の値2});
使用例
//メンバ関数ポインタに格納されるメンバ関数
int32 ASampleActor::Add(int32 A, int32 B)
{
    return A + B;
}

void ASampleActor::Hoge()
{
    //メンバ関数ポインタを初期化する
    int32(ASampleActor:: * MemberFunctionPtr)(int32, int32) = &ASampleActor::Add;

    //メンバ関数ポインタの指し示す関数を呼び出す
    int32 Result = (this->*MemberFunctionPtr)(1, 2);

    //この例では「3」と表示される
    UE_LOG(LogTemp, Log, TEXT("%d"), Result);
}

通常の関数ポインタ(静的関数など)

また、クラスや構造体のインスタンスに依存しない静的関数などを関数ポインタで扱いたい場合は以下の記述方法で初期化する。

{戻り値の型名}(*{関数ポインタ名})({引数の型名1}, {引数の型名2}) = &{関数名};

そして、関数ポインタで指し示している関数を呼び出す際は以下のコードのように記述する。

{関数ポインタ名}({引数の値1}, {引数の値2});
使用例
//関数ポインタに格納される静的関数(ヘッダーファイルでの宣言時に static が付いている)
int32 ASampleActor::StaticAdd(int32 A, int32 B)
{
    return A + B;
}

void ASampleActor::Hoge()
{
    //関数ポインタを初期化する(今回の場合は「&ASampleActor::StaticAdd」でも「&StaticAdd」でも良い)
    int32(*StaticFunctionPtr)(int32, int32) = &StaticAdd;

    //関数ポインタの指し示す関数を呼び出す
    int32 Result = StaticFunctionPtr(1, 2);
 
    //この例では「3」と表示される
    UE_LOG(LogTemp, Log, TEXT("%d"), Result);
}

最後に

参考記事

プリプロセッサディレクティブ

依存関係の解決

マクロ

キーワード

お問い合わせ

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