Real Unreal Engine C++ 2017-12 (part-1/5)
0. はじめに
この記事は Unreal Engine 4 (UE4) その2 Advent Calendar 2017 の DAY 1 に寄稿したものです。😃
1. 概要
std 慣れしたプロフェッショナルとしてある程度の経験と知識のあるベテランの C++er でも UE4 フレームワークの C++ ソースコードと対峙する上では、事前に知っておくと混乱の予防接種となるコツは多く、 API Reference や公式の解説も入門向けには整備が善いが、 C++er 向けには十分とは言い難い。
そこで、本記事では一般的なプロフェッショナルレベルの C++er を対象に、2章構成で次の内容を記す。
それなりのボリュームとなるのでここでは先ず章立てのみ記した。より具体的には後の節の「目次」を参照されたい。
背景
今年はお仕事でも C++er から UE4/C++er にクラスチェンジし、ゲームよりも土木分野でのアプリケーション開発に UE4 を転用している伊藤兎@北海道札幌市在住/石川県金沢市勤務です・x・
弊社のお仕事仲間は UE4 とは縁の遠めの生粋の土木マンたち。何人かいるイケメン C++er やエース C++er たちも現在わたしが中心に開発している UE4/C++ のソースを突然読み書きさせると事故を起こしたり混乱したりしそうな気がします。
そこで、今回は "std 標準ライブラリーや C++ 言語で一般的な機能、実装などは把握して使い熟せている C++er 向け" の UE4/C++ 入門 tips を私の現時点での視点から執筆する事にしました。
執筆時点の環境等
- この記事は 2017-12-01 に公開するものです。
- C++ は C++14 を基本とします。
- C++17 に触れる場合は C++17 の内容である旨を明示します。
- C++ の template を意味する表現としてソースコード以外の本文では
template < typename T > X
をX<T>
のように簡略して表記します。また、この簡略表記の際は文意に対して本質的ではない省略可能なテンプレートパラメーターは省略します。 - UE4 は 4.18.0 を暗黙的に基本とします。参考にする場合はバージョン間の差異に十分に注意して下さい。
- UE4 について C++er としては本質的ではない入門的な内容、開発環境のインストール、新規プロジェクトの作成、 C++ クラスの作成などについては UNREAL ENGINE プログラミングガイド プログラマ向けクイックスタート 程度は自習してある前提とします。
誤りの指摘と質疑について
- 本記事の記述への誤りの指摘は Facebook または Twitter でどうぞ。
- それ以外の UE4 に本質的な内容の質疑は UE4 ANSWERHUB へどうぞ。
目次
- プロフェッショナル C++er のための UE4/C++ チュートリアル&予防接種
- UE4/C++ の数値型と std 互換性
- 数値型
- TNumericLimits
と std::numeric_limits
- UE4/C++ の文字・文字列型と std 互換性
- UE4/C++ における Unreal Header Tool の役割と制約
- UE4/C++ における入門的な「メモリー」アロケーションとガベージコレクション
NewObject
とCreateDefaultSubObject
及び C++ のnew
の違いUActorComponent
のRegisterComponent
及びAttachToComponent
またはSetupAttachment
UObject
とメンバー変数におけるUPROPERTY()
UHT マクロのガベージコレクターに対する効果
- UE4/C++ における「ムーブセマンティクス」
MoveTemp
,MoveCopy
とstd::move
Forward
とstd::forward
- UE4/C++ における「型変換」
Cast<T>
,CastChecked<T>
とdynamic_cast<T>
及び RTTI
- UE4/C++ における「アサーション」
- UE4/C++ における「例外処理」の注意点
- ビルドプロファイルとプラットフォームに与える制約
- UE4/C++ における入門的な「数学」ライブラリーの std 互換性と独自性
FMath
における C++ CRT 互換層FMath
における Generic Platform 層FMath
における Platform Native 層FMath
の真価FMath
に含まれる型
- UE4/C++ における入門的な「コンテナー」ライブラリーの std 互換性
TArray<T>
とstd::deque<T>
/std::vector<T>
/std::queue
/std::stack
TMap<K,V>
とstd::unordered_map<K,V>
- その他の UE4 コンテナーライブラリー
- UE4/C++ における入門的な「スレッド」ライブラリーの std 互換性
TPromise<T>
とstd::promise<T>
TFuture<T>
とstd::future<T>
FRunnableThread
とstd::thread
- UE4/C++ における「アトミック」ライブラリーの std 互換性
- 主なメンバー関数の対応
- UE4/C++ における「並列処理」ライブラリーと std / OpenMP / Intel-TBB / Microsoft-PPL
ParallelFor
とfor_each
( C++17 Parallelism )#pragma omp parallel for
/parallel_for
( TBB ) /parallel_for
( PPL )
- UE4/C++ における「ファンクター」ライブラリーと std 互換性
TFunctionRef<T>
とstd::function<T>
- UE4/C++ の数値型と std 互換性
- プロフェッショナル C++er のための UE4/C++ リアル・ベーシック
- UE4/C++ におけるガベージコレクターの操作
- 任意の
UObject*
の参照カウントを確認する - 強制的にガベージコレクターを動作させる
- 任意の
- UE4/C++ における「相互排除と同期」ライブラリーと std 互換性
FCriticalSection
クリティカルセクション型とstd::mutex
FRWLock
読み書きロック型とstd::shared_mutex
- UE4/C++ における基礎的な「HTTP」ライブラリー
FHttpModule
- UE4/C++ における「JSON」ライブラリー
- UE4/C++ における「ファイルシステム」ライブラリー
- エンジンプラグインのリビルド
- エンジンプラグインのリビルド
- UE4/C++ におけるガベージコレクターの操作
本論
1. プロフェッショナル C++er のための UE4/C++ チュートリアル&予防接種
1.1. UE4/C++ の数値型と std 互換性
1.1.1. 数値型
C++11 以降で取り込まれた int8_t
などのライブラリー定義と同様の型の定義が UE4 では int8
のように行われ使用できる。
所属 | 型 | Header |
---|---|---|
std | int8_t , int_fast8_t など |
<cstdint> |
std | size_t , nullptr_t |
<cstddef> |
UE4 | int8 , int32 など |
Runtime/Core/Public/GenericPlatform/GenericPlatform.h |
std と UE4 それぞれで具体的に定義される型の対応関係は以下の通り。
type | std | UE4 |
---|---|---|
unsigned char |
uint8_t |
uint8 CHAR8 |
unsigned short int |
uint16_t |
uint16 CHAR16 |
unsigned int |
uint32_t |
uint32 CHAR32 TYPE_OF_NULL |
unsigned long long |
uint64_t |
uint64 |
signed char |
int8_t |
int8 ANSICHAR |
signed short int |
int16_t |
int16 |
signed int |
int32_t |
int32 |
signed long long |
int64_t |
int64 |
wchar_t |
WIDECHAR |
|
decltype(nullptr) |
nullptr_t |
TYPE_OF_NULLPTR |
整数以外については、言語組み込み型の float
, double
, long double
に対応する UE4 の独自定義は無く、 UE4 でも必要に応じて言語組み込み型をそのまま使う。
UE4 がライブラリー定義する整数型のうち uin8
, int32
それに言語組み込み型の単精度浮動小数点数型の float
は UE4Editor / Blueprint でも使用可能な UPROPERTY()
や UFUNCTION()
で直接使用可能な型として重要となる。 UPROPERTY()
, UFUNCTION()
については §1.3 で触れるので必要に応じて参照されたい。
UE4/C++ でも一般には任意の型を用いて構わないが、整数型については UE4 ライブラリーの定義する型は名前も簡便かつ直感的なため、翻訳単位として UE4 から独立したソースコードを除いては UE4 ライブラリーの定義する整数型のエイリアスを使うと善い。
1.1.2. TNumericLimits と std::numeric_limits
一般的な C++er は少々冗長であっても製品の必要に応じてしばしば <limits>
に定義される std::numeric_limits<T>
を用いる。
UE4 にも対応するライブラリー実装が "一応" ある。
所属 | 型 | Header |
---|---|---|
std | numeric_limits<T> |
<limits> |
UE4 | TNumericLimits<T> |
Runtime/Core/Public/Math/NumericLimits.h |
numeric_limits<T>
については std を使っておけば善い。その理由としては以下を挙げる。
TNumericLimits<T>
にはMin()
,Max()
,Lowest()
の3つのconstexpr
関数しか定義されていない。用途にもよるが C++er が一般に<limits>
を用いる際に必要な多くの機能が不足している。TNumericLimits<T>
の template 特殊化は std 版より少なく、例えばlong double
,bool
,char
,wchar_t
等に汎用的に使用できないため、それらの使用頻度が例えマイナーであったとしても必要となった際にそれらについてだけ std 版を使うのはプロジェクト単位でのソースコードの整合性を欠く可能性がある。
さしあたり、 UE4 版もあるが std 版よりも優先的に使う意味が無い事だけ覚えておくと混乱を避ける意味で善い。
1.2. UE4/C++ の文字・文字列型と std 互換性
1.2.1. UE4 の文字エンコーディング
C++11 以降 C++er は char
, wchar_t
, char16_t
, char32_t
の文字型を用いて製品のプラットフォームに併せて、あるいは言語に併せて高度な文字エンコーディングの対応を行える。
UE4 では文字エンコーディングを UTF-16LE
で扱う。但し 基本多言語面 のコードポイントのみに限定されサロゲートペアの必要な文字は対象としない。
このため、 UE4Editor は新たに C++ クラスを作成するとファイルの文字エンコーディングを UTF-16LE
で生成してくれる。この事は UE4Editor に頼らず独自にシェルから、あるいはお好みのテキストエディターからソースコードファイルを作成する際、特に日本語を扱う可能性がある製品においては注意する必要がある。
UE4 では C++ 標準の wchar_t
= UE4 ライブラリー定義の WIDECHAR
で UTF-16LE
の文字を扱い、 C++ レベルでの互換性のため補助的に char
= UE4 ライブラリー定義の ANSICHAR
との変換もサポートしている。なお、 UE4 では char16_t
を使用していない。
// UE4 文字 wchar_t moji_1 = L'あ'; TCHAR moji_2 = TEXT( 'あ' ); // UE4 生の文字列 wchar_t moji_retsu_1[] = L"あいう\0"; TCHAR moji_retsu_2[] = TEXT( "あいう\0" ); // 一般には Windows SDK で定義される TCHAR / TEXT(X) 相当する定義が UE4 でも提供され、 // 少なくとも UE4 の開発では TCHAR / TEXT(X) は wchar_t / L ## X に展開される。
C++er レベルではこの知識はしばしば UE4 の文字列と他の系での文字列との相互変換が必要な際の基本知識として役立つ。但し、多くの C++er が今時時別な理由も無く生の文字列を扱わずに std::basic_string<T>
を使うように、 UE4 ではそれに対応する FString
を用いる事になる。 FString
については次項を参照されたい。
また、本件についてより詳しくは日本語の丁寧な公式の解説 文字列の取り扱い/文字エンコード も参照されたい。
1.2.2. FString
と std::basic_string<T>
所属 | 型 | Header |
---|---|---|
std | basic_string<T> |
<string> |
UE4 | FString |
Runtime/Core/Public/Containers/UnrealString.h |
C++er が一般的に文字列に対して basic_string<T>
を用いる事に対応するように、UE4 ではそれに当たる文字列型として FString
を用いる。 UE4 を扱うソースコードでは文字列は基本的に FString
で扱うと考えて善い。
UE4 の FString
は内部的には wchar_t
ベースの文字列を扱い、 basic_string<T>
よりもやや高機能に作られている。 wchar_t
(≃ TCHAR
= WIDECHAR
)文字列からの ctor のほか、 char
(= ANSICHAR
) 文字列からの ctor も備えているので UE4 フレームワークの下層に char
, std::string
実装しか文字列出力の無いライブラリーを使用する際も、 "概ね" は、その出力を UE4 フレームワーク側へ持ち込むためにややこしい文字列変換処理を書く必要は無い。
機能 | std::basic_string<T> |
FString |
---|---|---|
ctor( const char* ) |
T=char なら可 |
可能 |
ctor( const wchar_t* ) |
T=wchar_t なら可 |
可能 |
ctor( const char16_t* ) |
T=char16_t なら可 |
|
ctor( const char32_t* ) |
T=char32_t なら可 |
|
整数を文字列化した文字列オブジェクトを得る | std::to_string |
FromInt |
浮動小数点数型を文字列化した文字列オブジェクトを生成する | std::to_string |
SanitizeFloat |
文字を文字列オブジェクトへ変換する | Chr |
|
文字を指定回数繰り返した文字列オブジェクトへ変換する | ChrN |
|
{0} から始まる配置インデックス文字列を置換するフォーマット文字列を生成する |
Format |
|
printf互換のフォーマット文字列を生成する | Printf |
|
文字列オブジェクトの配列を結合した文字列オブジェクトを得る | Join |
|
文字列を挿げ替える | operator= assign |
operator= |
インデックス位置の文字を取得 | operator[] |
operator[] |
True , Yes , On / False , No , Off 文字列から bool 値を得る |
ToBool |
|
整数値をカンマ桁区切り付きで文字列オブジェクト化する | FormatAsNumber |
|
文字列を追加 | operator+= append |
operator+= Append AppendChars |
文字を追加 | operator+= append push_back |
operator+= AppendChar |
整数を文字列化して追加 | std::to_string + operator+= |
AppendInt |
パス区切り文字を考慮した文字列の追加を行う | operator/= PathAppend |
|
文字・文字列を内挿する | insert |
InsertAt |
範囲の文字を削除 | erase |
RemoveAt |
終端から文字列を削除 | RemoveFromStart |
|
先頭から文字列を削除 | RemoveFromEnd |
|
文字列を空にする | clear |
Empty Reset |
タブ文字を空白文字へ置き換える | ConvertTabsToSpaces |
|
順方向イテレーター先頭を取得 | begin |
CreateIterator |
順方向イテレーター終端を得る | end |
|
逆方向イテレーター先頭を得る | rbegin |
|
逆方向イテレーター終端を得る | rend |
|
const 順方向イテレーター先頭を取得 | cbegin |
CreateConstIterator |
const 順方向イテレーター終端を得る | cend |
|
const 逆方向イテレーター先頭を得る | crbegin |
|
const 逆方向イテレーター終端を得る | crend |
|
先頭から文字列を検索 | find |
Find Contains |
終端から文字列を検索 | rfind |
Find |
先頭から文字を検索 | find_first_of |
FindChar |
終端から文字を検索 | find_last_of |
FindLastChar |
先頭から文字を否定検索 | find_first_not_of |
|
終端から文字を否定検索 | find_last_not_of |
|
先頭からファンクターを用いて検索 | FindLastCharByPredicate |
|
ワイルドカード文字(* ,? )を含む文字列に合致するか判定する |
MatchesWildcard |
|
空か判定する | empty |
IsEmpty |
インデックスが範囲内か判定する | IsValidIndex |
|
数字のみからなる文字列を格納しているか判定する | IsNumeric |
|
他の文字列と比較 | compare |
Compare |
文字列が等しいか判定する | operator== |
operator== Equals |
文字列先頭の部分文字列を比較する | StartsWith |
|
文字列終端の部分文字列を比較する | EndsWith |
|
文字列先頭から部分文字列を指定文字数だけ取得する | Left |
|
文字列先頭から部分文字列を{長さ-指定文字数}だけ取得する | LeftChop |
|
文字列終端から部分文字列を指定文字数だけ取得する | Right |
|
文字列終端から部分文字列を{長さ-指定文字数}だけ取得する | RightChop |
|
部分文字列を取得 | substr |
Mid |
部分文字列を文字列で置換した文字列を生成する | Replace |
|
文字列範囲を文字列で置換する | replace |
|
部分文字列を文字列で置換する | (find +replace ) |
ReplaceInline |
先頭に空白文字を追加した文字列オブジェクトを生成する | LeftPad |
|
終端に空白文字を追加した文字列オブジェクトを生成する | RightPad |
|
\\ , \n , /r , \t , \' , \" についてエスケープ処理した文字列を生成する |
ReplaceCharWithEscapedChar |
|
\\ , \n , /r , \t , \' , \" についてエスケープ解除した文字列を生成する |
ReplaceEscapedCharWithChar |
|
ダブルクォート文字 " をエスケープ処理した文字列を生成する |
ReplaceQuotesWithEscapedQuotes |
|
小文字化した文字列オブジェクトを生成する | std::transform + std::tolower |
ToLower |
小文字化する | ToLowerInline |
|
大文字化した文字列オブジェクトを生成する | std::transform + std::toupper |
ToUpper |
大文字化する | ToUpperInline |
|
先頭から空白文字群を削除した文字列オブジェクトを生成する | TrimStart |
|
先頭から空白文字群を削除する | TrimStartInline Trim |
|
終端から空白文字群を削除した文字列オブジェクトを生成する | TrimEnd |
|
終端から空白文字群を削除する | TrimEndInline TrimTrailing |
|
先頭と終端の両方から空白文字群を削除した文字列オブジェクトを生成する | TrimStartAndEnd |
|
先頭と終端の両方から空白文字群を削除する | TrimStartAndEndInline |
|
終端から null 文字 \0 を削除する |
TrimToNullTerminator |
|
部分文字列で分割した左側・右側からなる2つの文字列を取得する | Split |
|
バイト列を10進数で文字列化した文字列オブジェクトを得る | FromBlob |
|
バイト列を16進数で文字列化した文字列オブジェクトを得る | FromHexBlob |
|
10進数ブロブ文字列をバイト列領域へ書き出す | ToBlob |
|
16進数ブロブ文字列をバイト列領域へ書き出す | ToHexBlob |
|
先頭と終端の両方からダブルクォート文字 " を除去した文字列オブジェクトを生成する |
TrimQuotes |
|
逆方向に読み出した文字列を生成する | Reverse |
|
逆方向に読み出した文字列に置換する | ReverseString |
|
ANSI文字配列へシリアライズする | SerializeAsANSICharArray |
|
分割文字列を指定して文字列を文字列の配列に分割する | ParseIntoArray |
|
復帰文字・改行文字群により文字列を文字列の配列に分割する | ParseIntoArrayLines |
|
ホワイトスペースと任意の分割文字群を指定して文字列を文字列の配列に分割する | ParseIntoArrayWS |
|
文字列オブジェクト配列から空の文字列を除去する | CullArray |
|
文字数を取得 | size |
Len |
保持可能な最大文字数を取得する | max_size |
|
文字列の長さを変更する | resize |
|
内部バッファーの容量を得る | capacity |
GetAllocatedSize |
内部バッファーのポインターを得る | data c_str |
operator*() GetCharArray |
内部バッファーの容量を指定する | reserve |
|
内部バッファーを使用中の文字数へ切り詰める | shrink_to_fit |
Shrink |
他のオブジェクトと中身を入れ替える | swap |
|
アロケーターを取得 | get_allocator |
|
生の文字列を対象領域へ複製する | copy |
できるだけ前後で似た機能が並ぶように整理した。 UE4 のライブラリーは一部のメンバーの命名が C++ 標準に対して紛らわしかったり、そもそも命名が下手なものがあり、注意を要したり、似たような関数名で効果を混乱する恐れがあるものがある。
他のコンテナー類にも共通する C++ 標準に対して紛らわしいメンバーの代表格は次の2つ。
命名が下手で関数名から動作の違いがわからないものの例として trim
系について動作を詳しく解説すると次の通り。
TrimStartInline
: 内部バッファーを置き換える。戻り値は無い。TrimStart
: 内部バッファーを置き換えないが内部的に新しい文字列オブジェクトを生成してTrimStartInline
を使っているTrim
: 内部バッファーを置き換え、戻り値としても結果を出力する。実装詳細がTrimStartInline
よりも効率良い。
こうした詳細について API Reference を頼りたいと考えるのは C++er として当然の思考だが、残念ながら UE4 の API Reference の整備ははっきり言ってお粗末な状態で、十分に動作を理解できる解説がある項目の方が少ない。
他の例では、 ReplaceCharWithEscapedChar
の対象となる文字群は一体何なのか、引数を省略した場合はどのような挙動になるのか、そうした重要なドキュメンテーションが欠陥している事が多い。そこで、結局 C++er は実装詳細を確認する事になる。
調べたい内容について API Reference が未整備に見えたなら、あるいは明らかに言葉足らずに見えたなら、速やかに諦めて手元のソースコードを読もう。幸い、ソースコードは比較的綺麗で C++er にとっては比較的簡単に読み解けるコーディングになっている。
UE4 のソースコードは開発環境を整えていれば、例えば Windows ならば Program Files/Epic Games/UE_4.18/Engine/Source/
などバージョンごとのディレクトリーに格納されている。現在プロジェクトで使用中のソースコードを参照する場合はこのローカルファイルを読むのが楽だろう。また、インストールしていないバージョンのソースコードは GitHub の EpicGames / UnrealEngine で読むのが楽だろう。
話が軽くそれかけけたが、ともあれ UE4 の FString
は std::basic_string
に比べ平易に扱いやすく多機能に作られていて便利が良い。また、数値型でも触れた UPROPERTY()
などの指定による UE4Editor / Blueprint で使用可能な文字列型でもある。 std::basic_string
は残念ながら UE4Editor / Blueprint では直接扱えないので、それを用いる場合には必要に応じて FString( my_std_string.c_str() )
のような薄いラッピングが必要になる。
少なくとも std 版文字列にある機能に対して不足を感じる事は無いし、もし、そういった事があれば生の文字列を経由したり、ポインターのレンジを <algorithm>
で処理するなどの対応もできる。特別な理由が無い限り、 UE4 プロジェクトでの文字列型は FString
を使うと考えて良い。
1.2.3. UE4 の文字列型と用途
UE4 の文字列型として前項で FString
を std::basic_string
と比較して紹介した。その他にも UE4 ではおよそ文字列に相当するオブジェクトが用途別にいくつか定義されている。
用途と特徴 | 型 | Header |
---|---|---|
一般的な文字列。C++erが文字列操作を行う場合はおおよそこれを中心として使い、必要なら他の文字列型とは変換して扱う。 | FString |
Runtime/Core/Public/Containers/UnrealString.h |
高速・軽量な参照中心の用途向け。開発・実行中に頻繁に使用されるアセット名などで使われる。大文字小文字を区別しない。 | FName |
Runtime/Core/Public/UObject/NameTypes.h |
エンドユーザーが直接入出力する用途向け。安全のため原則的に const な設計。 | FText |
Runtime/Core/Public/Internationalization/Text.h |
FString の実装詳細でも一部使用される <cstring> 互換・ラッパー層。さしあたりはいちおうあるよ、程度の認識で構わない。 |
FCString |
Runtime/Core/Public/CString.h |
これらについて概要以上の知識が必要になった際には公式ドキュメントの 文字列の取り扱い を読むと善い。 FCString
については API Reference の Runtime/Core/Public/CString.h
から TCString
を辿ると善い。 FCString::Atof
など時折有用な事がある。
1.2.4. FString のワイルドカードマッチと正規表現
C++ 標準には文字列の「ワイルドカード」マッチ機能は無いが、「正規表現」は std::regex
がある。
UE4 で頻繁に用いる文字列型 FString
にはメンバー関数実装のワイルドカードマッチがある。また、 FString
を用いた正規表現を行う FRegexPattern
/ FRegexMatcher
がある。
基本的な扱い方は以下の通り。
// std regex auto example_std_regex( const std::wstring& input ) { std::wregex pattern( L"^hoge.*piyo$" ); std::wcout << L"std regex: input={" << input << L"} match={" << std::boolalpha << std::regex_match( input, result, pattern ) << L'}' << std::endl; } int main() { example_std_regex( L"hoge fuga piyo" ); example_std_regex( L"hoge fuga piyo fuga" ); }
std regex: input={hoge fuga piyo} match={true} std regex: input={hoge fuga piyo fuga} match={false}
同様に UE4 では std::cerr
に替えて UE4 標準のログ出力マクロを使用すると:
// UE4 FString::MatchesWildcard auto example_ue4_wildcard( const FString& input ) { FString pattern( TEXT( "hoge*piyo$" ) ); UE_LOG ( LogTemp, Log , TEXT( "UE4 FString::MatchesWildcard: input={%s} match={%s}" ) , *input , input.MatchesWildcard( pattern ) ? TEXT( "true" ) : TEXT( "false" ) ); }
// UE4 FRegexPattern / FRegexMatcher auto example_ue4_regex( const std::wstring& input ) { FString input; FRegexPattern pattern( TEXT( "^hoge.*piyo$" ) ); FRegexMatcher m( pattern, input ); UE_LOG ( LogTemp, Log , TEXT( "UE4 FString::MatchesWildcard: input={%s} match={%s}" ) , *input , m.FindNext() ? TEXT( "true" ) : TEXT( "false" ) ); }
注意点として、 FString::MatchesWildcard
は API Reference でも警告が表示されているとおり、一般に速度重視の UE4 のライブラリー機能としては例外的に「遅い」実装に留まっている。?
*
式のワイルドカードはパターンの記述が簡単という点では便利だが、特に速度的に不利な要因となりそうな場合には可能な他の合致判定を採用するのが良い。実際、ソースを眺めると速度最適化に努力されていないことがわかるので興味があれば " Runtime/Core/Private/Containers/String.cpp" を覗いてみると良い。
参考として以下に著者環境で同じ UE4 プロジェクト、同じ実行条件で std::chrono::steady_clock::now()
で得られる時刻が開始から 1 sec 未満の間、 while
ループで入力値 L"hoge fuga piyo"
に対してパターン L"?oge * piy?"
または等価な正規表現での合致判定を何回行えるか、アバウトに計測したデータを示す。
出処 | method | #/sec (O0) | #/sec (O2) | vs. (O2) |
---|---|---|---|---|
UE4 | MatchesWildcard |
373,411 | 382,446 | 1.000 |
UE4 | FRegexPattern + FRegexMatcher |
90,041 | 86,302 | 0.226 |
UE4 | static FRegexPattern + FRegexMatcher |
419,414 | 439,156 | 1.148 |
std | std::wregex + std::regex_match |
169,407 | 345,582 | 0.904 |
std | static std::wregex + std::regex_match |
294,024 | 968,259 | 2.532 |
Mehrdad | wildcard |
3,770,052 | 8,557,528 | 22.376 |
Win32 | PathMatchSpec |
960,855 | 1,150,534 | 3.008 |
この表だけ掲載すれば一般的な C++er には関連の実行コスト事情、何をどう使うべきか理解できたと思う。なお、この "アバウトな計測" のソースは次のように実装している。
// FString の構築コストは計測外 FString input( L"hoge fuga piyo" ); FString wildcard_pattern( L"?oge * piy?" ); FString regex_pattern( L".oge .* piy." ); // UE4 FString::MachesWildcard の例 { uint64 count = 0; const auto end = std::chrono::steady_clock::now() + std::chrono::seconds( 1 ); while ( std::chrono::steady_clock::now() < end ) { input.MatchesWildcard( wildcard_pattern ); ++count; } UE_LOG( LogTemp, Log, TEXT( "count(wildcard)={%d}" ), count ); }
同様のアバウトな計測を合致判定コアをそれぞれ次のように変えたものを用意した。
// UE4 Regex
FRegexPattern p( regex_pattern );
FRegexMatcher m( p, input );
m.FindNext();
// UE4 Regex(static) static FRegexPattern p( regex_pattern ); FRegexMatcher m( p, input ); m.FindNext();
// std regex #include <regex> std::wregex p( *regex_pattern ); std::regex_match( *input, p );
// std regex(static) #include <regex> static std::wregex p( *regex_pattern ); std::regex_match( *input, p );
// Mehrdad wildcard // https://stackoverflow.com/questions/3300419/file-name-matching-with-wildcard/12231681#12231681 wildcard( *wildcard_pattern, *input );
// win32 PathMatchSpec #include <Shlwapi.h> #pragma comment(lib, "shlwapi.lib") PathMatchSpec( *input, *wildcard_pattern );
ワイルドカードの合致判定処理くらい、いくらでも誰かが公開してくれたものがあるだろうと stackoverflow を眺めたところ Mehrdad の「どうぞご自由に」とライセンスされたコードが使いやすくバグも無さそうだったので比較に入れてみたら桁違いにさいつよ過ぎた。
ちなみに、本項とは直接関係ないが、表では計測を O0 と O2 で行った事になっている。これは "{your-project-name}.Build.cs" へ次のように明示的に翻訳時の最適化制御するオプションを設定すれば簡単に固定(変更)できる。
// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する // O0 OptimizeCode = CodeOptimization.Never; // O2 OptimizeCode = CodeOptimization.Always;
この設定は他にも Enum があるので必要に応じてエンジンソースの ModuleRules.cs public enum CodeOptimization
を参照すると良い。(残念ながらこの周りの機能はしばしば更新されており、読み物としてのドキュメントでは設定方法やフラグのシンボル名が微妙に違っていたりしてあまり役に立たない。)
1.3. UE4/C++ における Unreal Header Tool の役割と制約
UE4/C++ コードを含むプロジェクトのビルドプロセスで、 C++ ツールチェインの前に UE4 がソースコード群、特にヘッダーファイル群に対して前処理を行うツール。AnswerHub などでは略称の UHT と記載される事もままある。
1.3.1. Unreal Header Tool の役割
- リフレクションの構築 ( ← これが主機能 )
- 補助実装の付与
- 例:
enum class
にビット演算のoperator
群を付与するマクロENUM_CLASS_FLAGS
など。
- 例:
1.3.2. Unreal Header Tool の制約
UHT はユーザーの C++ ソースコードを読んで適切な基底型を作成して継承させるなどして実行時のリフレクション機能を組み込んだりする手前、ある程度の規則に従ってソースコードを書く必要性、野生の C++er にとっては「制約」が生じる。
そこで、本記事の趣旨に従い、一般的な C++er が UHT 対応において戸惑う可能性のある事項を整理する。
- UE4 フレームワークに組み込んで使用可能な
USTRUCT()
マクロを付与したstruct
では- 基底型が
USTRUCT()
マクロ付きのstruct
型に制限される。 - 多重継承が不能となる。
- template型も継承不能となる。
private
protected
継承不能となる。- 型名の先頭1文字が
F
に制限される。 UFUNCTION()
マクロを付与したメンバー関数は定義できない。
- 基底型が
- UE4 フレームワークに組み込んで使用可能な
UCLASS()
マクロを付与したclass
では- 1つ目の基底型は必ず
UObject
の派生型に制限される。- 2つ目以降に定義する基底型は制約を受けず、多重継承、tempalte型の継承も可能。
private
protected
継承不能となる。- 型名の先頭1文字が1つ目の基底型に基づいた
U
またはA
に制限される。
- 1つ目の基底型は必ず
- UE4 フレームワークに組み込んで使用可能な
UENUM()
マクロを付与したenum class
では- 基底型が事実上
uint8
に制限される。(UENUM()
マクロにBlueprintType
を与えなければ制限されないが、ユースケース的にまず無い ) - 型名の先頭1文字が
E
に制限される。
- 基底型が事実上
UCLASS()
のメンバー関数を UE4 フレームワークで扱えるよう指定するUFUNCTION()
マクロではUSTRUCT()
またはUCLASS()
のメンバー変数を UE4 フレームワークで扱えるよう指定するUPROPERTY()
マクロでは
UHT は完全に近い C++ の機能に対する互換性の対応よりも実用的な UE4 での C++ コード親和性の現実的なコストでの必要最小限程度の確保を目的としている様子がある。このため、リフレクションの設計都合ではなくパーサーが貧弱なせいでは、あるいは UE4 エンジン側でそう多くないコストで実装・対応できますよね、と思うような制約も発生するが、慣れるしかない。諦めろ・x・
1.3.3. ENUM のフラグ化とリフレクションの恩恵
C++er にとって C# などの言語を羨ましく感じる事の1つに enum class
の不便がある。 Unreal Header Tool で前処理する UENUM()
ではそんな不便を少しばかり解消できる機能もある。
1つには、 enum class
のビットフラグ化。これについては下記の記事に以前書いたので興味があればそちらを参照して欲しい。
ここでは、もう1つ、リフレクションの恩恵を紹介する。
// definition UENUM() enum class ESomething: uint8 { aaa, bbb, ccc }; // enum class -> string FString to_string( ESomething in ) { UEnum* pe = FindObject< UEnum >( ANY_PACKAGE, L"ESomething" ); check( pe ); return pe->GetNameStringByIndex( (int32)in ) ); } // string -> enum class ESomething to_something( const FString& in ) { UEnum* pe = FindObject< UEnum >( ANY_PACKAGE, L"ESomething" ); check( pe ) auto n = pe->GetValueByName( FName( *in ) ); check( n != INDEX_NONE ) return (ESomething)n; }
諸事情により本記事のボリュームを修正したところ、書くところが無くなったのでここでついでのように触れた。興味があれば API Reference の UEnum を参照すると良い。
1.4. UE4/C++ における入門的な「メモリー」アロケーションとガベージコレクション
一般的な C++er が UE4/C++er にクラスチェンジする場合、往々にして UE4 のガベージコレクションと UE4 オブジェクトの生成について知っておく必要があります。
1.4.1. NewObject
と CreateDefaultSubObject
及び C++ の new
の違い
一般的な C++er は実行時のメモリーアロケーション(ヒープアロケーション)に new
あるいは事情によっては malloc
を使用する。
UE4 ではライブラリー、フレームワークとしてのレベルでガベージコレクションをサポートする都合から、 NewObject<T>
関数、またその亜種とも言える CreateDefaultSubobject<T>
を使用する機会が頻出する。
UE4/C++ においてメモリーアロケーションに使う関数の基本的な選択基準は次の通り:
対象 | 使用箇所 | 関数 |
---|---|---|
UHT の UCLASS() マクロ付きの UObject 派生型 |
UObject 派生型の ctor ではない場所 |
NewObject<T> |
UHT の UCLASS() マクロ付きの UObject 派生型 |
UObject 派生型の ctor |
CreateDefaultSubobject<T> |
その他の型 | どこでも | new |
UE4 プロジェクトの C++ コードでも言語組み込みの new
を使う事はまま有り得る。それについては通常の C++ 一般と同様の状況となる。これについては本記事では特に解説しないが、もしそのような機会がある場合には、 "Public/HAL/UnrealMemory.h" に定義される UE4 版の Malloc
, Realloc
, Free
ベースの低レベルのメモリー管理機能について知っておくと役立つかもしれない。 UE4 の用途では C++ の new
, delete
は仮想関数テーブルやコンストラクター、デストラクターのコストが余分な事もままある点についても留意したい。
NewObject<T>
と CreateDefaultSubobject<T>
について C++er が注意する点は次の通り:
UObject
派生型のctor
は UE4 フレームワークにより実行時にリフレクションの静的なオブジェクトを生成するため特別な扱いを受ける。この都合、UObject
派生型のctor
ではNewObject<T>
は使用できない。UObject
派生型のctor
ではCreateDefaultSubobject<T>
を使用する。- 引数は基本的には "適当な名前" を渡せば良い。例:
CreateDefaultSubobject< UHoge >( TEXT( "hoge" ) )
- 引数は基本的には "適当な名前" を渡せば良い。例:
NewObject<T>
ではオブジェクト管理上の親子関係のため、基本的にはUObject
派生型のthis
を使う。 例:NewObject< UHoge >( this )
NewObject<T>
CreateDefaultSubobject<T>
で生成しただけでは UE4 フレームワークのガベージコレクションの管理対象とは「ならない」。- §1.4.3 を参照。
UObject
以外の型全般についてはヒープアロケーションが必要な場合は- 基本的に
new
を行う。 NewObject<T>
やCreateDefaultSubobject<T>
は使用できない。(これらは UHT で正常に前処理されたUObject
派生型にしか使えない。)
- 基本的に
参考
1.4.2. UActorComponent
の RegisterComponent
及び AttachToComponent
または SetupAttachment
UE4 で C++er が NewObject<T>
, CreateDefaultSubobject<T>
する機会が多い型に UObject
派生型の UActorComponent
型の派生型 UStaticMeshComponent
, URadialForceComponent
, UAudioComponent
のような「コンポーネント派生型」がある。本稿とは直接は関係無いが C++er が UE4/C++er になるにあたり誤解しかねない点があるので、 NewObject<T>
, CreateDefaultSubobject<T>
と併せて補足する。
多くの場合、 NewObject<T>
したコンポーネントのオブジェクトは RegisterComponent
を直後に行う。 RegisterComponent
について C++er が理解しておくと良い注意点は次の通り。
RegisterComponent
はガベージコレクションとは関係ない。RegisterComponent
はUActorComponent
によるコンポーネントの親子関係の紐付けのためにある。RegisterComponent
しないとコンポーネントの親子関係に基づく多くの機能の対象とならなず、メッシュの表示なども行われない。
CreateDefaultSubobject<T>
はRegisterComponent
の機能も内包しているのでCreateDefaultSubobject<T>
する場合は "たいていは"RegisterComponent
する必要は無い。
主に言いたかった事は始めに挙げた「 RegisterComponent
はガベージコレクションとは関係ない。」です。 NewObject<T>
, CreateDefaultSubobject<T>
で生成したオブジェクトとガベージコレクションは次項で触れます。
- 参考
1.4.3. UObject
とメンバー変数における UPROPERTY()
UHT マクロのガベージコレクターに対する効果
UE4 のガベージコレクターがどのように管理対象のオブジェクトの参照カウントを管理するか、この答えには UE4 のリフレクション、 UHT のマクロの解説と注意点を示す必要がありました。本項でようやく、 UE4 の UObject
派生型のオブジェクトがどのように UE4 のガベージコレクションで管理されるかを C++er 向けに記述します。
- UHT で
UPROPERTY()
マクロを定義したメンバー変数は実行中にガベージコレクションの監視対象に入る。UPROPERTY()
付きのメンバー変数であればTVector<T>
,TMap<K,V>
等の UE4 が特別にサポートするテンプレートコンテナー型を介しても監視対象に入る。
UObjectBaseUtility::AddToRoot
によりガベージコレクションの管理対象へ明示的に手管理で入れる事も "いちおう" できる。- 但し、通常一般的にこの手段を用いる事は無い。
つまり、 UE4 のガベージコレクションに NewObject<T>
, CreateDefaultSubobject<T>
したオブジェクトを加える為には、多くの場合にはその生成を行ったクラスに UPROPERTY()
マクロ付きで定義したメンバー変数を用意しておき、生成後にそのメンバー変数へ代入する必要がある。
UE4 のガベージコレクターはデフォルトの設定では 60 秒毎に全てのオブジェクトの参照カウントを UE4 の UObject
派生型とリフレクションに基づいてカウントし、参照カウントが無いオブジェクトについて破棄を行う。
さしあたり §1 チュートリアル&予防接種 としてはこの程度の解説に留める。より詳細なガベージコレクターの挙動や管理については §2.1 を参照されたい。
1.5. UE4/C++ における「ムーブセマンティクス」
C++er によって C++11 以降は既に常識となった機能の1つに右辺値参照とムーブセマンティクスがあある。
UE4 では std の move
に対応する MoveTemp
がライブラリー機能として用意されているが、 std::move
とは挙動が異なるため C++er が UE4/C++er にクラスチェンジするために必要な知識について触れる。
1.5.1. MoveTemp
, MoveCopy
と std::move
所属 | 関数 | Header |
---|---|---|
std | move |
<memory> |
std | forward |
<memory> |
UE4 | MoveTemp |
Runtime/Core/Public/Templates/UnrealTemplate.h |
UE4 | CopyTemp |
Runtime/Core/Public/Templates/UnrealTemplate.h |
UE4 | Forward |
Runtime/Core/Public/Templates/UnrealTemplate.h |
まず、 std::move
と MoveTemp
の挙動の違いについて触れる。
std::move
を用いた場合:- なんでも rvalue-reference にして返してくれる。
MoveTemp
を用いた場合:- lvalue-reference が入ると
static_assert
して教えてくれる。 - const が入ると
static_assert
して教えてくれる。
- lvalue-reference が入ると
何れも基本的な機能は同等だが、 MoveTemp
では static_assert
により落ちるケースが定義されている。注意深く必要な右辺値参照化をよく理解して使用できる一般的な C++er にとってはどうと言うことはないだろう。
UE4 では MoveTemp
の他に CopyTemp
も提供されている。 CopyTemp
は l-value-reference に対する挙動が MoveTemp
とは異なる。把握しやすいよう std::move
, MoveTemp
, CopyTemp
の挙動の違いを以下に整理する。
function | rvalue-reference | lvalue-reference | const lvalue-reference |
---|---|---|---|
std::move |
rvalue-reference | rvalue-reference | const lvalue-reference |
MoveTemp |
rvalue-reference | static_assert |
static_assert |
CopyTemp |
rvalue-reference | const lvalue-reference | const lvalue-reference |
なお、実際問題としては std 版のムーブセマンティクスを用いても UE4/C++ として問題が生じる事はない。
1.5.2. Forward
と std::forward
Forward
は std::forward
の互換実装である。MoveTemp
, MoveCopy
とは違い、それ以上でもそれ以下でも無い。違わない事を知る事も大切と考え項を設けた。
1.6. UE4/C++ における「型変換」
C++er は必要に応じて C++ スタイルのキャスト static_cast<T>
など、あるいはポリシーによっては冗長な記述の必要ない単純なケースでは C スタイルのキャスト (T)
を用いて、静的型付けの恩恵を受けながらマシンネイティブな言語としての C++ においてオブジェクトのセマンティクスをプログラマーが意図通りに操作しバイナリーレベルでの論理的で高度なプログラミングを施… C++ の一般論はこのくらいにしておこう・x・
1.6.1. Cast<T>
, CastChecked<T>
と dynamic_cast<T>
及び RTTI
UE4 にはライブラリーレベルのキャスト実装として Cast<T>
が提供されている。
Cast<T>
は dynamic_cast<T>
の UE4 版と考えると C++er にとっては理解が速い。しかし、 Cast<T>
についてはムーブセマンティクスのように C++ レベルで提供される機能と UE4 で提供される機能の何れを使用しても構わないわけでは「ない」ので注意が必要である。
先ず、 UE4 では dynamic_cast<T>
は事実上使えない。デフォルトで RTTI は無効化されるからだ。一般論としては悪役にされる事もままあるダウンキャストを前提とした設計も合理的な理由がある場合、特にゲーム用途のライブラリーでは有用な事もある。 UE4 でもしばしばダウンキャストは有用に用いられるが、 RTTI を前提とした dynamic_cast<T>
は実行時コストのオーバーヘッドが UE4 の目指す低レイテンシーなフレームワークの実装と相容れないため、 UE4 では RTTI を無効化しつつも独自のリフレクションに基づいた高速で安全なダウンキャスト機能として Cast<T>
を提供している。
ダウンキャストについて C スタイル、 C++ スタイル、 UE4 の 3 つについて整理する。
出処 | 実装 | 入力ポインターの制限 | 使用可能な条件 | 効果 | 失敗時の挙動 |
---|---|---|---|---|---|
C | (T) |
任意のオブジェクト | 常に使用可能 | メモリー構造の実体によらず常にキャスト | (常に失敗しない) |
C++ | dynamic_cast<T> |
任意のオブジェクト | RTTIが有効 | RTTIにより安全な場合にのみキャスト | nullptr 相当の値が返る |
UE4 | Cast<T> |
UObject 派生オブジェクト |
UE4を使用 | UE4のリフレクションにより安全な場合のみキャスト | nullptr 相当の値が返る |
UE4 | CastChecke<T> |
UObject 派生オブジェクト |
UE4を使用 | UE4のリフレクションにより安全な場合のみキャスト | UE_LOG への Fatal ログ出力により停止 |
UE4 の Cast<T>
は UObject
派生型の型安全なダウンキャストに配慮した実装になっている。 UObject
に仕込まれた GetInterfaceAddress
/ StaticClass
から型安全なダウンキャストを判断し、安全では無い場合には nullptr
を返す。
Cast<T>
にはエラーログ機能付き版の CastChecked<T>
もあり、 DO_CHECK
CPPマクロが有効な場合には入力または出力が nullptr
あるいは bool
への暗黙的なキャスト結果が fasle
となるような場合には Fatal
レベルのエラーログ UE4 のログ機能を通してを出力しつつ停止する。
ほかに、入力が nullptr
ではなく入力の GetClass
と出力型の StaticClass
が operator==
的に一致する場合にのみキャストを行い、失敗した場合は nullptr
を出力する ExactCast
も使用できる。
以下に UE4 Cast<T>
/ CastChecked<T>
, C++ RTTI dynamic_cast<T>
, C (T)
についてダウンキャストを UE4 プロジェクトで速度的に評価した結果を整理する。
出処 | method | #/sec (O0) | #/sec (O2) | vs. (O2) | 安全性 |
---|---|---|---|---|---|
UE4 | Cast<T> |
16,984,356 | 23,152,004 | 1.030 | Bad |
UE4 | Cast<T> + if |
16,269,809 | 22,481,783 | 1.000 | Good |
UE4 | CastChecked<T> |
16,446,959 | 23,379,950 | 1.040 | Good |
C++ | std::dynamic_cast<T>( x ) |
15,470,261 | 20,449,124 | 0.910 | Bad |
C++ | std::dynamic_cast<T>( x ) + if |
15,470,788 | 20,670,679 | 0.919 | Good |
C | (T) |
17,233,758 | 24,727,323 | 1.100 | Worst |
C | (T) + marking + if |
17,729,100 | 24,630,186 | 1.096 | No-bad |
それぞれこのあと明示する実装をコアとして std:chrono::steady_clock
で 1 秒間にダウンキャストとメンバー変数のインクリメントを処理できた回数を評価した。安全性について Bad としたものはそもそも安全なダウンキャストシステムの使い方として間違いだが表では単純な処理性能の指標として "いちおう" 載せた。通常は Good とした何れかを用いる。
単純な C スタイルのキャストでは安全なダウンキャストはできないため、少なくともそのままでは安全なダウンキャスト用途には使用できず、安全性としては Worst である。但し、表の最後に設けた、(T)
+ marking + if
のように簡単な仕込みを基底型、派生型に施せば実用上の実際問題としては十分に実用に耐える高速な型 "やや" 安全なダウンキャストシステムを作る事はできる。キャスト対象のオブジェクトが基底型か派生型であれば意図通りに動作するが実装上はそれ以外の何かのアドレスを渡してもキャストできるし、偶然そのオブジェクトが基底型で定義した mark
と同じアドレスに mark_type
の値とバイナリー的に同等の値を読み出せる場合(たいていは何かしら読み出せてしまうだろう)は危険であり、安全性について Good とは言い難いため、 No-bad という微妙な表現に留めた。基本的にはこれも比較用の参考値として見て欲しい。
// .h // Cスタイル用の細工用 enum class mark_type: uint8 { unknown, x, y }; // 基底型 UCLASS() class UX: public UObject { GENERATED_BODY() protected: mark_type mark = mark_type::x; public: auto is_mark( const mark_type m ) { return mark == m; } }; // 派生型 UCLASS() class UY: public UX { GENERATED_BODY() public: UY() { mark = mark_type::y; } uint64 counter = 0; };
// .cpp; 実際には項目ごと個別にコードするが便宜上続けて並べて記述する。 // UE4 Cast<T>; 成否判定しなければ実用性はないが参考値として。 ++(Cast< UY >( its_y )->counter); // UE4 Cast<T> + if if ( auto y = Cast< UY >( its_y ) ) ++y->counter; // UE4 CastChecked<T> ++(CastChecked< UY >( its_y )->counter); // C++ RTTI dynamic_cast<T>; 成否判定しなければ実用性はないが参考値として。 ++(dynamic_cast< UY* >( its_y )->counter); // C++ RTTI dynamic_cast<T> + if if ( auto y = dynamic_cast< UY* >( its_y ) ) ++y->counter; // C (T); 成否判定しなければ実用性はないが参考値として。 ++(((UY*)its_y)->counter); // C (T) + mark + if auto y = (UY*)its_y; if ( y->is_mark( mark_type::y ) ) ++y->counter;
なお、 C++er が追加的に知っておく知識がある。以下のコードは一見すると UE4 でも C++ の RTTI が「なぜか」有効で dynamic_cast
が機能するかのように振る舞う。
UObject* x = NewObject< USceneComponent >( this ); USceneComponent* y = dynamic_cast< USceneComponent* >( x );
しかし、これは C++ の RTTI による dynamic_cast
ではない。 "Runtime/CoreUObject/Public/Templates/Casts.h" の末尾に以下の定義がある。
#define dynamic_cast UE4Casts_Private::DynamicCast
一般的な C++er ならば察するように、つまり Cast<T>
と同等の機能に置き換えられるよう仕込まれている。もし、本当に RTTI による dynamic_cast<T>
を使いたければ、次のようにコードする必要がある。(もっとも、それが必要とは思わないが本記事の趣旨として "いちおう" 紹介する。)
// C++98 言語規格的に #ifdef __cpp_rtti だけで済ませたいが… // https://connect.microsoft.com/VisualStudio/feedback/details/3144244 #if defined( __cpp_rtti ) || defined( _CPPRTTI ) #ifdef dynamic_cast #undef dynamic_cast #endif auto delivered_object_ptr = dynamic_cast< delivered_type* >( base_object_ptr ); #endif
C++ ソースコードとしては上記の様に #undef dynamic_cast
すれば RTTI による C++ 本来の dynami_cast<T>
が UE4/C++ でも使用可能となる。加えて、 UE4 プロジェクトの "{your-project-name}.Build.cs" へ次のように明示的に RTTI を使用するコンパイルモードの設定を行えば実際に RTTI が有効な UE4/C++ コードのコンパイルが可能となる。
// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する bUseRTTI = true;
このフラグの定義については ModuleRules.cs bUseRTTI
を参照すると良い。例によってしばしばシンボル名が変更されているためソースで確認した方が速い。
UE4/C++ で RTTI を有効化したい状況には次のような事情が考えられる。
- 使用したいライブラリーが RTTI を前提に書かれている。
UObject
派生型以外の型についてデバッグ目的のロギング等でtypeid
を使用したい。UObject
派生型以外のユーザー定義型に対してdynamic_cast
を使用した設計のコードを記述したい。
本稿では知識としての紹介に留め、これ以上の具体的なユースケースやポリシーについては言及しないが事情に対して現実的な解決策として "必要" ならば使用せざるを得まい。
- 参考
- Cast | Unreal Engine
- CastChecked | Unreal Engine
- SD-6: SG10 Feature Test Recommendations : Standard C++
- Templates | Unreal Engine
- Cast | Unreal Engine
- CastChecked | Unreal Engine
- ExactCast | Unreal Engine
- https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Programs/UnrealBuildTool/System/RulesCompiler.cs
1.7. UE4/C++ における「アサーション」
本節ではアサーションと例外によるエラーハンドリングについて std と UE4 の共通点、相違点を明らかとする。
1.7.1. UE4 アサーション各種と static_assert
/ assert
(<cassert>
) の挙動
UE4 ではライブラリーレベルのアサーションが比較的充実している。 std との対応も含めて紹介する。
所属 | macro/keyword | header |
---|---|---|
C++ | static_assert |
(keyword) |
C++ | assert |
<cassert> |
UE4 | check checkf checkCode checkNoReentry checkNoRecursion unimplemented verify verifyf checkSlow checkSlowf verifySlow verifySlowf ensure ensureMgsf |
Runtime/Core/Public/Misc/AssertionMacros.h |
C++er は例外に頼らないエラーハンドリングとしてしばしば簡単には int
値や enum class
によるエラーコードを用いる方法や、 <cassert>
によるアサーション、 C++11 から導入された static_assert
を開発中に仕込む事でリリース前に設計や実装のミスによる不慮の動作を潰す手段を十分に知っている。
UE4 では C++ で一般的な手法に加えて、 UE4 独自の比較的豊富なアサーション機能が使用可能となる。以下に UE4 で使用可能なアサーションを C++ 標準機能を含めて整理する。
所属 | macro/keyword | 評価 | 有効化条件 | 無効時の挙動 | 特徴・停止条件など |
---|---|---|---|---|---|
C++ | static_assert |
翻訳時 | 常に有効 | 条件式が false で停止する | |
std | assert |
実行時 | #ifndef NDEBUG | 何もしない | 条件式が false で停止する |
UE4 | check |
実行時 | #ifdef DO_CHECK | 何もしない | 条件式が false で停止する |
UE4 | checkf |
実行時 | #ifdef DO_CHECK | 何もしない | 条件式が false で停止し、 条件式に続く引数をprintf的に表示する |
UE4 | checkCode |
実行時 | #ifdef DO_CHECK | 何もしない | do{...;}while(false); に展開される |
UE4 | checkNoReentry |
実行時 | #ifdef DO_CHECK | 何もしない | 複数回の実行で停止する |
UE4 | checkNoRecursion |
実行時 | #ifdef DO_CHECK | 何もしない | 複数回の実行で停止する |
UE4 | unimplemented |
実行時 | #ifdef DO_CHECK | 何もしない | 到達で停止する |
UE4 | verify |
実行時 | #ifdef DO_CHECK | 条件式を単純に実行 | 条件式が false で停止する |
UE4 | verifyf |
実行時 | #ifdef DO_CHECK | 条件式を単純に実行 | 条件式が false で停止し、 条件式に続く引数をprintf的に表示する |
UE4 | checkSlow |
実行時 | #ifdef DO_GUARD_SLOW | 何もしない | 条件式が false で停止する |
UE4 | checkSlowf |
実行時 | #ifdef DO_GUARD_SLOW | 何もしない | 条件式が false で停止し、 条件式に続く引数をprintf的に表示する |
UE4 | verifySlow |
実行時 | #ifdef DO_GUARD_SLOW | 条件式を単純に実行 | 条件式が false で停止する |
UE4 | verifySlowf |
実行時 | #ifdef DO_GUARD_SLOW | 条件式を単純に実行 | 条件式が false で停止し、 条件式に続く引数をprintf的に表示する |
UE4 | ensure |
実行時 | #ifdef DO_CHECK | 何もしない | 条件式が false でコールスタックを生成 |
UE4 | ensureMsgf |
実行時 | #ifdef DO_CHECK | 何もしない | 条件式が false でコールスタックを生成し、 条件式に続く引数をprintf的に表示する |
列挙すると多いが、基本的には static_assert
, check
, verify
の 3 パターン、準じて unimplemented
, checkNoReentry
, checkNoRecursion
を使用する事になる。
具体的な実装イメージは以下の仮想コードの様になる。
UCLASS( Blueprintable, BlueprintType ) class UMyBaseComponent : USceneComponent { GENERATED_BODY() public: virtual void InitializeHoge() { checkNoReentry(); verify( xyz = NewObject< UHogeComponent >( this ) ) } int8 CalcSquarePlusX( const int8 value ) const { constexpr auto int8_max = std::numeric_limits< int8 >::max(); checkSlow( value < std::sqrt( int8_max ) ) auto square = value * value; checkSlow( (int32)square + (int32)x < int8_max ) return verifySlow( value * value + x ); } virtual void Something() { unimplemented(); } UPROPERTY( EditAnywhere ) UHogeComponent* hoge = nullptr; int8 x = 1; };
- 参考
1.8. UE4/C++ における「例外処理」の注意点
1.8.1. ビルドプロファイルとプラットフォームに与える制約
C++er は一般的にツールチェイン、対象プラットフォームにおける例外(SJLJ、DW4、SEHなど)について安全性やコストを吟味し、開発するアプリケーションに適した例外の使用方法を決め、アプリケーションの実装へ適用する。
UE4 を扱おうとする C++er 各位には残念な事実を受け入れて頂く必要がある。 UE4 では原則的には例外処理を使わない。なお「使えない」わけでは「ない」。
UE4 ではパフォーマンスとクロスプラットフォーム対応のため原則的には例外を用いないようにコードを設計する。特に正常系や致命的でないエラーの対応でも通過しうるような例外や制御構造の代用となるような例外の使い方は避ける。(もっとも、まともな C++er はそのような使い方はジョーク以外ではしないだろうが。)
例えば throw
を使用したソースコードは Win64 向けにはビルドできても、 Android 向けにビルドしようとすると、以下のエラーで失敗し、 UE4 がデフォルトでは Android 向けの例外処理を無効化している事がわかる。
error: cannot use 'throw' with exceptions disabled
なお、諸事情により UE4 がデフォルトで例外処理を無効化している処理系に対して例外処理を有効化したビルドを行いたい場合、 §6.1. と同様の手法でビルド時の例外処理を有効化できる。 "{your-project-name.Build.cs}" に次のように記述する:
// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する bEnableExceptions = true;
この定義については ModuleRules.cs bEnableExceptions
を参照すると良い。これもしばしばシンボル名が変更されているためソースで確認すると良い。
これで "デプロイ対象の処理系が対応しているならば" 例外処理を有効にしたビルドを生成できるように "いちおう" なる。プラットフォームによっては実行上のデメリットがあったり、ビルドがプロジェクトの例外有効化設定だけではビルド完了できないなど悩まされる事になろうが "いちおう" プロジェクト単位での例外処理はこれで有効化できる。
少なくともデスクトップ向け専用でクロスプラットフォーム展開を絶対にしないという強い確信、信念を持てないプロジェクトでは C++ の例外処理を用いないエラーハンドリング、例外処理を正常系でも通過し得るライブラリーの使用は回避するつもりで UE4/C++er へクラスチェンジする必要がある。
(はてなブログの記事あたりの容量制限のため続き §1.9. 以降は次の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-2/5) - C++ ときどき ごはん、わりとてぃーぶれいく☆)