プリプロセッサディレクティブ
プリプロセッサは、ソースコードをコンパイラーに渡す前に実行される前処理です。
プリプロセッサ ディレクティブ | 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 | 可 | 不可 | 不可 | 不可 |
アクセス権(ブループリント)
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;
};
指定される型を限定する
関数テンプレートやクラステンプレートが外部から利用される際に指定される型が、あるクラスの派生クラスである事を保証したい場合は以下のコードのように static_assert を使用したり、
template<typename T>
void TemplateFunction(T* InObject)
{
// T が UObject クラスの派生クラスではない場合は「T must be a subclass of UObject.」というコンパイルエラーを発生させる
static_assert(TIsDerivedFrom<T, UObject>::Value, "T must be a subclass of UObject.");
}
void Hoge()
{
//コンパイルエラーは発生しない
UObject* Object = NewObject<UObject>();
TemplateFunction(Object);
//コンパイルエラーは発生しない
AActor* Actor = GetWorld()->SpawnActor<AActor>();
TemplateFunction(Actor);
// FSampleStruct 構造体は UObject クラスの派生クラスではないため「Error C2338 : static_assert failed: 'T must be a subclass of UObject.」というコンパイルエラーが発生する
FSampleStruct* SampleStruct = new FSampleStruct();
TemplateFunction(SampleStruct);
}
TEnableIf を使用したりすると良い。
template<typename T>
// T が UObject クラスの派生クラスではない場合はこの関数をオーバーロード解決の候補から除外する(存在しないものとして扱う)
typename TEnableIf<TIsDerivedFrom<T, UObject>::Value, void>::Type
TemplateFunction(T* InObject)
{
}
void Hoge()
{
//コンパイルエラーは発生しない
UObject* Object = NewObject<UObject>();
TemplateFunction(Object);
//コンパイルエラーは発生しない
AActor* Actor = GetWorld()->SpawnActor<AActor>();
TemplateFunction(Actor);
// FSampleStruct 構造体は UObject クラスの派生クラスではないため「Error C2672 : 'ASampleActor::TemplateFunction': no matching overloaded function found」というコンパイルエラーが発生する
FSampleStruct* SampleStruct = new FSampleStruct();
TemplateFunction(SampleStruct);
}
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!"));
}
}
TThreadSingleton
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 となる。
auto
auto を使用すると以下のコードのように明示的に型を書かなくても良くなるが、
// FString
auto Message = FString(TEXT("Hello World"));
//「Hello World」と表示される
UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
// int
auto Num = 1;
//「1」と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), Num);
Epic 公式ドキュメントのコーディング規約に書かれているように auto はあまり多用しない方が良い。
下の例外に該当しない場合は、C++ コードで auto を使わないようにします。初期化している型について常に明示的でなければなりません。つまり、読み手がその型を見えるようにしなければなりません。このルールは C# の var キーワードの使用にも適用されます。
コーディング規約
C++20 の構造化バインディング機能も、事実上可変個引数の auto であるため、使用しないでください。
次のような場合に、auto の使用が認められます。
・lambda を変数にバインドする必要がある場合です。lambda 型はコードで表現できないからです。
・iterator 変数に対して認められます。しかし、iterator の型が非常に詳細で読みづらくなります。
・テンプレートのコードで認められます。この場合、式の型は簡単に見分けることはできません。これは高度な事例です。
演算子
デリファレンス演算子
「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;
関数ポインタ
メンバ関数ポインタ
関数を変数のように扱いたい時は関数ポインタを使用し、クラスや構造体のインスタンスに依存するメンバ関数を関数ポインタで扱いたい場合(メンバ関数ポインタ)は以下の記述方法で初期化する。
{戻り値の型名}({クラス名}:: * {メンバ関数ポインタ名})({引数の型名}, {引数の型名}) = &{クラス名}::{メンバ関数名};
そして、メンバ関数ポインタで指し示しているメンバ関数を呼び出す際は以下のコードのように記述する。
//ポインタ経由でオブジェクトにアクセスする場合
({オブジェクトのポインタ}->*{メンバ関数ポインタ名})({引数の値}, {引数の値});
//オブジェクトに直接アクセスする場合
({オブジェクト}.*{メンバ関数ポインタ名})({引数の値}, {引数の値});
使用例
//メンバ関数ポインタに格納されるメンバ関数
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);
}
通常の関数ポインタ(静的関数など)
また、クラスや構造体のインスタンスに依存しない静的関数などを関数ポインタで扱いたい場合は以下の記述方法で初期化する。
{戻り値の型名}(*{関数ポインタ名})({引数の型名}, {引数の型名}) = &{関数名};
そして、関数ポインタで指し示している関数を呼び出す際は以下のコードのように記述する。
{関数ポインタ名}({引数の値}, {引数の値});
使用例
//関数ポインタに格納される静的関数(ヘッダーファイルでの宣言時に 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);
}
その他
ラムダ式
概要
ラムダ式を使用すると通常の関数を定義する事なく、その場で匿名の関数を定義する事ができる。
基本的にラムダ式を定義する時は以下のコードのように記述する。
(戻り値の型を明確に推論できる場合は戻り値の型を明示しなくても良い)
[{キャプチャする変数名}, {キャプチャする変数名}]({引数の型名} {引数名}, {引数の型名} {引数名}) -> {戻り値の型名}
{
//処理
};
例えば、以下のコードのような FString 型の引数と FString 型の戻り値が1つずつあるデリゲートをヘッダーファイル等で宣言している時、
DECLARE_DELEGATE_RetVal_OneParam(FString, FSampleDelegate, const FString&);
以下のコードのように記述する事でラムダ式を定義し、デリゲートにバインドして実行する事が出来る。
(ラムダ式はデリゲートに限らず、TFunction 等でも使用できる)
FSampleDelegate SampleDelegate;
//ラムダ式を定義する
auto LambdaFunction = [](const FString& InputValue) -> FString
{
return InputValue + TEXT(" World");
};
//ラムダ式をデリゲートにバインドする
SampleDelegate.BindLambda(LambdaFunction);
//デリゲートを実行する
const FString ReturnValue = SampleDelegate.Execute(TEXT("Hello"));
//「Hello World」と表示される
UE_LOG(LogTemp, Log, TEXT("%s"), *ReturnValue);
ローカル変数のキャプチャ
また、以下のコードのようにラムダ式の中でローカル変数の値を読み取ろうとすると「Error C3493 : ‘LocalVariable’ cannot be implicitly captured because no default capture mode has been specified」や「Error C2326 : ‘FString ASampleActor::Hoge::::operator ()(const FString &) const’: function cannot access ‘LocalVariable’」等のコンパイルエラーが発生する。
//ローカル変数を初期化する
const FString LocalVariable = TEXT(" World");
//ラムダ式を定義する
auto LambdaFunction = [](const FString& InputValue) -> FString
{
//ローカル変数の値を読み取る(コンパイルエラーが発生する)
return InputValue + LocalVariable;
};
この場合は以下のコードのようにキャプチャリストにローカル変数を追加する事でラムダ式の中でもローカル変数の値を読み取れるようになる。
//ローカル変数を初期化する
const FString LocalVariable = TEXT(" World");
//ラムダ式を定義する(ローカル変数を値キャプチャ)
auto LambdaFunction = [LocalVariable](const FString& InputValue) -> FString
{
//ローカル変数の値を読み取る(コンパイルエラーは発生しない)
return InputValue + LocalVariable;
};
しかし、このキャプチャ方法はローカル変数を値渡ししているだけであり、ラムダ式の中でローカル変数に値を代入しようとすると「Error C2678 : binary ‘=’: no operator found which takes a left-hand operand of type ‘const FString’ (or there is no acceptable conversion)」等のコンパイルエラーが発生する。
//ローカル変数を宣言する
FString LocalVariable;
//ラムダ式を定義する(ローカル変数を値キャプチャ)
auto LambdaFunction = [LocalVariable](const FString& InputValue) -> FString
{
//ローカル変数に値を代入する(コンパイルエラーが発生する)
LocalVariable = InputValue;
//ローカル変数の値を読み取る(コンパイルエラーは発生しない)
return LocalVariable;
};
この場合はキャプチャリストにローカル変数を追加する際に以下のコードのように「&」を付けると参照渡しになり、ローカル変数に値を代入する事が出来るようになるが、スマートポインタ等を用いてローカル変数のライフタイムを明確に制御する必要がある。
FSampleDelegate SampleDelegate;
//ローカル変数を宣言する
FString LocalVariable;
//ラムダ式を定義する(ローカル変数を参照キャプチャ)
auto LambdaFunction = [&LocalVariable](const FString& InputValue) -> FString
{
//ローカル変数に値を代入する(コンパイルエラーは発生しない)
LocalVariable = InputValue;
//ローカル変数の値を読み取る(コンパイルエラーは発生しない)
return LocalVariable;
};
//ラムダ式をデリゲートにバインドする
SampleDelegate.BindLambda(LambdaFunction);
//デリゲートを実行する
SampleDelegate.Execute(TEXT("Hello World"));
//「Hello World」と表示される
UE_LOG(LogTemp, Log, TEXT("%s"), *LocalVariable);
また、「=」をキャプチャリストに追加する事で全てのローカル変数を値キャプチャしたり、
//ローカル変数を宣言する
FString LocalVariable1;
FString LocalVariable2;
//ラムダ式を定義する(全てのローカル変数を値キャプチャ)
auto LambdaFunction = [=](const FString& InputValue)
{
//ローカル変数に値を代入する(コンパイルエラーが発生する)
LocalVariable1 = TEXT("Hello");
LocalVariable2 = TEXT(" World");
//ローカル変数の値を読み取る(コンパイルエラーは発生しない)
return LocalVariable1 + LocalVariable2;
};
「&」をキャプチャリストに追加する事で全てのローカル変数を参照キャプチャしたり出来る。
//ローカル変数を宣言する
FString LocalVariable1;
FString LocalVariable2;
//ラムダ式を定義する(全てのローカル変数を参照キャプチャ)
auto LambdaFunction = [&](const FString& InputValue)
{
//ローカル変数に値を代入する(コンパイルエラーは発生しない)
LocalVariable1 = TEXT("Hello");
LocalVariable2 = TEXT(" World");
//ローカル変数の値を読み取る(コンパイルエラーは発生しない)
return LocalVariable1 + LocalVariable2;
};
クラスのキャプチャ
また、以下のコードのようにラムダ式の中で静的ではないメンバ関数を使用したり、静的ではないメンバ変数に値を代入したり、値を読み取ったりすると「error C3493: ‘this’ cannot be implicitly captured because no default capture mode has been specified」や「error C2352: ‘ASampleActor::MemberFunction’: a call of a non-static member function requires an object」、「note: see declaration of ‘ASampleActor::MemberFunction’」等のコンパイルエラーが発生する。
//ラムダ式を定義する
auto LambdaFunction = [](const FString& InputValue) -> FString
{
//静的ではないメンバ関数を使用する(コンパイルエラーが発生する)
MemberFunction();
//静的ではないメンバ変数に値を代入する(コンパイルエラーが発生する)
MemberVariable = InputValue;
//静的ではないメンバ変数の値を読み取る(コンパイルエラーが発生する)
return MemberVariable;
};
この場合は以下のコードのようにキャプチャリストに this を追加する事でラムダ式の中でもメンバ関数を使用したり、メンバ変数に値を代入したり、値を読み取ったり出来るようになるが、ローカル変数の時と同様にスマートポインタ等を用いてクラスのライフタイムを明確に制御する必要がある。
//ラムダ式を定義する(クラスをポインタキャプチャ)
auto LambdaFunction = [this](const FString& InputValue) -> FString
{
//静的ではないメンバ関数を使用する(コンパイルエラーは発生しない)
MemberFunction();
//静的ではないメンバ変数に値を代入する(コンパイルエラーは発生しない)
MemberVariable = InputValue;
//静的ではないメンバ変数の値を読み取る(コンパイルエラーは発生しない)
return MemberVariable;
};
また、ラムダ式の中で使用するメンバ関数やメンバ変数が静的な場合はクラスのインスタンスに依存していないため、コンパイルエラーは発生しない。
//ラムダ式を定義する
auto LambdaFunction = [](const FString& InputValue) -> FString
{
//静的なメンバ関数を使用する(コンパイルエラーは発生しない)
StaticMemberFunction();
//静的なメンバ変数に値を代入する(コンパイルエラーは発生しない)
StaticMemberVariable = InputValue;
//静的なメンバ変数の値を読み取る(コンパイルエラーは発生しない)
return StaticMemberVariable;
};
最後に
参考記事
プリプロセッサディレクティブ
依存関係の解決
マクロ
- UPROPERTY
- UE5/UE4 C++で変数のアクセス権系の属性をプロパティ指定子(Property Specifiers)で指定する(UPROPERTY(EditAnywhere)、UPROPERTY(EditDefaultsOnly)、UPROPERTY(EditInstanceOnly)、UPROPERTY(VisibleAnywhere)、UPROPERTY(VisibleDefaultsOnly)、UPROPERTY(VisibleInstanceOnly))
- UE4 よく使うUPROPERTYメモ
- [UE5]UE初心者がよく使っているUPROPERTY、UFUNCTIONまとめ
- [UE5]便利そうなUPROPERTYまとめ
- UObjectの動作原理
キーワード
- constexpr(Constant Expressions:定数式)
- 符号なし整数型 【unsigned integer type】 unsigned int型 / uint型
- new(placement new)
- template
- extern
- TLazySingleton