Epic ではシンプルなコーディング規約をいくつか使用しています。このページでは、検討中のものではなく、現在の Epic Games のコーディング規約を説明します。コーディング規約には必ず従わなければなりません。
コード規則がプログラマーにとって重要な理由は以下のように数多くあります。
-
ソフトウェアのライフタイム コストの 80 %はメンテナンス関連です。
-
ソフトウェアの最初の製作者がライフタイムを通してメンテナンスを継続することはほとんどありません。
-
コード規則によってソフトウェアの可読性が向上し、エンジニアは新しいコードを迅速にしっかりと理解できるようになります。
-
MOD コミュニティのデベロッパーに公開するソース コードを理解しやすく用意することが重要です。
-
クロスコンパイラの互換性を維持するために、こうした規則の多くが実際に必要です。
以下のコーディング規約は C++ が中心となっていますが、どの言語を使用した場合でもこの規約の考え方に従うことが求められます。特定の言語に対して必要な場合には、同等のルールや例外が示されています。
クラスの構成
クラスは、書き手の都合ではなく読み手の立場で構成するべきです。読み手のほとんどがクラスに public なインターフェースを使用するため、まずこれを宣言し、次にクラスの private な実装を行います。
著作権情報
Epic が配布目的で提供するすべてのソース ファイル (.h、.cpp、.xaml など) には、ファイルの最初の行に必ず著作権表示がなされていなくてはなりません。表示フォーマットは下記の例と正確に一致させてください。
// Copyright Epic Games, Inc. All Rights Reserved.
この行の表示がない場合やフォーマットに誤りがある場合、CIS がエラーとなり失敗します。
命名規則
-
すべてのコードおよびコメントは米国英語のスペルとグラマーを使用しなければなりません。
-
名前の最初の文字 (例:型や変数) は大文字とし、通常は文字間にアンダースコアを使用しません。たとえば、
Health
やUPrimitiveComponent
は正しいですが、lastMouseCoordinates
やdelta_coordinates
は使用しません。 - 型名には大文字をプレフィックスとして追加し、変数名と区別します。たとえば
FSkin
は型名で、Skin
はFSkin
のインスタンスとなります。-
テンプレートクラスにはプレフィックス T が付きます。
-
UObject
から継承されるクラスにはプレフィックス U が付きます。 -
AActor
から継承されるクラスにはプレフィックス A が付きます。 -
SWidget
から継承されるクラスにはプレフィックス S が付きます。 -
抽象インターフェースのクラスにはプレフィックス I が付きます。
-
Epic の概念に似たクラス型 (
TModels
型特性の最初の引数として使用される) には、プレフィックス C が付いています。 -
列挙型変数にはプレフィックス E が付きます。
-
ブール変数には、必ずプレフィックス b を付けてください (例:
bPendingDestruction
やbHasFadedIn
)。 -
その他のほとんどのクラスにはプレフィックス F が付きますが、サブシステムによっては別の文字が使用されます。
- Typedef には、その型に対して適切なプレフィックスが付きます。構造体の typedef の場合は F、
UObject
の typedef の場合は U のようになります。-
特定のテンプレートのインスタンス化の typedef は、テンプレートではなくなり、それに応じてプレフィックスが付きます。たとえば、以下のようになります。
typedef TArray<FMytype> FArrayOfMyTypes;
-
-
C# ではプレフィックスは省略されます。
- UnrealHeaderTool では正しい接頭辞が必要な場合が多いため、正しいものを使うことが重要です。
-
-
型のカテゴリが不明であるため、型テンプレート パラメータおよびそれらのテンプレート パラメータに基づく入れ子になった型エイリアスは、上記のプレフィックス規則の対象ではありません。
- 説明的な用語の後にタイプ サフィックスを付けることをお勧めします。
-
In プレフィックスを使用して、テンプレート パラメーターをエイリアスから明確にします。
template <typename InElementType> class TContainer { public: using ElementType = InElementType; };
-
型と変数の名前には名詞を使用します。
-
メソッド名は、その効果を説明する動詞、または効果のないメソッドの戻り値を説明する動詞を使用します。
- マクロ名は、単語をアンダースコアで区切って完全に大文字にし、接頭辞として「UE_」を付ける必要があります (名前空間を参照)。
変数、メソッド、クラス名には明確で記述的な名前を使用します。名前のスコープが大きいほど、名前の明確さがより重要となります。過度に名前を省略しないでください。
変数は 1 つずつ宣言するようにして、変数の意味をコメントとして付けられるようにします。これは JavaDocs のスタイルの要求事項でもあります。変数の前のコメントは 1 行でも複数行でもかまいません。変数をグループ化する空白行の挿入は任意となっています。
bool を返すすべての関数は、true または false の質問形式とします。たとえば、IsVisible()
や ShouldClearBuffer()
です。
プロシージャ (戻り値のない関数) の名前には、明確な動詞の後にオブジェクトが続きます。ただし、メソッドのオブジェクトがそのメソッドが所属するオブジェクト自体である場合は例外です。その場合はコンテキストからオブジェクトが認識されます。「Handle」や「Process」のような動詞は曖昧になるため、使用は避けてください。
必須ではありませんが、参照から渡されたり、関数によって値が書かれる場合は、関数パラメータ名にプレフィックスとして「Out」を付けることを推奨します。こうすることで、引数に渡された値が関数によって置き換えられることが明白になります。
In または Out のパラメータも boolean の場合、In/Out のプレフィックスの前に b を付けます (例: bOutResult
)。
値を返す関数は、戻り値を名前で説明すべきです。関数が返す値を名前によって明確にします。これは特にブール関数で重要です。以下の 2 通りの例を検討してください。
// what does true mean?
bool CheckTea(FTea Tea);
// name makes it clear true means tea is fresh
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 のコードベースで使われていたものの、別の用語に置き換えた方がよいと思われる用語です。
Blacklist
代替表現:deny list、block list、exclude list、avoid list、unapproved list、forbidden list、permission list
Whitelist
代替表現:allow list、include list、trust list、safe list、prefer list、approved list、permission list
Master
代替表現:primary、source、controller、template、reference、main、leader、original、base
Slave
代替表現:secondary、replica、agent、follower、worker、cluster node、locked、linked、synchronized
上記の原則に従ってコードを記述するよう、積極的に取り組んでいます。
移植可能な C++ のコード
-
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 のサイズは想定しない)
C++ の int
型と符号なしの int
型 (サイズはプラットフォームによって変動する場合がありますが、最低 32 ビット幅を保証) の使用は、整数の幅が重要でない場合に認められます。シリアル化またはレプリケートされたフォーマットでは、サイズが明示的に指定された型を使用しなければなりません。
標準ライブラリの使用
これまで 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>
:パフォーマンス上の明らかな利点がある場合は、それぞれ FMemory::Memcpy
と FMemory::Memset
の代わりに memcpy()
と memset()
を使用できます。
標準のコンテナおよび文字列は interop code での場合以外は避けてください。
コメント
コメントはコミュニケーションの手段であり、コミュニケーションは必要不可欠です。コメントを書く際は、以下の点に注意してください (Kernighan & Pike The Practice of Programming から引用)。
ガイドライン
-
自己説明的なコードを書いてください。
// Bad: t = s + l - b; // Good: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
-
役立つコメントを書いてください。
// Bad: // increment Leaves ++Leaves; // Good: // we know there is another tea leaf ++Leaves;
-
悪いコードはコメントでごまかさず、コードを書き直してください。
// Bad: // total number of leaves is sum of // small and large leaves less the // number of leaves that are both t = s + l - b; // Good: TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
-
コードは矛盾しないようにしてください。
// Bad: // never increment Leaves! ++Leaves; // Good: // we know there is another tea leaf ++Leaves;
Const を正しく設定する
const はドキュメンテーションでもあり、コンパイラ ディレクティブでもあります。そのため、すべてのコードで const を正しく設定するようにします。
これには下記が含まれています。
-
引数が関数によって書き換えられない場合に const ポインタや参照によって関数の引数を渡します。
-
オブジェクトを書き換えない場合は const としてメソッドにフラグ付けします。
-
ループがコンテナを書き換えない場合は、コンテナに const のイテレーションを使います。
例:
void SomeMutatingOperation(FThing& OutResult, const TArray<Int32>& InArray)
{
// InArray will not be modified here, but OutResult probably will be
}
void FThing::SomeNonMutatingOperation() const
{
// This code will not modify the FThing it is invoked on
}
TArray<FString> StringArray;
for (const FString& : StringArray)
{
// The body of this loop will not modify StringArray
}
Const は値渡しの関数パラメータやローカルでも推奨されます。Const は変数が関数のボディ内で変更されないことを示すので、読み手が理解しやすくなります。この場合、JavaDoc プロセスに影響を与えることができるので、宣言とその定義が一致するようにします。
例:
void AddSomeThings(const int32 Count);
void AddSomeThings(const int32 Count)
{
const int32 CountPlusOne = Count + 1;
// Neither Count nor CountPlusOne can be changed during the body of the function
}
唯一の例外は最終的にはコンテナへ移動する値渡しのパラメータ (「ムーブ セマンティクス」を参照) ですが、これは稀です。
例:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
(指定先ではなく) ポインタ自体を const にする場合は、最後に const キーワードを入れます。参照を「再代入する」方法はないため、同じ方法で const にすることはできません。
例:
// Const pointer to non-const object - pointer cannot be reassigned, but T can still be modified
T* const Ptr = ...;
// Illegal
T& const Ref = ...;
戻り型で const は絶対に使用しないでください。これは複合型に対するムーブ セマンティクスを禁止し、組み込み型に対してコンパイルの警告をするからです。このルールは戻り型そのものにのみ適用されます。ポインタのターゲット型や戻されている参照には適用されません。
例:
// Bad - returning a const array
const TArray<FString> GetSomeArray();
// Fine - returning a reference to a const array
const TArray<FString>& GetSomeArray();
// Fine - returning a pointer to a const array
const TArray<FString>* GetSomeArray();
// Bad - returning a const pointer to a const array
const TArray<FString>* const GetSomeArray();
フォーマットの例
Epic では JavaDoc に基づいたシステムを使用し、コードから自動的にコメントを抽出してドキュメントを作成します。その際にコメントのフォーマットに関する従うべきルールがいくつかあります。
以下は クラス、メソッド、変数 コメントのフォーマットの実例です。コメントはコードを補強するということを覚えておいてください。コードは実装を文書化します。そしてコメントは意図を文書化します。部分的であってもコードの意図を修正した場合は、必ずコメントも更新してください。
Steep
方式および Sweeten
方式で具体化された 2 通りのパラメータ コメント スタイルがサポートされています。Steep
方式の @param
スタイルが従来のスタイルですが、シンプルな関数に関しては Sweeten 方式に見られるようにパラメータ文書を説明コメントとまとめるとより明確になります。@see
や @return
のような特別コメントのタグは、最初の説明の後で新しい行を開始する場合のみ使用します。
メソッド コメントは、メソッドがパブリックに宣言された場所で一度だけ書いてください。メソッド コメントは、呼び出し元に関連するメソッドのオーバーライドに関する情報など、メソッドの呼び出し元に関連した情報のみを書きます。メソッドの実装と呼び出し元に関係のないメソッドのオーバーライドに関する詳細は、メソッドの実装の中でコメントとして残してください。
/** The interface for drinkable objects. */
class IDrinkable
{
public:
/**
* Called when a player drinks this object.
* @param OutFocusMultiplier - Upon return, will contain a multiplier to apply to the drinker's focus.
* @param OutThirstQuenchingFraction - Upon return, will contain the fraction of the drinker's thirst to quench (0-1).
* @warning Only call this after the drink has been properly prepared.
*/
virtual void Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction) = 0;
};
/** A single cup of tea. */
class FTea : public IDrinkable
{
public:
/**
* Calculate a delta-taste value for the tea given the volume and temperature of water used to steep.
* @param VolumeOfWater - Amount of water used to brew in mL
* @param TemperatureOfWater - Water temperature in Kelvins
* @param OutNewPotency - Tea's potency after steeping starts, from 0.97 to 1.04
* @return The change in intensity of the tea in tea taste units (TTU) per minute
*/
float Steep(
const float VolumeOfWater,
const float TemperatureOfWater,
float& OutNewPotency
);
/** Adds a sweetener to the tea, quantified by the grams of sucrose that would produce the same sweetness. */
void Sweeten(const float EquivalentGramsOfSucrose);
/** The value in yen of tea sold in Japan. */
float GetPrice() const
{
return Price;
}
virtual void Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction) override;
private:
/** Price in Yen */
float Price;
/** Current level of sweet, in equivalent grams of sucrose */
float Sweetness;
};
float FTea::Steep(const float VolumeOfWater, const float TemperatureOfWater, float& OutNewPotency)
{
...
}
void FTea::Sweeten(const float EquivalentGramsOfSucrose)
{
...
}
void FTea::Drink(float& OutFocusMultiplier, float& OutThirstQuenchingFraction)
{
...
}
クラスコメントは何を含みますか?
-
このクラスが解決する問題の説明。
-
このクラスが作成された理由。
複数行のメソッド コメントのそれぞれの部分は何を意味していますか?
-
最初に関数の目的:「この関数が処理する問題」を文書化します。上記でも述べましたが、コメントは
intent
を文書化し、コードはimplementation
を文書化します。 - パラメータ コメント:各パラメータ コメントには以下を含みます。
-
測定単位
-
期待値範囲
-
「不可能」な値
-
ステータス/エラーのコードの意味
-
-
戻りのコメント:出力変数を文書化するように期待される戻り値を文書化します。関数がこの値を返すことだけを目的としている場合は、重複を避けるため明示的な @return コメントは使用しません。これは関数の目的において文書化されています。
- 追加情報:
@warning
、@note
、@see
、@deprecated
は関連情報を文書化するためにオプションで使用できます。他のコメントに続いてそれぞれ独自の行で宣言します。
モダン C++ 言語の構文
Unreal Engine は数多くの C++ コンパイラへ一括して移植するためにビルドされます。サポートを想定するコンパイラと互換性をもつ機能の使用には注意しています。機能が非常に便利なため、それらをマクロにラップし幅広く使用する場合もありますが、通常はサポートを想定するコンパイラがすべて最新標準になるまで待つことになります。
Unreal Engine を構築するには、C ++ 17 の最小言語バージョンが必要であり、最新のコンパイラ全体で十分にサポートされている多くの最新の言語機能を使用しています。場合によっては、これらの機能の使用法をプリプロセッサの条件文にまとめることができます。ただし、移植性やその他の理由から、特定の言語機能を完全に回避することを決定する場合があります。
サポートしている最新の C++ コンパイラ機能として以下で指定していない場合で、プリプロセッサ マクロあるいは条件演算子でラップして慎重な使用ができない限りは、コンパイラ固有の言語機能の使用は控えてください。
static_assert
このキーワードはコンパイル時間のアサーションが必要な場合の使用で有効です。
override と final
こうしたキーワードの使用は有効であり、使用することを強くお勧めします。これらが省略される場合が多くありますが、時間の経過とともに修正されます。
nullptr
すべての場合において、C-style NULL
マクロの代わりに nullptr
を使うようにします。
唯一の例外は、C++/CX ビルド (Xbox One など) nullptr
が実際には null 参照型によって管理されることです。型といくつかのテンプレートのインスタンス化のコンテキスト以外は、ネイティブ C++ の nullptr
とほとんど互換性があります。したがって、互換性のためには、より一般的な decltype(nullptr)
ではなく TYPE_OF_NULLPTR
マクロを使用するべきです。
'auto' キーワード
以下の例外がなければ、C++ コードで auto
を使わないようにします。初期化している型について常に明示的でなければなりません。つまり、読み手がその型を見えるようにしなければなりません。このルールは C# の var
キーワードの使用にも適用されます。
C++17 の構造化バインディング機能も、事実上可変個引数の 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;
// Old style
for (auto It = MyMap.CreateIterator(); It; ++It)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
}
// New style
for (TPair<FString, int32>& Kvp : MyMap)
{
UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), *Kvp.Key, Kvp.Value);
}
スタンドアローンのイテレータ型も範囲の置き換えがあります。
例:
// Old style
for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
{
UProperty* Property = *PropertyIt;
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
// New style
for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
{
UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
}
Lambda と 匿名関数
Lambda は自由に使用することができます。ベストな lambda は、2、3 個程度の処理文で構成されるものです。特に、大きな式や処理文の一部として使用する場合、たとえば汎用アルゴリズムの術語としての場合にこれが該当します。
例:
// Find first Thing whose name contains the word "Hello"
Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });
// Sort array in reverse order of name
Algo::Sort(ArrayOfThings, [](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });
また、ステートフルな lambda は、頻繁に使用しがちな関数ポインタへの代入ができないことに注意してください。
非自明な lambda 関数と匿名関数のドキュメンテーションは、通常の関数と同様に考えてください。コメントを入れるために、必要に応じて数行に分けてください。
自動キャプチャよりも明示的キャプチャにしてください ([&]
および [=]
)。可読性、保全性、パフォーマンスの点において非常に重要です (特に大きな lambda や遅延実行の場合)。オーサーの意図を宣言するので、間違いはコード レビューでより簡単に発見できます。誤ったキャプチャのセマンティクスで変数を間違ってキャプチャすると、望ましくない結果が生じることがあります。これはコードが長期にわたり維持されると起こる可能性が高くなります。
-
ポインタの by-reference キャプチャと by-value キャプチャ (
this
ポインタを含む) は、lambda がキャプチャした変数のコンテキスト外で実行されると間違ったダングリング参照の原因になることがあります。 -
遅延しない lambda に対して必要のないコピーを行うと、by-value キャプチャはパフォーマンスに影響します。
-
間違ってキャプチャした UObject ポインタは、ガベージ コレクターからは見えません。
[=]
は lambda がすべてに対して独自のコピーがあるような印象を与えますが、メンバー変数が参照されている場合、自動キャプチャはthis
を暗示的にキャプチャします。
大きな lambda または別の関数呼び出しの結果を戻している場合は、明示的な戻り型にします。これらは、'auto' キーワードと同じように考えます。これらは、auto
キーワードと同じように考えます。
// Without the return type here, the return type is unclear
auto Lambda = []() -> FMyType
{
return SomeFunc();
}
自動キャプチャと暗黙的な戻り型は、たとえば Sort 呼び出しなどの自明な lambda では認められます。この場合、セマンティクスは明らかであり、明示的であることで過剰に詳細になります。
C++14 のキャプチャ初期化機能を使用できます。
TUniquePtr<FThing> ThingPtr = MakeUnique<FThing>();
AsyncTask([UniquePtr = MoveTemp(UniquePtr)]()
{
// Use UniquePtr here
});
Strongly - Typed Enums
Enum クラスは、一般的な列挙型変数と UENUMs
の両方に対して、名前空間が入っている旧式の列挙型変数と置き換えることが推奨されています。例:
// Old enum
UENUM()
namespace EThing
{
enum Type
{
Thing1,
Thing2
};
}
// New enum
UENUM()
enum class EThing : uint8
{
Thing1,
Thing2
}
これらも UPROPERTYs
でサポートされており、古い TEnumAsByte<>
ワークアラウンドを置き換えます。Enum プロパティもバイトだけでなくすべてのサイズに対応します。
// Old property
UPROPERTY()
TEnumAsByte<EThing::Type> MyProperty;
// New property
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 に設定されます。
// Old
if (Flags & EFlags::Flag1)
// New
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;
};
このように書くことで以下のメリットがあります。
-
複数のコンストラクタで初期化子を重複する必要がありません。
-
初期化順と宣言順を混ぜることができません。
-
メンバー型、プロパティ フラグ、デフォルト値がすべて一つの場所にあるので、可読性と保全性の点から有用です。
ただし、次のような短所もあります。
-
デフォルトが変更された場合、すべての依存ファイルのリビルドが必要です。
-
ヘッダはエンジンのパッチ リリースで変更できないので、この形式は使用可能な修正の種類が限られます。
-
この方法で初期化できないものもあります (基本クラス、
UObject
サブオブジェクト、前方宣言型へのポインタ、コンストラクタ引数から推測された値、複数の段階を踏んで初期化されたメンバー)。 -
一部の初期化子をヘッダに、残りを .cpp ファイルのコンストラクタに配置するので、可読性と保全性が悪くなります。
ご自身で判断して使用してください。経験則では、デフォルト メンバー初期化子はエンジン コードよりもゲーム コードで合理的です。デフォルト値にコンフィグ ファイルの使用も検討してください。
第三者コード
エンジンで使用しているライブラリにコード変更を反映する際は、「//@UE4 コメント」と変更理由を必ずタグ付けしてください。タグ付けにより、新規ライブラリ バージョンへの変更が容易に反映できます。また、ライセンシーの方々に簡単に変更箇所を知らせることもできます。
エンジンに格納される第三者コードは、簡単に検索できるフォーマットのコメントでマークします。例:
// @third party code - BEGIN PhysX
#include <physx.h>
// @third party code - END PhysX
// @third party code - BEGIN MSDN SetThreadName
// [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
// Used to set the thread name in the debugger
...
//@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:
...
// falls through
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::
など) に配置します。 Using
宣言:-
グローバル スコープで、
using
宣言を使用しないでください。「.cpp」ファイルも例外ではありません (弊社が使用する「unity」ビルド システムで問題が生じます)。 -
その他の名前空間や関数本体で
using
を使用した宣言の使用は問題ありません。 -
名前空間に
using
を使用した場合、同じ翻訳単位内の名前空間の他のオカレンスへ引き継がれることを覚えておいてください。一貫性が保たれている場合は特に問題はありません。 -
上記のルールが守られている場合のみ
using
をヘッダ ファイル内で安全に使用することができます。
-
-
前方宣言された型は、それぞれの名前空間内で宣言されなければいけません。そうしないとリンク エラーとなります。
-
たくさんのクラスと型を名前空間で宣言した場合、これらを他のグローバル スコープにあるクラスで使用することは難しくなります (クラス宣言で使用する場合、関数シグネチャは明示的な名前空間を使用する必要があります)。
-
名前空間内にある特定の変数のみ、
using
ディレクティブを使用してスコープにエイリアスを作成することが可能です (例:Foo::FBar
の使用)。この方法は、Unreal コードではあまり使用されません。 - マクロは名前空間に存在できませんが、たとえば
UE_LOG
ではなくUE_
のプレフィックスを付けます。
物理的な依存関係
-
ファイル名には可能な限りプレフィックスは使用しません。たとえば、
UScene.cpp
ではなくScene.cpp
とします。これにより、必要なファイルを明確にするために必要な文字数を減らすことで、ソリューションで Workspace Whiz や Visual Assist の Open File in Solution などのツールを使いやすくします。 -
すべてのヘッダを
#pragma once
ディレクティブを使用して複数の include から保護します。使用する必要があるすべてのコンパイラは、最近は#pragma once
をサポートしています。#pragma once //<file contents>
-
一般的に、物理的な結合は最小限にとどめてください。特に、別のヘッダの標準ライブラリ ヘッダを含まないようにしてください。
-
ヘッダをインクルードする代わりに前方宣言が可能な場合は、その方法を優先してください。
-
できる限り綿密なインクルードをしてください。「Core.h」ファイルをインクルードせずに、そこから定義が必要な特定のヘッダ ファイルを Core にインクルードしてください。
-
綿密なインクルードを簡単に行うために、必要なヘッダ ファイルすべてを直接インクルードしてください。
-
インクルードした他のヘッダ ファイルに間接的にインクルードされているヘッダ ファイルには依存しないでください。
-
他のヘッダ ファイルを通じてインクルードされるような依存はしないでください。必要なファイルはすべてインクルードしてください。
-
モジュールには、プライベートとパブリックのソースディレクトリが存在します。他のモジュールが必要とする定義はパブリック ディレクトリのヘッダ ファイルに格納されなければいけません。その他はすべてプライベート ディレクトリに格納してください。古いバージョンのアンリアル モジュールでは "Src" と "Inc" と呼ばれていましたが、目的はプライベートとパブリック コードを区別するためで、ソース ファイルとヘッダ ファイルを区別するためではありません。
-
プリコンパイル済みヘッダ生成にヘッダ ファイルを設定することに配慮する必要はありません。UnrealBuildTool がうまく対処します。
-
大きな関数を論理的なサブ関数に分けます。コンパイラの最適化のひとつのエリアとして、共通部分式の除去があります。関数が大きくなるほど、それらを特定するためにコンパイラが行わなければならない作業が増え、ビルド時間が大幅に長くなります。
-
インラインの関数はよく考えて使用してください。それらを使用しないファイルでさえリビルドを強制してしまうからです。インライン化は、トリビアルなアクセサおよびプロファイリングでそれを行うメリットがあるとわかった場合に限り使用してください。
-
FORCEINLINE
の使用についてはさらに注意深く行ってください。すべてのコードとローカル変数は呼び出している関数に展開され、大きな関数と同じビルド時間の問題が生じます。
カプセル化
protection キーワードでカプセル化を実行します。クラスに対するパブリック / 保護されたインターフェースの一部である場合を除いて、クラス メンバーは private に宣言します。ご自身で最適な判断をしてください。ただし、アクセサがないとプラグインや既存のプロジェクトをブレークせずに後でリファクタリングするのが難しくなることに注意してください。
特定のフィールドが派生クラスによってのみ使用できるようにしたい場合は、private にし、保護されたアクセサを提供します。
クラスが派生元になることを意図していない場合は、final を使用します。
一般的なスタイルの問題
-
プログラミングの依存距離を最小限にします。ある特定の値を持つ変数にコードが依存する場合、変数値の設定は値を使用する直前に行います。変数を実行ブロックの先頭で初期化して、この変数が何百行後まで使用されない場合、依存関係が分からずプログラマーが間違って値を変更してしまう可能性があります。次の行に明記することによって、変数が初期化される理由と使用箇所が明確になります。
-
可能な場合はメソッドをサブメソッドへ細分化します。人間は、詳細から全体像を想像するのではなく、全体像を見据えたうえで関心を引く詳細へ掘り下げていくことが得意です。同様に、サブ処理すべてをまとめたコードが書かれているメソッドよりも、適切な名前が付けられたいくつかのサブメソッドを呼び出す単純なメソッドを理解するほうが簡単です。
-
関数宣言または関数呼び出しサイトでは、関数名と引数リストの前に置かれている括弧 () 間にスペース (空白) を挿入しないでください。
-
コンパイラの警告に対処します。コンパイラの警告メッセージは、何か問題があることを意味します。メッセージに基づいて問題を解決してください。問題をどうしても解決できない場合、
#pragma
を使用して警告を削除することができます。これは最後の手段として使用してください。 -
ファイルの最後に空行を残してください。gcc がスムーズにコンパイル処理できるように、「.cpp」ファイルと「.h」ファイルはすべてに空行を残してください。
-
デバッグ コードは普通、便利な完成品か、チェックインされていないかのいずれかです。デバッグ コードを他のコードと混ぜるとコードの解読が難解になります。
-
文字列リテラルの周囲には
TEXT()
マクロを常に使用してください。それがないと、リテラルからFStrings
を構築すると、望ましくない文字列変換プロセスが行われます。 -
ループ内で同じ操作を重複して繰り返さないようにしてください。計算の重複を避けるためにループから共通のサブ式をホイストしてください。一部のケースでは、統計を使ってたとえば、文字列リテラルから
FName
を構築するなど関数呼び出しでグローバルに重複する操作を回避します。 -
ホット リロードは注意して行ってください。イテレーション時間を短縮するために依存関係を最小限にしてください。リロードで変化しそうな関数に対してインライン化やテンプレートは使用しないでください。リロードしても一定のままであると予測されるものに限り統計を使用してください。
-
複雑な式を簡素化させるため中間変数を使用します。複雑な式が存在する場合、部分式に分けることによって簡単に理解することができます。部分式は、親の式内の部分式の意図を名前で表した中間変数に代入されます。例:
if ((Blah->BlahP->WindowExists->Etc && Stuff) && !(bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday()))) { DoSomething(); }
should be replaced with
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) { // Use Count } } private: int32 Count; }
-
関数コールで匿名のリテラルの使用は避け、意味を説明している名前のついた定数を推奨します。
// Old style Trigger(TEXT("Soldier"), 5, true);. // New style 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 デザイン ガイドライン
-
bool
関数パラメータは避けてください (特に関数に渡されるフラグの場合)。これらは前述した匿名リテラルと同じ問題がありますが、API に挙動が増えて拡張するため、時間経過とともに乗算処理する傾向があります。代わりに、列挙型変数を推奨します (「Strongly-Typed Enums」セクションで列挙型変数をフラグとして使用することに関する助言を参照)。// Old style FCup* MakeCupOfTea(FTea* Tea, bool bAddSugar = false, bool bAddMilk = false, bool bAddHoney = false, bool bAddLemon = false); FCup* Cup = MakeCupOfTea(Tea, false, true, true); // New style 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)
など)。そうでない場合は、リファクタリングを検討します。
-
関数パラメータ リストは長くなりすぎないようにします。関数が受け取るパラメータが多い場合は、代わりに専用の構造体を渡すことを検討してください。
// Old style TUniquePtr<FCup[]> MakeTeaForParty(const FTeaFlags* TeaPreferences, uint32 NumCupsToMake, FKettle* Kettle, ETeaType TeaType = ETeaType::EnglishBreakfast, float BrewingTimeInSeconds = 120.0f); // New style 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")); // Calls the bool overload!
-
インターフェース クラス (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
Windows/WindowsPlatform.h の場合:
#define PLATFORM_USE_PTHREADS 0
複数のプラットフォーム用コードは、プラットフォームを知らなくても直接 define を使用できます。
#if PLATFORM_USE_PTHREADS
#include "HAL/PThreadRunnableThread.h"
#endif
理由:エンジンのプラットフォーム固有の詳細を一元化することで、その詳細全体をプラットフォーム固有のソース ファイルに含むことができます。コードベースを探し回らずに、複数のプラットフォーム上でのエンジンの維持、およびプラットフォーム固有の define に対する新しいプラットフォームへのコード移植が簡単になります。
プラットフォーム固有のフォルダでのプラットフォーム コードの維持は、PlayStation、XboxOne、Nintendo Switch などの NDA プラットフォームの要件です。
[PLATFORM]
サブディレクトリの有無に関係なく、コードがコンパイルおよび実行可能なことを確実にすることが重要です。つまり、複数のプラットフォーム用コードはプラットフォーム固有のコードに依存してはいけません。