Epic ではシンプルなコーディング規約をいくつか使用しています。このページでは、現在の Epic Games のコーディング規約を説明します。コーディング規約には必ず従わなければなりません。
コーディング規約がプログラマーにとって重要な理由はいくつかあります。
-
ソフトウェアのライフタイム コストの 80 %はメンテナンス関連です。
-
ソフトウェアの最初の制作者がライフタイムを通してメンテナンスを継続することはほとんどありません。
-
コーディング規約によってソフトウェアの可読性が向上し、エンジニアは新しいコードを迅速にしっかりと理解できるようになります。
-
MOD コミュニティのデベロッパーに公開するソース コードを理解しやすく用意することが重要です。
-
クロスコンパイラの互換性を維持するために、こうした規則の多くが必要です。
以下のコーディング規約は C++ が中心となっていますが、どの言語を使用した場合でもこの規約に従うことが求められます。特定の言語に対して必要な場合には、同等のルールや例外が示されています。
クラスの構成
クラス は、書き手の都合ではなく読み手の立場で構成するべきです。読み手のほとんどがクラスに public なインターフェースを使用するため、まず public な実装を宣言し、次にクラスの private な実装を行います。
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
public:
// このアクタのプロパティのデフォルト値を設定します。
AExampleActor();
protected:
// ゲームの開始時またはスポーン時に呼び出されます。
virtual void BeginPlay() override;
};
著作権情報
Epic が public な配布の目的で提供するすべてのソース ファイル (.h
、.cpp
、.xaml
) には、ファイルの最初の行に必ず著作権表示がなされていなくてはなりません。表示フォーマットは下記の例と正確に一致させてください。
// Copyright Epic Games, Inc. All Rights Reserved.
この行の表示がない場合やフォーマットに誤りがある場合、CIS がエラーとなり失敗します。
命名規則
命名規則を使用する場合、すべてのコードおよびコメントは米国英語のスペルとグラマーを使用しなければなりません。
- 名前に含まれる各用語の最初の文字 (例:型や変数) は大文字とし、通常は用語間にアンダースコアを使用しません。たとえば、
Health
とUPrimitiveComponent
は正しく、lastMouseCoordinates
やdelta_coordinates
は間違いです。
これは、他のオブジェクト指向のプログラミング言語に精通しているユーザー向けの PascalCase 形式です。
-
型名には大文字をプレフィックスとして追加し、変数名と区別します。たとえば
FSkin
は型名で、Skin
はFSkin
のインスタンスとなります。 -
テンプレート クラスにはプレフィックス T が付きます。
class TAttribute
- UObject から継承されるクラスにはプレフィックス U が付きます。
class UActorComponent
- AActor から継承されるクラスにはプレフィックス A が付きます。
class AActor
- SWidget から継承されるクラスにはプレフィックス S が付きます。
class SCompoundWidget
- 抽象インターフェースのクラスにはプレフィックス I が付きます。
class IAnalyticsProvider
- Epic の概念に似たクラス型には、プレフィックス C が付いています。
template <typename Concept, typename... Ts>
- 列挙型変数にはプレフィックス E が付きます。
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};
- ブール変数には、必ずプレフィックス b を付けてください。
bPendingDestruction
bHasFadedIn.
-
その他のほとんどのクラスにはプレフィックス F が付きますが、サブシステムによっては別の文字が使用されます。
-
Typedef には、次のようにその型に対して適切なプレフィックスが付きます。
-
構造体の typedef には F
-
UObject
の typedef には U
-
-
特定のテンプレートのインスタンス化の typedef は、テンプレートではなくなり、それに応じてプレフィックスが付きます。
typedef TArray<FMytype> FArrayOfMyTypes;
-
C# ではプレフィックスは省略されます。
-
Unreal Header Tool では正しいプレフィックスが必要な場合が多いため、正しいものを使うことが重要です。
-
型テンプレート パラメータと、それらのテンプレート パラメータに基づくネスティングされている型エイリアスは、型カテゴリが不明なため、上記のプレフィックス規則の対象ではありません。
-
説明的な用語の後に型サフィックスを付けることをお勧めします。
-
In プレフィックスを使用して、テンプレート パラメータをエイリアスから明確にします。
template <typename InElementType>
class TContainer
{
public:
using ElementType = InElementType;
};
-
型と変数の名前には名詞を使用します。
-
メソッド名には、その効果を説明する動詞、または効果のないメソッドの戻り値を説明する動詞を使用します。
-
マクロ名は、すべて大文字にして単語をアンダースコアで区切り、
UE_
をプレフィックスとして付ける必要があります。
#UE_AUDIT_SPRITER_IMPORT を定義する
変数、メソッド、クラス名は次のことを考慮する必要があります。
-
明確であること
-
曖昧でないこと
-
記述的であること
名前のスコープが大きいほど、名前の明確さがより重要となります。過度に名前を省略しないでください。
変数の意味をコメントとして付けられるように、変数ごとに行を変えて宣言する必要があります。
これは JavaDocs のスタイルの要求事項です。
変数の前のコメントは 1 行でも複数行でもかまいません。変数をグループ化する空白行の挿入は任意となっています。
bool を返すすべての関数は、true または false の質問形式とします。たとえば、IsVisible()
や ShouldClearBuffer()
です。
プロシージャ (戻り値のない関数) の名前には、明確な動詞の後にオブジェクトが続きます。ただし、メソッドのオブジェクトがそのメソッドが所属するオブジェクト自体である場合は例外です。その場合はコンテキストからオブジェクトが認識されます。「Handle」や「Process」のような動詞は曖昧になるため、使用は避けてください。
次のような場合は、関数パラメータ名にプレフィックスとして「Out」を付けることを推奨します。
-
関数パラメータが参照から渡される場合
-
関数によって値が書かれる場合
こうすることで、引数に渡された値が関数によって置き換えられることが明白になります。
In または Out のパラメータも boolean の場合、bOutResult
のように In/Out のプレフィックスの前に「b」を付けます。
値を返す関数は、戻り値を名前で説明すべきです。関数が返す値を名前によって明確にします。これは特にブール関数で重要です。以下の 2 通りの例を検討してください。
// true の意味は?
bool CheckTea(FTea Tea);
// true はお茶が新鮮なことを意味しているのが、名前によって明確になります
bool IsTeaFresh(FTea Tea);
float TeaWeight;
int32 TeaCount;
bool bDoesTeaStink;
FName TeaName;
FString TeaFriendlyName;
UClass* TeaClass;
USoundCue* TeaSound;
UTexture* TeaTexture;
インクルーシブな言葉の選択
Unreal Engine のコードベースでは、敬意、包摂性、高い職業意識を持って言葉を使用してください。
次を命名する際に、言葉の選択を考慮します。
-
クラス
-
関数
-
データ構造
-
型
-
変数
-
ファイルおよびフォルダ
-
プラグイン
UI、エラー メッセージ、通知など、ユーザー向けのスニペットを記述する際にも当てはまります。また、コメントやチェンジリストの記述など、コードに関する記述にも当てはまります。
以下のセクションに、あらゆる状況や対象者に敬意を払った適切な言葉や名称を選択し、より有効な伝え方ができるよう、ガイダンスや提案を示します。
人種、民族、宗教の包摂性
-
固定観念を助長するような暗喩や直喩は使用しません。たとえば、黒と白の対比や blacklist と whitelist があります。
-
歴史的トラウマや差別の実体験を想起させる言葉は使用しません。たとえば slave、master、nuke があります。
ジェンダーの包摂性
-
単数の場合でも、they、them、their などで仮想的に集団を指すようにします。
-
人物以外のものを指す場合は it や its を使用します。たとえば、モジュール、プラグイン、関数、クライアント、サーバー、その他ソフトウェアやハードウェアのコンポーネントの場合です。
-
性別のないものには性別を適用しません。
-
guys などの性別を連想させる集合名詞は使用しません。
-
「a poor man's X」など、偶然性別を含むような口語表現を避けます。
スラング
-
世界中の人々に読まれることを意識します。彼らは同じ慣用句や考え方を共有したり、文化的に同じものを連想したりするわけではありません。
-
害がなく面白いと思っても、スラングや口語表現は避けるようにします。第 1 言語が英語でない人には理解しづらく、うまく伝わらないかもしれません。
-
下品な言葉は使用しません。
多義語
- 技術的な意味で使用する用語の多くは、技術用途以外の意味もあります。たとえば abort、execute、native があります。こうした用語を使う場合は、文脈をよく吟味して正確に使用してください。
用語リスト
以下のリストは、過去に Unreal のコードベースで使われていたものの、別の用語に置き換えた方がよいと思われる用語です。
用語名 | 代替用語名 |
---|---|
ブラックリスト | _deny list_ 、_block list_ ,_exclude list_ 、_avoid list_ ,_unapproved list_ ,_forbidden list_ ,_permission list_ |
ホワイトリスト | allow list、include list、trust list、safe list、prefer list、approved list、permission list |
マスター | primary、source、controller、template、reference、main、leader、original、base |
スレーブ | secondary、replica、agent、follower、worker、cluster node、locked、linked、synchronized |
上記の原則に従ってコードを記述するよう、積極的に取り組んでいます。
移植可能な C++ のコード
int
型と符号なしの int
型は、プラットフォームによってサイズが変動します。これらは最低 32 ビット幅が保証され、整数の幅が重要でない場合に使用が認められます。シリアル化またはレプリケートされたフォーマットでは、サイズが明示的に指定された型を使用します。
次に一般的な型を示します。
-
Boolean 値に使う
bool
(Bool のサイズは想定しない)。BOOL
はコンパイルしません。 -
character 用の
TCHAR
(TCHAR のサイズは想定しない) -
符号なしバイト用の
uint8
(1 バイト) -
符号付きバイト用の
int8
(1 バイト) -
符号なし shorts 用の
uint16
(2 バイト) -
符号付き shorts 用の
int16
(2 バイト) -
符号なし ints 用の
uint32
(4 バイト) -
符号付き ints 用の
int32
(4 バイト) -
符号なし quad words 用の
uint64
(8 バイト) -
符号付き quad words 用の
int64
(8 バイト) -
単精度浮動小数点用の
float
(4 バイト) -
倍精度浮動小数点用の
double
(8 バイト) -
ポインタを保持する整数用の
PTRINT
(PTRINT のサイズは想定しない)
標準ライブラリの使用
これまで UE は、次の理由から C および C++ 標準ライブラリの直接使用を避けてきました。
-
速度の遅い実装を、メモリ割り当てをより制御できる独自の実装に置き換えるため
-
広範囲で使用可能になる前に、次のような新機能を追加するため
-
標準以外の挙動の変更をするため
-
コードベース全体の構文に整合性を持たせるため
-
UE のイディオムとの互換性のない構成を避けるため
-
しかし、標準ライブラリは進化し、抽象化レイヤーでのラップや独自実行を避けたい機能が含まれるようになりました。
独自のライブラリでも標準ライブラリでも可能な場合は、結果の良い方法が好ましいですが、整合性が非常に重要であることを覚えておきましょう。従来の UE の実装では目的を達成できない場合、それを非推奨にして、すべての使用を標準ライブラリへ移行することができます。
同じ API の中に UE のイディオムと標準ライブラリのイディオムが混在しないようにしてください。次の表に、一般的なイディオムと使用する際の推奨事項を示します。
イディオム | 説明 |
---|---|
<atomic> |
アトミックのイディオムは新しいコードで使用し、その場合は古いコードも新しいコードに移行する必要があります。アトミックはサポートされたすべてのプラットフォームにおける完全かつ効率的な実装が求められます。独自の TAtomic は部分的な実装であり、メンテナンスや改善は意図されていません。 |
<type_traits> |
型のトレイトのイディオムは従来の UE トレイトと標準のトレイトで重複がある場合に使用します。トレイトは正確さのために compiler intrinsics として実装されることが多くあります。コンパイラは標準トレイトの知識があり、プレーン C++ として扱うのではなく、より高速なコンパイル パスを選択することができます。気を付けるべき点は、独自のトレイトでは通常大文字の Value static または Type typedef ですが、標準のトレイトでは value と type の使用が求められます。特定の構文はコンポジションのトレイトによって求められるため、この区別は重要です (std::conjunction など)。新しいトレイトを追加する場合は、コンポジションをサポートするために小文字の value または type で書きます。どちらもサポートするために、既存のトレイトを更新する必要があります。 |
<initializer_list> |
初期化リストのイディオムは、波括弧初期化構文をサポートするために使用する必要があります。これは言語と標準ライブラリが重複し、サポートするための手段が他にない場合です。 |
<regex> |
正規表現のイディオムは直接使用できますが、エディタ専用コードでカプセル化して使用します。独自の正規表現ソリューションを実装する計画はありません。 |
<limits> |
std::numeric_limits は全体の中で使用することができます。 |
<cmath> |
このヘッダのすべての浮動小数点関数を使用できます。 |
<cstring> :memcpy() および memset() |
パフォーマンス上の明らかな利点がある場合は、これらのイディオムをそれぞれ FMemory::Memcpy と FMemory::Memset の代わりに使用できます。 |
標準のコンテナおよび文字列は interop code での場合以外は避けてください。
コメント
コメントはコミュニケーションの手段であり、コミュニケーションは必要不可欠です。以下のセクションでは、コメントを書く際に重要な点について説明しています (Kernighan & Pike The Practice of Programming から引用)。
ガイドライン
- 自己説明的なコードを書いてください。たとえば、次のようになります。
~~~ // 悪い例: t = s + l - b;
// 良い例: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
* 役立つコメントを書いてください。たとえば、次のようになります。
~~~
// 悪い例:
// 茶葉を増やす
++Leaves;
// 良い例:
// 別の茶葉があります
++Leaves;
-
悪いコードはコメントでごまかさず、コードを書き直してください。たとえば、次のようになります。
~~~ // 悪い例: // 茶葉の合計は // 小さい葉と大きい葉の合計から // 両方の茶葉の数を引いた数です t = s + l - b;
// 良い例: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves; ~~~
-
コードは矛盾しないようにしてください。たとえば、次のようになります。
// 悪い例: // 茶葉を増やさないでください! ++Leaves; // 良い例: // 別の茶葉があります ++Leaves;
Const を正しく設定する
const はドキュメンテーションでもあり、コンパイラ ディレクティブでもあります。そのため、すべてのコードで const を正しく設定するようにします。これには、次のガイドラインが含まれます。
-
引数が関数によって書き換えられない場合に const ポインタや参照によって関数の引数を渡します。
-
オブジェクトを書き換えない場合は const としてメソッドにフラグ付けします。
-
ループがコンテナを書き換えない場合は、コンテナに const のイテレーションを使います。
Const の例:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// ここで InArray は変更されませんが、OutResult は変更される可能性があります
}
void FThing::SomeNonMutatingOperation() const
{
// このコードは呼び出される FThing を変更しません
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// このループのボディは StringArray を変更しません
}
Const は値渡しの関数パラメータやローカルでも推奨されます。Const は変数が関数のボディ内で変更されないことを示すため、読み手が理解しやすくなります。この場合、JavaDoc プロセスに影響を与えることができるので、宣言とその定義が一致するようにします。
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Count も CountPlusOne も関数のボディでは変更できません
}
唯一の例外はコンテナへ移動する値渡しのパラメータです。詳細については、このページの「ムーブ セマンティクス」セクションを参照してください。
例:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
(指定先ではなく) ポインタ自体を const にする場合は、最後に const キーワードを入れます。参照を「再代入する」方法はないため、同じ方法で const にすることはできません。
例:
// 非定数のオブジェクトへの Const ポインタ - ポインタを再割り当てすることはできませんが、T を変更することはできます
T* const Ptr = ...;
// 不正
T& const Ref = ...;
戻り型で const は絶対に使用しないでください。これは複合型に対するムーブ セマンティクスを禁止し、組み込み型に対してコンパイルの警告を発するためです。このルールは戻り型そのものにのみ適用されます。ポインタのターゲット型や戻されている参照には適用されません。
例:
// 悪い例 - const 配列を返します
const TArray<FString> GetSomeArray();
// 良い例 - const 配列への参照を返します
const TArray<FString>& GetSomeArray();
// 良い例 - const 配列へのポインタを返します
const TArray<FString>* GetSomeArray();
// 悪い例 - const 配列への const ポインタを返します
const TArray<FString>* const GetSomeArray();
フォーマットの例
Epic では JavaDoc に基づいたシステムを使用し、コードから自動的にコメントを抽出してドキュメントを作成します。そのため、特定のコメント形式に関するルールを推奨します。
以下は クラス、メソッド、変数 コメントのフォーマットの実例です。コメントはコードを補強するということを覚えておいてください。コードは実装を文書化し、コメントは意図を文書化します。部分的であってもコードの意図を修正した場合は、必ずコメントも更新してください。
Steep
方式および Sweeten
方式で具体化された 2 通りのパラメータ コメント スタイルがサポートされています。Steep
方式の @param
スタイルが従来の複数行のスタイルですが、シンプルな関数に関しては、パラメータと戻り値の文書を説明コメントとまとめると、より明確になります。これは、Sweeten
の例で確認できます。@see
や @return
のような特別コメントのタグは、最初の説明の後で新しい行を開始する場合にのみ使用します。
メソッド コメントは、メソッドがパブリックに宣言された場所で一度だけ書いてください。メソッド コメントは、呼び出し元に関連するメソッドのオーバーライドに関する情報など、メソッドの呼び出し元に関連した情報のみを書きます。メソッドの実装と呼び出し元に関係のないメソッドのオーバーライドに関する詳細は、メソッドの実装の中でコメントとして残してください。
クラス コメントには以下を含めます。
-
このクラスが解決する問題の説明。
-
このクラスが作成された理由。
複数行のメソッド コメントには以下を含めます。
-
関数の目的:この関数が解決する問題を文書化します。上述のように、コメントは意図を文書化し、コードは実装を文書化します。
-
パラメータ コメント:各パラメータ コメントには以下を含めます。
-
測定単位
-
期待値範囲
-
「不可能」な値
-
ステータス/エラーのコードの意味
-
-
戻りのコメント:出力変数を文書化するように期待される戻り値を文書化します。関数がこの値を返すことだけを目的としている場合は、重複を避けるため明示的な
@return
コメントは使用しません。これは関数の目的において文書化されています。 -
追加情報:
@warning
、@note
、@see
、@deprecated
は関連情報を文書化するためにオプションで使用できます。他のコメントに続いてそれぞれ独自の行で宣言します。
モダン C++ 言語の構文
Unreal Engine は数多くの C++ コンパイラへ一括して移植するためにビルドされます。サポートを想定するコンパイラと互換性をもつ機能の使用には注意しています。機能が非常に便利なため、それらをマクロにラップし幅広く使用する場合もありますが、通常はサポートを想定するコンパイラがすべて最新標準になるまで待つことになります。
Unreal Engine はデフォルトで C++20 の言語バージョンでコンパイルされ、ビルドには最小バージョンの C++20 が必要です。また、最新のコンパイラ全体で十分にサポートされている多くの最新の言語機能を使用しています。場合によっては、これらの機能の使用法をプリプロセッサの条件文にまとめます。ただし、移植性やその他の理由から、特定の言語機能を完全に回避することを決定する場合があります。
サポートしている最新の C++ コンパイラ機能として以下で指定していない場合で、プリプロセッサ マクロあるいは条件演算子でラップして慎重な使用ができない限りは、コンパイラ固有の言語機能の使用は控えてください。
静的アサート
static_assert
のキーワードはコンパイル時間のアサーションが必要な場合の使用で有効です。
Override と Final
override
と final
のキーワードの使用は有効であり、使用することを強くお勧めします。これらが省略される場合が多くありますが、時間の経過とともに修正されます。
Nullptr
すべての場合において、C-style NULL
マクロの代わりに nullptr
を使うようにします。
唯一の例外は、C++/CX ビルド (Xbox One など) での nullptr
の使用です。この場合、nullptr
の使用は実際には null 参照型で管理されています。型といくつかのテンプレートのインスタンス化のコンテキスト以外は、ネイティブ C++ の nullptr
とほとんど互換性があります。したがって、互換性のためには、より一般的な decltype(nullptr)
ではなく TYPE_OF_NULLPTR
マクロを使用するべきです。
自動
以下の例外に該当しない場合は、C++ コードで auto
を使わないようにします。初期化している型について常に明示的でなければなりません。つまり、読み手がその型を見えるようにしなければなりません。このルールは C# の var
キーワードの使用にも適用されます。
C++20 の構造化バインディング機能も、事実上可変個引数の auto
であるため、使用しないでください。
次のような場合に、auto
の使用が認められます。
-
lambda を変数にバインドする必要がある場合です。lambda 型はコードで表現できないからです。
-
iterator 変数に対して認められます。しかし、iterator の型が非常に詳細で読みづらくなります。
-
テンプレートのコードで認められます。この場合、式の型は簡単に見分けることはできません。これは高度な事例です。
コードの読み手に型がはっきり見えるようにすることは非常に重要です。一部の IDE では型を推測できますが、これはコンパイル可能な状態にあるコードに依存します。merge/diff ツールのユーザーもサポートしません。または、GitHub 上など各ソース ファイルを別個に見る場合などもサポートしません。
認められる方法で auto
を使う場合、型名で使うように常に正しく const
、&
、または *
を使うようにしてください。auto
を使うと、推測された型が希望の型になります。
Range-Based for
コードをわかりやすくし、管理しやすくするにはお勧めです。古い TMap
イテレータを使うコードを移行する場合は、イテレータ型のメソッドであった古い Key()
関数と Value()
関数が、単に基本の TPair
キー値の Key
フィールドと Value
フィールドになっていることに注意してください。
例:
TMap<FString, int32> MyMap;
// 旧スタイル
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// 新スタイル
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
スタンドアローンのイテレータ型も範囲の置き換えがあります。
例:
// 旧スタイル
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// 新スタイル
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Lambda と 匿名関数
Lambda は自由に使用することができます。ベストな lambda は、2、3 個程度の処理文で構成されるものです。特に、大きな式や処理文の一部として使用する場合、たとえば汎用アルゴリズムの述語としての場合にこれが該当します。
例:
// 名前に「Hello」を含む最初のものを検索します
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
// 名前の逆順で配列をソートします
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });|
また、ステートフルな lambda は、頻繁に使用しがちな関数ポインタへの代入ができないことに注意してください。非自明な lambda 関数と匿名関数のドキュメンテーションは、通常の関数と同様に考えてください。
キャプチャと戻り型
自動キャプチャよりも明示的キャプチャにしてください ([&]
および [=]
)。可読性、保全性、パフォーマンスの点において非常に重要です (特に大きな lambda や遅延実行の場合)。
明示的キャプチャは制作者の意図を宣言するため、間違いはコード レビューで発見できます。誤ったキャプチャによって望ましくない結果が生じることがあります。これはコードが長期にわたって維持されると起こる可能性が高くなります。lambda キャプチャについては、次のような追加の注意事項がいくつかあります。
-
ポインタの by-reference キャプチャと by-value キャプチャ (
this
ポインタを含む) は、lambda の実行が遅延されると、間違ったダングリング参照の原因になることがあります。 -
遅延しない lambda に対して必要のないコピーを行うと、by-value キャプチャはパフォーマンスに影響します。
-
間違ってキャプチャした UObject ポインタは、ガベージ コレクターからは見えません。
[=]
は lambda がすべてに対して独自のコピーがあるような印象を与えますが、メンバー変数が参照されている場合、自動キャプチャはthis
を暗示的にキャプチャします。
大きな lambda または別の関数呼び出しの結果を戻している場合は、明示的な戻り型にします。これらは、auto
キーワードと同じように考えます。
強い型付けの列挙型
列挙型 (Enum) クラスは、一般的な列挙型変数と UENUMs
の両方に対して、名前空間が入っている旧式の列挙型変数の置き換えです。たとえば、次のようになります。
// 古い列挙型
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// 新しい列挙型
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
~~~
列挙型は `UPROPERTYs` としてサポートされ、古い `TEnumAsByte<>` ワークアラウンドが置き換えられます。Enum プロパティもバイトだけでなくすべてのサイズに対応します。
~~~
// 古いプロパティ
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// 新しいプロパティ
UPROPERTY()
EThing MyProperty;
ブループリントに公開された列挙型変数は引き続き uint8
を基本にしなければなりません。
Enum クラスをフラグとして使用すると、新しい ENUM_CLASS_FLAGS(EnumType)
マクロを使ってビット演算子をすべて自動的に定義することができます。
enum class EFlags
{
None = 0x00,
Flag1 = 0x01,
Flag2 = 0x02,
Flag3 = 0x04
};
ENUM_CLASS_FLAGS(EFlags)
ひとつの例外として、truth コンテキストでのフラグの使用があります。これは言語上の制約です。代わりにすべての列挙型フラグは、None
という列挙子を持つようにします。この列挙子は比較するために「0」に設定されます。
// 従来の形式
if (Flags & EFlags::Flag1)
// 新しい形式
if ((Flags & EFlags::Flag1) != EFlags::None)
ムーブ セマンティクス
TArray
、TMap
、TSet
、FString
などの主要なコンテナ型はすべて、移動コンストラクタと移動代入演算子が定義されています。これらは、値で型の受け渡しをする際に自動的に使用されることが多くありますが、MoveTemp
(UE の std::move
に匹敵) を使って明示的に呼び出すこともできます。
値でコンテナまたは文字列を返すことは、一時コピーによる通常の負荷を発生させず、表現力においてメリットがあります。値渡し (pass-by-value) および MoveTemp
の使用方法に関する規則は現在も作成中ですが、コードベースが最適化された領域ではすでにあります。
デフォルト メンバー初期化子
デフォルト メンバー初期化子を使って、そのクラス内でクラスのデフォルトを定義することができます。
UCLASS()
class UTeaOptions : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
int32 MaximumNumberOfCupsPerDay = 10;
UPROPERTY()
float CupWidth = 11.5f;
UPROPERTY()
FString TeaType = TEXT("Earl Grey");
UPROPERTY()
EDrinkingStyle DrinkingStyle = EDrinkingStyle::PinkyExtended;
};
このように書くことで以下のメリットがあります。
-
複数のコンストラクタで初期化子を重複する必要がありません。
-
初期化順と宣言順を混ぜることができません。
-
メンバー型、プロパティ フラグ、デフォルト値がすべて 1 つの場所にあるため、可読性と保全性の点から有用です。
ただし、次のような短所もあります。
-
デフォルトが変更された場合、すべての依存ファイルのリビルドが必要です。
-
ヘッダはエンジンのパッチ リリースで変更できないので、この形式は使用可能な修正の種類が限られます。
-
この方法で初期化できないものもあります (基本クラス、
UObject
サブオブジェクト、前方宣言型へのポインタ、コンストラクタ引数から推測された値、複数の段階を踏んで初期化されたメンバー)。 -
一部の初期化子をヘッダに、残りを .cpp ファイルのコンストラクタに配置するので、可読性と保全性が悪くなります。
デフォルト メンバー初期化子の使用については、ご自身で判断してください。 経験則では、デフォルト メンバー初期化子はエンジン コードよりもゲーム コードで合理的です。デフォルト値にコンフィグ ファイルの使用も検討してください。
第三者コード
エンジンで使用しているライブラリにコード変更を反映する際は、「//@UE5 コメント」と変更理由を必ずタグ付けしてください。タグ付けにより、新規ライブラリ バージョンへの変更が容易に反映できます。また、ライセンシーの方々に簡単に変更箇所を知らせることもできます。
エンジンに格納される第三者コードは、簡単に検索できるフォーマットのコメントでマークします。たとえば、次のようになります。
// @third party code - BEGIN PhysX
#include <physx.h>
// @third party code - END PhysX
// @third party code - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/ja-jp/library/xcb2z8hs.aspx]
// デバッガでスレッド名を設定するために使用されます
...
//@third party code - END MSDN SetThreadName
コードのフォーマット
中括弧
中括弧論争はやっかいなものです。Epic では、改行した新しい行に中括弧を付ける方式を長年にわたって使用してきました。引き続きこの方式に従ってください。
単独の処理文のブロックには常に中括弧を含むようにしてください。例:
if (bThing)
{
return;
}
If - Else
if-else 文中の実行ブロックはすべて中括弧で囲んでください。囲むことにより編集ミスを防ぐことができます。中括弧を使用していないと、気付かないうちに if ブロックに行を追加してしまう恐れがあります。行が追加されても if 式の制御対象とならず、問題になります。最悪のケースでは、条件付きでコンパイルされた行によって、if/else 文がブレークしてしまいます。以上の理由から必ず中括弧を使用してください。
if (bHaveUnrealLicense)
{
InsertYourGameHere();
}
else
{
CallMarkRein();
}
多分岐選択のある if 文は、各 else if
が最初の if
と同じインデント位置にくるようにインデントを使用してください。読み手にとってわかりやすい構造となります。
if (TannicAcid < 10)
{
UE_LOG(LogCategory, Log, TEXT("Low Acid"));
}
else if (TannicAcid < 100)
{
UE_LOG(LogCategory, Log, TEXT("Medium Acid"));
}
else
{
UE_LOG(LogCategory, Log, TEXT("High Acid"));
}
タブとインデント
コードのインデント処理の標準を次に示します。
-
実行ブロックでコードをインデントします。
-
行始まりの空白文字は、スペースではなくタブキーを使用します。タブのインデント文字数を 4 文字に設定します。しかし、タブに設定された文字数に関係なく、コードを揃える際などにスペースが必要となる場合もあります。たとえば、タブを使用していない文字行に揃えてコードを整列させたいときです。
-
C# でコードを書いている場合も、スペースではなくタブキーを使用してください。理由は、プログラマーは作業中に C# と C++ 間でコードの切り替えをしばしば行うため、一貫性のあるタブの使用法が必要となります。Visual Studio は C# ファイルにスペースの使用がデフォルトで設定されているので、Unreal Engine のコードで作業する際には、この設定の変更を忘れないでください。
Switch 文
空のケースを除いて (同じコードで書かれた複数のケース)、switch ケース文は、ケースが次のケースへ意図的にフォールスルーすることを明示的に表示してください。つまり、break または「フォールスルー」をするコメントが各ケースにあるようにしてください。その他の制御移行コマンド (return、continue など) を使用しても構いません。
常にデフォルト ケースを持つようにしてください。また、後で他のプログラマーが新規ケースを追加しても対応できるように break を含めてください。
switch (condition)
{
case 1:
...
// フォールスルー
case 2:
...
break;
case 3:
...
return;
case 4:
case 5:
...
break;
default:
break;
}
名前空間
下記のルールに従う限り、名前空間を使用してクラス、関数、変数を適切な場所で管理することができます。使用時は以下のルールに従います。
-
ほとんどの Unreal コードは、グローバル名前空間にラップされていません。
- 特に第三者コードで使用する際など、グローバル スコープとの衝突に気を付けてください。
-
名前空間は、UnrealHeaderTool でサポートされません。
- 名前空間は
UCLASSes
、USTRUCTs
などの定義には使用しないでください。
- 名前空間は
-
UCLASSes
、USTRUCTs
などではない新しい API はUE::
名前空間に配置してください。UE::Audio::
のようにネスティングされている名前空間が理想です。- 外部公開されない API に属さない、実装の詳細を保持するために使用する名前空間は、
Private
名前空間 (UE::Audio::Private::
など) に配置します。
- 外部公開されない API に属さない、実装の詳細を保持するために使用する名前空間は、
-
Using
宣言:- グローバル スコープで
using
宣言を使用しないでください。「.cpp」ファイルも例外ではありません (弊社が使用する「unity」ビルド システムで問題が生じます)。
- グローバル スコープで
-
別の名前空間や関数本体での
using
宣言の使用は問題ありません。 -
名前空間に
using
宣言を使用した場合、同じ翻訳単位内の名前空間の他のオカレンスへ引き継がれることを覚えておいてください。一貫性が保たれている場合は特に問題はありません。 -
上記のルールが守られている場合のみ、
using
宣言をヘッダ ファイル内で安全に使用することができます。 -
前方宣言された型は、それぞれの名前空間内で宣言されなければいけません。
- そうしない場合はリンク エラーとなります。
-
たくさんのクラスと型を名前空間で宣言した場合、これらを他のグローバル スコープにあるクラスで使用することは難しくなります (クラス宣言で使用する場合、関数シグネチャは明示的な名前空間を使用する必要があります)。
-
名前空間内にある特定の変数のみ、
using
宣言を使用してスコープにエイリアスを作成することが可能です。- これには
Foo::FBar
の使用などがありますが、この方法は Unreal コードではあまり使用されません。
- これには
-
マクロは名前空間に存在できません。
- 名前空間ではなく
UE_
のプレフィックスを付けます (例:UE_LOG
)。
- 名前空間ではなく
物理的な依存関係
-
ファイル名には可能な限りプレフィックスは使用しません。
- たとえば、
UScene.cpp
ではなくScene.cpp
とします。これにより、必要なファイルを明確にするために必要な文字数を減らすことで、Workspace Whiz や Visual Assist の Open File in Solution などのツールを使いやすくします。
- たとえば、
-
すべてのヘッダを
#pragma once
ディレクティブを使用して複数の include から保護します。- 使用するすべてのコンパイラは、
#pragma once
をサポートしています。
- 使用するすべてのコンパイラは、
#pragma once
//<ファイルの内容>
-
物理的な結合は最小限にとどめてください。
- 特に、別のヘッダの標準ライブラリ ヘッダをインクルードしないようにしてください。
-
ヘッダをインクルードするより前方宣言を優先してください。
-
ヘッダをインクルードする場合は、できる限り綿密にしてください。
- たとえば
Core.h
をインクルードせず、そこから定義が必要な特定のヘッダ ファイルを Core にインクルードしてください。
- たとえば
-
綿密なインクルードを簡単に行うために、必要なヘッダ ファイルすべてを直接インクルードしてください。
-
インクルードした他のヘッダ ファイルに間接的にインクルードされているヘッダ ファイルには依存しないでください。
-
他のヘッダ ファイルを通じてインクルードされるような依存はしないでください。必要なファイルはすべてインクルードしてください。
-
モジュールには、プライベートとパブリックのソースディレクトリが存在します。
- 他のモジュールが必要とする定義はパブリック ディレクトリのヘッダ ファイルに格納されなければいけません。その他はすべてプライベート ディレクトリに格納してください。古いバージョンの Unreal モジュールでは「Src」と「Inc」と呼ばれていましたが、目的はプライベートとパブリック コードを区別するためで、ソース ファイルとヘッダ ファイルを区別するためではありません。
-
プリコンパイル済みヘッダ生成にヘッダ ファイルを設定することに配慮する必要はありません。
- UnrealBuildTool がうまく対処します。
-
大きな関数を論理的なサブ関数に分けます。
- コンパイラの最適化のひとつのエリアとして、共通部分式の除去があります。関数が大きくなるほど、それらを特定するためにコンパイラが行わなければならない作業が増え、ビルド時間が大幅に長くなります。
-
インラインの関数はよく考えて使用してください。
- それらを使用しないファイルでさえリビルドが強制されてしまいます。インライン化は、トリビアルなアクセサおよびプロファイリングでそれを行うメリットがあるとわかった場合に限り使用してください。
-
FORCEINLINE
の使用についてはさらに注意深く行ってください。- すべてのコードとローカル変数は呼び出している関数に展開され、大きな関数と同じビルド時間の問題が生じます。
カプセル化
protection キーワードでカプセル化を実行します。クラスに対するパブリック / 保護されたインターフェースの一部である場合を除いて、クラス メンバーは private に宣言します。ご自身で最適な判断をしてください。ただし、アクセサがないとプラグインや既存のプロジェクトをブレークせずに後でリファクタリングするのが難しくなることに注意してください。
特定のフィールドが派生クラスによってのみ使用できるようにしたい場合は、private にし、保護されたアクセサを提供します。
クラスが派生元になることを意図していない場合は、final を使用します。
一般的なスタイルの問題
-
プログラミングの依存距離を最小限にします。
- ある特定の値を持つ変数にコードが依存する場合、変数値の設定は値を使用する直前に行います。変数を実行ブロックの先頭で初期化して、この変数が何百行後まで使用されない場合、依存関係が分からずプログラマーが間違って値を変更してしまう可能性があります。次の行に明記することによって、変数が初期化される理由と使用箇所が明確になります。
-
可能な場合はメソッドをサブメソッドへ細分化します。
- 人間は、詳細から全体像を想像するのではなく、全体像を見据えたうえで関心を引く詳細へ掘り下げていくことが得意です。同様に、サブ処理すべてをまとめたコードが書かれているメソッドよりも、適切な名前が付けられたいくつかのサブメソッドを呼び出す単純なメソッドを理解するほうが簡単です。
-
関数宣言または関数呼び出しサイトでは、関数名と引数リストの前に置かれている括弧 () 間にスペース (空白) を挿入しないでください。
-
コンパイラの警告に対処します。
- コンパイラの警告メッセージは、何か問題があることを意味します。メッセージに基づいて問題を解決してください。問題をどうしても解決できない場合、
#pragma
を使用して警告を削除することができます。これは最後の手段として使用してください。
- コンパイラの警告メッセージは、何か問題があることを意味します。メッセージに基づいて問題を解決してください。問題をどうしても解決できない場合、
-
ファイルの最後に空行を残してください。
- gcc がスムーズにコンパイル処理できるように、「.cpp」ファイルと「.h」ファイルはすべてに空行を残してください。
-
デバッグ コードは便利な完成品か、チェックインされていないかのいずれかです。
- デバッグ コードを他のコードと混ぜると、コードの解読が難解になります。
-
文字列リテラルの周囲には
TEXT()
マクロを常に使用してください。TEXT()
マクロがないと、リテラルからFStrings
を構築すると、望ましくない文字列変換プロセスが行われます。
-
ループ内で同じ操作を重複して繰り返さないようにしてください。
- 計算の重複を避けるためにループから共通の部分式をホイストしてください。一部のケースでは、統計を使ってたとえば、文字列リテラルから
FName
を構築するなど関数呼び出しでグローバルに重複する操作を回避します。
- 計算の重複を避けるためにループから共通の部分式をホイストしてください。一部のケースでは、統計を使ってたとえば、文字列リテラルから
-
ホット リロードは注意して行ってください。
- イテレーション時間を短縮するために依存関係を最小限にしてください。リロードで変化しそうな関数に対してインライン化やテンプレートは使用しないでください。リロードしても一定のままであると予測されるものに限り統計を使用してください。
-
複雑な式を簡素化させるため中間変数を使用します。
- 複雑な式が存在する場合、部分式に分けることによって簡単に理解することができます。部分式は、親の式内の部分式の意図を名前で表した中間変数に代入されます。たとえば、次のようになります。
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
上記は以下で置き換えます。
const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff; const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday(); if (bIsLegalWindow && !bIsPlayerDead) { DoSomething(); }
-
ポインタと参照は、それぞれの右側に 1 個だけスペースを空けます。
- これにより、特定の型に対するすべてのポインタや参照に対して Find in Files を迅速に使いやすくなります。たとえば、次のようになります。
//これらを使用します FShaderType* Ptr *//次のものは使用しないでください。* FShaderType *Ptr FShaderType * Ptr
-
シャドウされた変数は認められません。
- C++ では、外部のスコープから変数をシャドウすることは認められており、読み手からは使用が曖昧になります。たとえば、このメンバー関数には、3 つの使用可能な
Count
変数があります。
class FSomeClass { public: void Func(const int32 Count) { for (int32 Count = 0; Count != 10; ++Count) { // 使用数 } } private: int32 Count; }
- C++ では、外部のスコープから変数をシャドウすることは認められており、読み手からは使用が曖昧になります。たとえば、このメンバー関数には、3 つの使用可能な
-
関数呼び出しで匿名のリテラルの使用は避け、
- 意味を説明している名前のついた定数を推奨します。理解するために関数宣言を調べる必要がなくなるので、これによりカジュアル リーダーに対して意図がより明確になります。
// 旧スタイル Trigger(TEXT("Soldier"), 5, true);. // 新スタイル const FName ObjectName = TEXT("Soldier"); const float CooldownInSeconds = 5; const bool bVulnerableDuringCooldown = true; Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
-
ヘッダで重要な静的変数を定義することは避けてください。
- 重要な静的変数により、そのヘッダを含むすべての変換ユニットでインスタンスがコンパイルされます。
// SomeModule.h static const FString GUsefulNamedString = TEXT("String"); //上記は以下で置き換えます。 // SomeModule.h extern SOMEMODULE_API const FString GUsefulNamedString; // SomeModule.cpp const FString GUsefulNamedString = TEXT("String");
API デザイン ガイドライン
-
ブール型関数パラメータは避けてください。
- 特に、関数に渡されるフラグの場合は避けてください。これらは前述した匿名リテラルと同じ問題がありますが、API に挙動が増えて拡張するため、時間経過とともに乗算処理する傾向があります。代わりに、列挙型変数を推奨します (「強い型付けの列挙型」セクションで列挙型変数をフラグとして使用することに関する助言を参照)。
// 旧スタイル FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // 新スタイル enum class ETeaFlags { None, Milk = 0x01, Sugar = 0x02, Honey = 0x04, Lemon = 0x08 }; ENUM_CLASS_FLAGS(ETeaFlags) FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None); FCup* Cup = MakeCupOfTea(Tea, ETeaFlags::Milk | ETeaFlags::Honey);
-
この形式はフラグの誤移植、およびポインタと整数引数からの誤変換を防ぎ、デフォルトを繰り返す必要がなくなるため、より効率的です。
-
セッターのように関数へ渡される完全ステートの場合、
bools
を引数として使用できます (void FWidget::SetEnabled(bool bEnabled)
など)。そうでない場合は、リファクタリングを検討します。 -
関数パラメータ リストは長くなりすぎないようにします。
- 関数が受け取るパラメータが多い場合は、代わりに専用の構造体を渡すことを検討してください。
// 旧スタイル TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // 新スタイル struct FTeaPartyParams { const FTeaFlags* TeaPreferences = nullptr; uint32 NumCupsToMake = 0; FKettle* Kettle = nullptr; ETeaType TeaType = ETeaType::EnglishBreakfast; float BrewingTimeInSeconds = 120.0f; }; TUniquePtr<FCup[]> MakeTeaForParty(const FTeaPartyParams& Params);
-
bool
とFString
による関数のオーバーロードは避けてください。- 予期せぬ挙動が起こる場合があります。
void Func(const FString& String); void Func(bool bBool); Func(TEXT("String")); // ブール オーバーロードを呼び出します!|
-
インターフェース クラスは常に抽象化してください。'
- インターフェース クラスは「I」のプレフィックスを持ち、メンバー変数を持ってはいけません。インターフェースはインラインに実装されている限り、純粋仮想ではないメソッド、また非仮想や静的なメソッドを含むことができます。
-
オーバーライドするメソッドを宣言する場合は、
virtual
およびoverride
のキーワードを使用します。
親クラスの仮想関数をオーバーライドする派生クラスに仮想関数を宣言する場合、virtual
および override
のキーワードを必ず使用します。たとえば、次のようになります。
class A
{
public:
virtual void F() {}
};
class B : public A
{
public:
virtual void F() override;
}
override
キーワードは最近追加されたため、多くの既存コードにはこのキーワードが含まれていません。適宜、override
キーワードを追加してください。
プラットフォーム固有のコード
プラットフォーム固有のコードは、常に適切に名前がつけられたサブディレクトリのプラットフォーム固有のソース ファイルで抽出および実行します。
Engine/Platforms/[PLATFORM]/Source/Runtime/Core/Private/[PLATFORM]PlatformMemory.cpp
通常、PLATFORM_[PLATFORM]
の使用を追加することは避けてください。たとえば、PLATFORM_XBOXONE
の使用を [PLATFORM]
という名前のディレクトリ以外のコードに追加するのは避けてください。代わりに、ハードウェア抽象化レイヤーを拡張して静的関数を追加します (例:FPlatformMisc)。
FORCEINLINE static int32 GetMaxPathLength()
{
return 128;
}
プラットフォームがこの関数をオーバーライドし、プラットフォーム固有の定数値を戻すか、プラットフォーム API を使って結果を決定します。関数に forceinline を使用する場合、パフォーマンス特性は define を使用する場合と同じです。
define がどうしても必要な場合、プラットフォームに適用可能な特別なプロパティを説明する #define
ディレクティブを新しく作成します (例:PLATFORM_USE_PTHREADS
)。Platform.h
にデフォルト値を設定し、プラットフォーム固有の Platform.h
ファイルでそれを要求するすべてのプラットフォームに対してオーバーライドします。
次に Platform.h
の例を示します。
#ifndef PLATFORM_USE_PTHREADS
#define PLATFORM_USE_PTHREADS 1
#endif
WindowsPlatform.h
は次のとおりです。
#define PLATFORM_USE_PTHREADS 0
クロス プラットフォームのコードは、プラットフォームを知らなくても直接 define を使用できます。
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
エンジンのプラットフォーム固有の詳細を一元化することで、詳細全体をプラットフォーム固有のソース ファイルに含むことができます。そうすることにより複数のプラットフォーム上でのエンジンの維持が簡単になり、さらにはコードベースを探し回らずに、プラットフォーム固有の define に対する新しいプラットフォームにコードを移植できるようになります。
プラットフォーム固有のフォルダでのプラットフォーム コードの維持は、PlayStation、Xbox、Nintendo Switch などの NDA プラットフォームの要件です。
[PLATFORM]
サブディレクトリの有無に関係なく、コードがコンパイルおよび実行可能なことを確実にすることが重要です。つまり、クロス プラットフォームのコードはプラットフォーム固有のコードに依存してはいけません。