はじめに
この記事では Unreal C++ について学んだことや調べたことをひたすら追加していきます!
(もし誤った内容を見つけた場合は X(Twitter)の方で優しく教えていただけると幸いです)
プリプロセッサディレクティブ
まずはプリプロセッサディレクティブについてです。
プリプロセッサディレクティブについては Microsoft Learn で以下のように説明されています。
#define や #ifdef のようなプリプロセッサ ディレクティブは通常、異なる実行環境でソース プログラムを簡単に変更したりコンパイルしたりするために使用されます。
プリプロセッサ ディレクティブ | Microsoft Learn
要するに「#」から始まるやつです。
Unreal C++ で一番目にするものでいうと #include がそれに該当します。
pragma once
Unreal Editor から C++ クラスを作成するとヘッダーファイルの上部に #pragma once と書かれるかと思います。

普段あまり意識することのない pragma once ですが、具体的にどのようなことをしてくれているかが分からなかったので調べてみました。
一言でいうと pragma once は同じファイルが複数回展開されないようにしてくれているっぽいです。
通常、とあるヘッダーファイルを他の複数のファイルで include すると、それぞれのファイルでヘッダーファイルの内容が複数回展開されます。
これにより、同じ定義が複数見つかって重複定義エラーを引き起こしてしまいます。
しかし、ヘッダーファイルに「#pragma once」と記述すると、そのヘッダーファイルが1度だけインクルードされるようにコンパイラに指示することができます!
例えば B.h と C.h で「#include “A.h” 」と書かれているとします。
このままでは B.h と C.h でそれぞれ A.h が展開されてしまいますが、A.h に「#pragma once」と記述すると A.h が1度のみ展開されるようになるといった感じです。
ちなみに pragma once は C++ 標準の機能というわけではなく、特定のコンパイラのみがサポートしている機能らしいです。
pragma region
次は pragma region です。
これは何か処理動作に影響を与えるわけではなく、開発者がコードをより見やすくするための機能のようなものです。
例えば以下のように記述するとソースコード内の任意の範囲を折り畳むことができます。
#pragma region {表示名}
//メンバ変数など
#pragma endregion


依存関係の解決
次は依存関係の解決についてです。
とあるクラスや構造体、関数、変数などの定義が見当たらないにも関わらず、それらを参照しようとすると「それは定義されてないよ!」というエラーが発生します。
これを解決する方法はいくつかありますが、代表的なものを紹介します。
include
まずは include です。
一番よく使います。
include は以下のように書くと、他のヘッダーファイルをその場所で展開してくれます。
(基本的に include するのはヘッダーファイルであることが多いですが、やろうと思えばソースファイルも include できるっぽいです)
#include "{フォルダ名}/{ファイル名}.h"
より詳しく知りたい方は「【C++】C++のヘッダインクルード周りの話 その1(includeの目的と分割コンパイルの基礎)」という記事が非常に参考になります!
using
次は using です。
using にも使い方がいくつかあるので紹介します。
名前空間や型の別名の定義
まず最初は名前空間や型の別名を定義するという使い方です。
ザックリいうと「using {別名} = {型名};」と書くことで名前空間や型の別名を定義することができます。
例えばこのコードでは FString クラスを「str」という別名で使用できるようにしています。
// FString の別名を str と定義
using str = FString;
void ASampleActor::Hoge()
{
// FString ではなく str と書いてもコンパイルが通るようになる
str Message = TEXT("Hello World");
UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
}
名前空間名の省略
次は名前空間名の省略です。
こちらも超ザックリと説明すると、本来なら「{名前空間名}::{関数名}()」と記述しなくてはいけない場合でも「using namespace {名前空間名};」という1行を追加することで「{関数名}()」のみの記述でもコンパイルが通るようになります。
具体的例を見ていきましょう。
例えば SampleNamespace という名前空間で SampleFunction() という関数を定義したとします。
#pragma once
namespace SampleNamespace
{
void SampleFunction()
{
}
}
他のファイルでこの SampleFunction() を使いたいときは include したうえで SampleNamespace::SampleFunction() と書く必要がありますが、using で名前空間をインポートすると SampleFunction() という記述のみでその関数を使えるようになります。
// using のみではなく include も必要
#include "SampleNamespace.h"
// using で名前空間をインポート
using namespace SampleNamespace;
void ASampleActor::Hoge()
{
//本来なら SampleNamespace::SampleFunction() と記述しなければコンパイルエラーが発生するが、
// using を使用することで、関数名のみの記述でもコンパイルエラーが発生しなくなる
SampleFunction();
}
ただ、その名前空間に宣言されている変数や関数と同じ名前のものがスコープ内に存在する場合は注意が必要です。
前方宣言
最後は前方宣言です。
ヘッダーファイルで関数や変数を宣言するときに「型名だけ分かればいいから、わざわざ include はしたくないなぁ」という場合に使用します。
もう少し具体的にいうと、外部のヘッダーファイルに宣言してあるクラスや構造体などの名前のみが必要で、そのクラスや構造体などの詳細(メンバなど)を知る必要が無い場合に使用するのが前方宣言です。
include とは違って、ヘッダーファイルを読み込むわけではないのでコンパイル時間を減らしたり、循環参照を避けたりすることができます。
例えば以下のコードでは ASampleActor クラスで UCameraComponent* 型の CameraComponent というメンバ変数を宣言しています。
「#include “Camera/CameraComponent.h”」と書いて UCameraComponent を参照することも可能ですが、わざわざ CameraComponent.h を include する必要は無いので、「class UCameraComponent;」と書いて UCameraComponent クラスを前方宣言しています。
#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;
};
マクロ
次はマクロについて書いていきます。
超簡単に言うと UCLASS() や USTRUCT()、UPROPERTY() などの大文字で書かれてるやつですね。
感覚的には Unity C# の属性(Attribute)に近いかもしれません。
(諸説あり)
UPROPERTY
まずは UPROPERTY() についてです。
このコードのように変数の宣言の上に付けるやつです。
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Num;
アクセス権(全体)
UPROPERTY() ではその変数をどこで閲覧できて、どこで編集できるのかをある程度指定することができます。
例えば AActor クラスを継承したクラスで int32 型の Num という名前のメンバ変数を宣言したとします。
UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 Num; //追加
ASampleActor();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};
このアクタをレベルに配置してレベルエディタからその変数の値を見たり、編集したりできるのか、この C++ 製のアクタクラスをブループリントで継承してブループリントエディタからその変数の値を見たり、編集したりできるのかといった感じです。
変数の値を見たり、編集したりできるときはこの画像のようにくっきりと表示されますが、

変数の値を見るだけで編集はできないという場合はグレーアウトしたような見た目になり、値を変更することはできなくなります。

変数の値すら見ることができない場合は Details にその変数の項目が出現することすらありません。
それらを踏まえたうえで、UPROPERTY() で変数のアクセス権について指定できるものを表にまとめてみました。
プロパティ指定子 | レベルエディタでの閲覧 | レベルエディタでの編集 | ブループリントエディタなどでの閲覧 | ブループリントエディタなどでの編集 |
---|---|---|---|---|
EditAnywhere | 可 | 可 | 可 | 可 |
VisibleAnywhere | 可 | 不可 | 可 | 不可 |
EditDefaultsOnly | 不可 | 不可 | 可 | 可 |
VisibleDefaultsOnly | 不可 | 不可 | 可 | 不可 |
EditInstanceOnly | 可 | 可 | 不可 | 不可 |
VisibleInstanceOnly | 可 | 不可 | 不可 | 不可 |
アクセス権(ブループリント)
UPROPERTY() ではブループリントのイベントグラフから変数の値を読み取るだけなのか、読み書きができるのかといったことも指定できます。
このコードのように UPROPERTY() で BlueprintReadWrite と設定したときはブループリントのイベントグラフから Get も Set もできます。
UPROPERTY(BlueprintReadWrite)
int32 Num;

ところが BlueprintReadWrite ではなく、BlueprintReadOnly というものに変更すると Get のみ表示されるようになります。

private なメンバ変数に対するアクセス権
次は private なメンバ変数に対するアクセス権についてです。
普通、private なメンバ変数に対して BlueprintReadWrite を付けると「BlueprintReadWrite should not be used on private members」というコンパイルエラーが発生します。
エラー文の通り、BlueprintReadWrite は private なメンバ変数では使えないということです。
(BlueprintReadOnly でも同様のコンパイルエラーが発生します)
private:
UPROPERTY(BlueprintReadWrite)
int32 Num;
ところが、以下のコードのように UPROPERTY() で AllowPrivateAccess を true に設定するとコンパイルエラーが発生しなくなり、ブループリントからでも private なメンバ変数にアクセスできるようになります。
private:
// AllowPrivateAccess が無いとコンパイルエラーが発生する
UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
int32 Num;
AllowPrivateAccess を true に設定した private なメンバ変数に他の C++ クラスからアクセスしようとすると「’ASampleActor::Num’: cannot access private member declared in class ‘ASampleActor’」といったコンパイルエラーが発生しますが、他のクラスが C++ ではなくてブループリントの場合はアクセスできるといった感じで C++ とブループリントでできることに違いが生じるという点には注意が必要です。
TitleProperty
次は TitleProperty についてです。
構造体を配列などで使用するときに TitleProperty というものを指定することで、構造体の特定のメンバの値を配列の要素のタイトルにすることができます。
例えばこのコードのように FSampleStruct という構造体を定義したとします。
USTRUCT(BlueprintType)
struct FSampleStruct
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
FString Name;
UPROPERTY(EditAnywhere)
int32 Num;
};
他のクラスや構造体でこの FSampleStruct を配列としてエディタに表示させます。
このとき、TitleProperty には構造体のメンバの名前を指定します。
// TitleProperty には構造体のメンバの名前を設定する
UPROPERTY(EditAnywhere, meta = (TitleProperty = "Name"))
TArray<FSampleStruct> SampleStructs;
この例では FSampleStruct の Name という名前のメンバを配列の要素のタイトルに指定しているのでエディタではこのように表示されます。

ちなみに FString 型以外にも float 型なども配列の要素のタイトルに使用することができます。

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

キーワード
ここからは Unreal C++ というよりかは普通の C++ の「キーワード」について解説します。
static
まずは static についてです。
static ローカル変数
一般的に static はメンバ変数やメンバ関数に対して使うことが多いかと思いますが、関数の中のローカル変数に対しても使うことができます。
通常のローカル変数はそのスコープを抜けるとメモリから解放されて関数が再び呼び出された時にまた初期化されますが、static を付けたローカル変数である「static ローカル変数」は関数が最初に呼び出されたときに一度のみ初期化されて、その後は値を保持し続けるという挙動をします。
例えばこのコードでは通常のローカル変数を使用していて、「Hello World」と毎フレーム表示されます。
void ASampleActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//関数が呼び出されるたびに bHasLogged 変数が初期化される
bool bHasLogged = false;
if (!bHasLogged)
{
// bHasLogged は絶対に false であるため、
//このログは毎フレーム表示される
UE_LOG(LogTemp, Log, TEXT("Hello World"));
//ここで true を代入しても、この関数がまた呼び出された時には初期化されてしまう
bHasLogged = true;
}
}
ですが、static ローカル変数を使用している以下のコードは一度のみ「Hello World」と表示されます。
void ASampleActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//関数が最初に呼び出されたときだけ bHasLogged 変数が初期化される
static bool bHasLogged = false;
if (!bHasLogged)
{
//次の行で bHasLogged 変数に true を代入しているため、
//このログは一度だけ表示される
UE_LOG(LogTemp, Log, TEXT("Hello World"));
//この関数が再び呼び出される時は、bHasLogged 変数の値は true になっている
bHasLogged = true;
}
}
これは Unreal Engine のブループリントのマクロのローカル変数もどきに近い挙動かもしれません…
static 関数と const メンバ関数
次はstatic 関数と const メンバ関数についてです。
一言でいうと static 関数はそのクラスや構造体のインスタンスに依存しませんが、const メンバ関数はそれらに依存するという違いがあります。
ピュア化してブループリントから呼び出したときの違い
Unreal C++ の関数に UFUNCTION() で BlueprintPure を指定するとブループリントからその関数を呼び出すときに実行ピンのないピュアな状態のノードが出現します。
static 関数をピュア化すると下の画像のような見た目になって、その関数を宣言しているクラスや構造体のインスタンスが無くても使用することができます。
UFUNCTION(BlueprintPure)
static bool StaticFunction(); //前方に static を付ける

一方、関数名の後ろに const を付けた const メンバ関数は UFUNCTION() で BlueprintPure ではなく、BlueprintCallable を指定した場合もピュア関数のような見た目になります。
しかし、static 関数をピュア化したときとは違って、その関数を宣言しているクラスや構造体のインスタンスを Target に接続しないとコンパイルエラーが発生するようになります。
UFUNCTION(BlueprintPure) // BlueprintCallable でも同じ見た目になる
bool ConstFunction() const; //後方に const を付ける

関数の中でできることの違い
関数をピュア化してブループリントから呼び出したときの違いは分かりましたが、C++ で関数の定義を記述するときにはどのような違いが生じるのでしょうか?
一言でいうと、static 関数はそのクラスや構造体の静的ではないメンバを読み取ろうとしたり、書き込んだりしようとするとコンパイルエラーが発生しますが、const メンバ関数ではそのクラスや構造体の静的ではないメンバを読み取ることができます。
(const メンバ関数でも静的ではないメンバに書き込むことはできません)
// static 関数
bool ASampleActor::StaticFunction()
{
// static 関数はそのクラスや構造体の静的ではないメンバを参照できないため、
//以下のようにメンバにアクセスしようとするとコンパイルエラーが発生する
//(もちろん、書き込みもできない)
return MemberVariable;
}
// const メンバ関数
bool ASampleActor::ConstFunction() const
{
// const メンバ関数はそのクラスや構造体の静的ではないメンバを読み取ることはできるが、
//書き込むことはできないため、以下のコードではコンパイルエラーが発生する
MemberVariable = true;
// const メンバ関数は static 関数とは違って、
//そのクラスや構造体のメンバを読み取るだけではコンパイルエラーは発生しない
return MemberVariable;
}
template
次はテンプレート(template)について説明します。
テンプレートについては Microsoft Learn で以下のように説明されています。
テンプレートは、C++ での汎用プログラミングの基礎となります。 厳密に型指定された言語として、C++ では、すべての変数に特定の型を割り当てる必要があります。これは、プログラマが明示的に宣言するか、コンパイラが推測するかのいずれかです。 ただし、操作する型に関係なく、多くのデータ構造とアルゴリズムは同じように見えます。 テンプレートを使用すると、クラスまたは関数の操作を定義し、その操作が具体的にどの型で動作するかをユーザーが指定できるようになります。
テンプレート (C++) | Microsoft Learn
事前に厳密に型を指定しなくてもユーザーがそれを使用するときに任意の型を指定できるということですね。
テンプレートの自作
ではテンプレートを自分で作る方法を紹介します。
関数テンプレート
まずは関数テンプレートです。
Unreal C++ では Cast() などが関数テンプレートに該当します。
通常、関数を宣言するときは引数と戻り値の型をあらかじめ指定しておく必要があります。
//引数も戻り値も型は int32
int32 Add(int32 A, int32 B);
しかし、template を使用して関数テンプレートを宣言することで、その関数の利用者が引数と戻り値に任意の型を指定できるようになります。
template<typename T>
T Add(T A, T B)
{
return A + B;
}
この Add() という関数テンプレートを呼び出すとき、引数に整数型を指定したとします。
この場合は当然、整数型どうしは加算できるのでコンパイルエラーは発生しません。
//ちなみにこの例では <int32> の部分は省略可
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);
クラステンプレート
次はクラステンプレートについてです。
関数テンプレートと同じようにクラスもテンプレート化することができます。
template<typename T>
class TSampleClassTemplate
{
public:
T Value;
};
この例のクラステンプレートを使う場合は以下のようになります。
TSampleClassTemplate<FVector> VectorSampleClassTemplate;
VectorSampleClassTemplate.Value = FVector();
TSampleClassTemplate<int32> IntSampleClassTemplate;
IntSampleClassTemplate.Value = 0;
そして Unreal C++ では TMap や TArray などの「T」から始まるものは基本的にクラステンプレートです。
指定される型を限定する
関数テンプレートやクラステンプレートの作り方は分かりましたが、そのテンプレートの利用者が誤った型を指定しないようにする、つまり関数テンプレートやクラステンプレートが利用されるときに指定される型が、あるクラスの派生クラスであることを保証したいときはどうすればいいのでしょうか?
方法はいくつかありますが、その1つは static_assert を使うというものです。
例えば以下のコードでは TemplateFunction() という関数テンプレートを呼び出すときに指定された型が UObject クラスの派生クラスではない場合にコンパイルエラーを発生させています。
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
次は TEnumAsByte についてです。
TEnumAsByte は列挙型のメンバ変数を宣言するときなどにこのコードのように使用するクラステンプレートです。
TEnumAsByte<ESampleEnum> SampleEnum;
例えばこのコードのように1バイトを超えるサイズの列挙型を定義したとします。
UENUM()
enum class ESampleEnum : int32 //ここで列挙型のサイズを 32 ビット(4バイト)に設定
{
A,
B,
C
};
C++ で以下のように書いてブループリントからその列挙型を使用すると、その列挙型の変数のピンの色が緑色ではなくなり、「unsupported_enum_type: enum size is larger than a byte」というエラーが表示されます。
UPROPERTY(BlueprintReadWrite)
ESampleEnum SampleEnum;

これの対処法としては列挙型を定義するときにその列挙型のサイズを int32(4バイト)から uint8(1バイト)に変更するか、UENUM() に BlueprintType を指定して uint8 以外のサイズの列挙型を定義できなくさせる必要があります。
BlueprintType を使う方法では 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バイトに抑えつつ符号付き整数を使用することも可能です。
例えばエンジン内部では「Runtime/ImageWrapper/Public/IImageWrapper.h」の EImageFormat で int8 (1バイト)の列挙型を定義しています。
(UE5.3.2 で確認)
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
次は TLazySingleton についてです。
これは遅延初期化という方法を使えるシングルトンを簡単に作るために使用するもので、ランタイム(ゲーム実行中)に使用するものというよりかはエディタ拡張やエンジン内部のエディタ部分の改造などで使う気がします。
通常のシングルトンはプログラム実行時(開始時)にインスタンスを生成しますが、そのインスタンスが必要になるまで何も生成せず、インスタンスが必要になったタイミングで初めて生成するという処理を遅延初期化といいます。
UE には遅延初期化と任意のタイミングでの破棄が可能なシングルトンを実現するクラステンプレートが用意されています。
それが TLazySingleton です。
基本的に TLazySingleton は以下のコードのように使用します。
主なポイントはこの2つです。
- コンストラクタとデストラクが 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 invalid!」と表示され続けるようになります。
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 invalid!"));
}
}
friend
次は friend についてです。
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:
// 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:
// private なメンバ関数を宣言
void PrivateFunction();
// ASampleActor クラスをフレンドクラスに設定する
friend class ASampleActor;
};
void ASampleActor::Hoge()
{
USampleObject* SampleObject = NewObject<USampleObject>();
// ASampleActor クラスは USampleObject クラスのフレンドクラスであるため、
// USampleObject クラスの private なメンバにアクセスしてもコンパイルエラーが発生しない
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:
// private なメンバ変数を宣言
FString PrivateVariable;
// USampleObject クラスのなかで宣言しているが、これはフリー関数
void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject);
};
//どのクラスにも属さないフリー関数
//(「USampleObject::」が付いてない)
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:
// private なメンバ変数を宣言
FString PrivateVariable;
//フリー関数 AccessToSampleObjectPrivateVariable() をフレンド関数に設定する
friend void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject);
};
//どのクラスにも属さないフリー関数
//(「USampleObject::」が付いてない)
void AccessToSampleObjectPrivateVariable(USampleObject* SampleObject)
{
// AccessToSampleObjectPrivateVariable() は USampleObject クラスのフレンド関数であるため、
//コンパイルエラーは発生しない
UE_LOG(LogTemp, Log, TEXT("%s"), *SampleObject->PrivateVariable);
}
extern
次は extern です。
extern は日本語では「外部」という意味です。
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 でも同じ名前のグローバル変数を定義しようとしているため、
// fatal error LNK1169: one or more multiply defined symbols found
//というコンパイルエラーが発生する
FString SampleVariable;
ASampleActor2::ASampleActor2()
{
PrimaryActorTick.bCanEverTick = true;
}
しかし、今回の場合はどちらか片方に extern を付け、グローバル変数を extern 宣言すると他のソースファイルに定義されているグローバル変数を参照することができます。
(「新たなグローバル変数を宣言するのではなくて、既に定義されているグローバル変数を使うよ!」ということをコンパイラに伝えられる)
先ほどの例では SampleActor2.cpp で同じ名前のグローバル変数を extern 宣言すると元のグローバル変数(SampleActor.cpp の SampleVariable 変数)の値を参照することができるため、このコードでは ASampleActor2::BeginPlay() の実行時に「Hello World」と表示されます。
#include "SampleActor2.h"
// extern 宣言することによって、
//既に定義済みの SampleVariable 変数を参照するとコンパイラに伝えられる
extern FString SampleVariable;
ASampleActor2::ASampleActor2()
{
PrimaryActorTick.bCanEverTick = true;
}
void ASampleActor2::BeginPlay()
{
Super::BeginPlay();
// SampleVariable 変数の値をログに表示する
//(中身は SampleActor.cpp の SampleVariable 変数の値)
UE_LOG(LogTemp, Log, TEXT("%s"), *SampleVariable);
}
static との関係
では extern 宣言して参照したいグローバル変数が static の場合はどうなるのでしょうか?
グローバル変数を定義するときに static を付けると static グローバル変数となり、その static グローバル変数を定義しているソースファイルからはその変数を参照することできますが、他のソースファイルからは参照することができなくなります。
#include "SampleActor.h"
// static グローバル変数を定義
static FString SampleVariable = TEXT("Hello World");
ASampleActor::ASampleActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void ASampleActor::BeginPlay()
{
Super::BeginPlay();
//同じソースファイルからは static グローバル変数を参照できる
UE_LOG(LogTemp, Log, TEXT("%s"), *SampleVariable);
}
#include "SampleActor2.h"
// extern 宣言しても
// fatal error LNK1120: 1 unresolved externals
//というコンパイルエラーが発生する
extern FString SampleVariable;
ASampleActor2::ASampleActor2()
{
PrimaryActorTick.bCanEverTick = true;
}
constexpr
次は constexpr についてです。
「const」という単語が含まれているので const と同じような使い方をします。
通常、const を使って初期化した変数はプログラム実行時に値が確定しますが、プログラム実行時ではなく、コンパイル時に値を確定させたい定数には constexpr を使用します。
少しイメージが湧きづらいので実際にコードを書いた際にどういった場合ではコンパイルエラーが発生して、逆にどうするとコンパイルエラーが発生しなくなるのかを見ていきます。
例えばこのコードでは const を使用してローカル変数を初期化するときに static でも const でもない関数の戻り値を代入していますが、コンパイルエラーは発生していません。
void ASampleActor::Hoge()
{
// const を付けたローカル変数を初期化
const int32 ConstVariable = GetNum();
}
// static でも const でもない関数
int32 ASampleActor::GetNum()
{
return 1;
}
ところが、このコードのように constexpr を使用している変数を初期化するときに static でも const でもない関数の戻り値を代入しようとすると「error C2131: expression did not evaluate to a constant」というコンパイルエラーが発生します。
void ASampleActor::Hoge()
{
// constexpr を付けたローカル変数を初期化
constexpr int32 ConstVariable = GetNum();
}
// static でも const でもない関数
int32 ASampleActor::GetNum()
{
return 1;
}
また、今回の例ではヘッダーファイルで GetNum() を宣言するときに static を付けて GetNum() を static 関数にしてもコンパイルエラーが発生します。
// .h
static int32 GetNum();
そして、ヘッダーファイルとソースファイルで GetNum() に constexpr を付けて GetNum() を static 関数ではなく、constexpr 関数にした場合もコンパイルエラーが発生します。
// .h
constexpr int32 GetNum();
// .cpp
constexpr int32 ASampleActor::GetNum()
{
return 1;
}
さらにヘッダーファイルでは GetNum() に 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 な変数を初期化するときに代入するものを関数の戻り値ではなく、ローカル変数の値にときはこのコードのように constexpr を使用して宣言した変数に const ではない変数の値を代入して初期化しようとするとコンパイルエラーが発生します。
void ASampleActor::Hoge()
{
// const ではないローカル変数を初期化
int32 Num = 1;
// constexpr を付けたローカル変数を初期化
//(コンパイルエラーが発生する)
constexpr int32 ConstVariable = Num;
}
この場合は constexpr を付けたローカル変数を初期化するときに使うローカル変数が const で初期化されているとコンパイルエラーが発生しなくなります。
void ASampleActor::Hoge()
{
// const なローカル変数を初期化
const int32 Num = 1;
// constexpr を付けたローカル変数を初期化
//(コンパイルエラーは発生しない)
constexpr int32 ConstVariable = Num;
}
explicit
次は explicit です。
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 のコンストラクタの引数である整数型の値のみを渡すことで
//暗黙的に整数型を 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) {}
};
// FSampleStruct を引数に渡す必要のある関数を定義
void ASampleActor::Hoge(const FSampleStruct& SampleStruct)
{
}
void ASampleActor::Hoge2()
{
// 1 という整数を暗黙的に FSampleStruct に変換できないため、
// error C2664: 'void ASampleActor::Hoge(const FSampleStruct &)': cannot convert argument 1 from 'int' to 'const FSampleStruct &'
//というコンパイルエラーが発生する
Hoge(1);
// 1 という整数を暗黙的に FSampleStruct に変換できないため、
// error C2440: 'initializing': cannot convert from 'int' to 'FSampleStruct'
//というコンパイルエラーが発生する
FSampleStruct SampleStruct = 1;
}
new
次は new についてです。
placement new
new には役割や使い方がいくつかありますが、ここでは placement new について解説します。
placement new は日本語では「配置 new」などと呼ばれ、通常の 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 はメモリを厳密に管理したいときや、メモリの使用量の計測や制限などを行いたいとき、処理速度を上げたいときなどに使用します。
ヒープ領域を確保する場合
では実際の使用方法を見てみましょう。
一般的にヒープ領域の確保のみを行う場合はこのコードように記述し、メモリを解放するときは delete は使用せず、デストラクタを呼び出した後に free() を使用します。
malloc(sizeof({型名}));
実際の例は以下の通りです。
// 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
次は unsigned です。
unsigned は例えば、以下のように int 型の変数を宣言するときに使うとその変数に負の値を代入できないようにすることができます。
unsigned int Num = 1;
通常、32 ビットの int 型で表せる値の範囲は -2,147,483,648~2,147,483,647 ですが、unsigned を付けると負の値を表すのに使用していた分を全て正の値を表すために使うことができるため、32 ビットの符号なし整数型の表せる値の範囲は 0~4,294,967,295 となります。
auto
次は auto です。
auto は C# の var に近いかと思います。
例えば、auto を使用すると以下のコードのように明示的に型を記述しなくてもコンパイルが通るようになります。
// FString ではなく auto と記述
auto Message = FString(TEXT("Hello World"));
//「Hello World」と表示される
UE_LOG(LogTemp, Log, TEXT("%s"), *Message);
// int ではなく auto と記述
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 の型が非常に詳細で読みづらくなります。
・テンプレートのコードで認められます。この場合、式の型は簡単に見分けることはできません。これは高度な事例です。
演算子
次は演算子についてです。
デリファレンス演算子
「*」という記号はポインタ変数を宣言するときや、掛け算をするときなどに使用しますが、Unreal C++ では少し特殊な使い方をすることがあるのでメモしておきます。
例えば FString 型の変数の値を UE_LOG() などに渡すときに以下のように記述することがあります。
UE_LOG(LogTemp, Log, TEXT("%s"), *StringVariable);
UE を使ってゲームを開発する側は 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;
関数ポインタ
次は関数ポインタについてです。
関数を変数のように扱いたいときは関数ポインタというものを使用します。
メンバ関数ポインタ
クラスや構造体のインスタンスに依存するメンバ関数(静的ではないメンバ関数)を関数ポインタで扱いたい場合は以下のような書き方で初期化します。
これを「メンバ関数ポインタ」といいます。
{戻り値の型名}({クラス名}:: * {メンバ関数ポインタ名})({引数の型名}, {引数の型名}) = &{クラス名}::{メンバ関数名};
そしてメンバ関数ポインタが指しているメンバ関数を呼び出すときは以下のように記述します。
//ポインタ経由でオブジェクトにアクセスする場合
({オブジェクトのポインタ}->*{メンバ関数ポインタ名})({引数の値}, {引数の値});
//オブジェクトに直接アクセスする場合
({オブジェクト}.*{メンバ関数ポインタ名})({引数の値}, {引数の値});
実際のコードの例は以下の通りです。
この例では ASampleActor クラスの Add() を Hoge() のなかでメンバ関数ポインタとして変数のように扱っています。
//メンバ関数ポインタに格納されるメンバ関数
int32 ASampleActor::Add(int32 A, int32 B)
{
return A + B;
}
void ASampleActor::Hoge()
{
//メンバ関数ポインタを初期化する
int32(ASampleActor:: * MemberFunctionPtr)(int32, int32) = &ASampleActor::Add;
//メンバ関数ポインタが指している関数(ASampleActor::Add)を呼び出す
int32 Result = (this->*MemberFunctionPtr)(1, 2);
//この例では「3」と表示される
UE_LOG(LogTemp, Log, TEXT("%d"), Result);
}
通常の関数ポインタ(静的関数など)
またクラスや構造体のインスタンスに依存しない静的関数などを関数ポインタで扱いたい場合は以下のようにして初期化します。
{戻り値の型名}(*{関数ポインタ名})({引数の型名}, {引数の型名}) = &{関数名};
そして関数ポインタが指している関数を呼び出すときは以下のように記述します。
{関数ポインタ名}({引数の値}, {引数の値});
実際の例は以下の通りです。
この例では ASampleActor クラスの StaticAdd() という静的関数を Hoge() のなかで関数ポインタとして変数のように扱っています。
//関数ポインタに格納される静的関数
//(ヘッダーファイルでの宣言では static が付いている)
int32 ASampleActor::StaticAdd(int32 A, int32 B)
{
return A + B;
}
void ASampleActor::Hoge()
{
//関数ポインタを初期化する
//(今回の場合は &ASampleActor::StaticAdd でも &StaticAdd でもどちらでも良い)
int32(*StaticFunctionPtr)(int32, int32) = &StaticAdd;
//関数ポインタが指している関数(ASampleActor::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;
};
this をキャプチャする
次は this のキャプチャについてです。
クラスや構造体の中でそのオブジェクト自身を取得するときは this を使うことがありますが、ラムダ式ではその this もキャプチャすることができます。
例えば以下のようにラムダ式の中で静的ではないメンバ関数を使用したり、静的ではないメンバ変数に値を代入 or 読み取ったりすると「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 を追加することでラムダ式の中でもメンバ関数を使用したり、メンバ変数に値を代入 or 読み取ったりすることができるようになりますが、ローカル変数のときと同様にスマートポインタなどを使用してそのオブジェクトのライフタイムを明確に制御する必要があります。
//ラムダ式を定義する(自身をポインタキャプチャ)
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