C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

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章構成で次の内容を記す。

  1. プロフェッショナル C++er のための UE4/C++ チュートリアル&予防接種
  2. プロフェッショナル C++er のための UE4/C++ リアル・ベーシック

それなりのボリュームとなるのでここでは先ず章立てのみ記した。より具体的には後の節の「目次」を参照されたい。

背景

今年はお仕事でも C++er から UE4/C++er にクラスチェンジし、ゲームよりも土木分野でのアプリケーション開発に UE4 を転用している伊藤兎@北海道札幌市在住/石川県金沢市勤務です・x・

弊社のお仕事仲間は UE4 とは縁の遠めの生粋の土木マンたち。何人かいるイケメン C++er やエース C++er たちも現在わたしが中心に開発している UE4/C++ のソースを突然読み書きさせると事故を起こしたり混乱したりしそうな気がします。

そこで、今回は "std 標準ライブラリーや C++ 言語で一般的な機能、実装などは把握して使い熟せている C++er 向け" の UE4/C++ 入門 tips を私の現時点での視点から執筆する事にしました。

執筆時点の環境等

  1. この記事は 2017-12-01 に公開するものです。
  2. C++C++14 を基本とします。
  3. C++17 に触れる場合は C++17 の内容である旨を明示します。
  4. C++ の template を意味する表現としてソースコード以外の本文では template < typename T > XX<T> のように簡略して表記します。また、この簡略表記の際は文意に対して本質的ではない省略可能なテンプレートパラメーターは省略します。
  5. UE4 は 4.18.0 を暗黙的に基本とします。参考にする場合はバージョン間の差異に十分に注意して下さい。
  6. UE4 について C++er としては本質的ではない入門的な内容、開発環境のインストール、新規プロジェクトの作成、 C++ クラスの作成などについては UNREAL ENGINE プログラミングガイド プログラマ向けクイックスタート 程度は自習してある前提とします。

誤りの指摘と質疑について

  1. 本記事の記述への誤りの指摘は Facebook または Twitter でどうぞ。
  2. それ以外の UE4 に本質的な内容の質疑は UE4 ANSWERHUB へどうぞ。

目次

  1. プロフェッショナル C++er のための UE4/C++ チュートリアル&予防接種
    1. UE4/C++ の数値型と std 互換性
      1. 数値型
      2. TNumericLimits と std::numeric_limits
    2. UE4/C++ の文字・文字列型と std 互換性
      1. UE4 の文字エンコーディング
      2. FStringstd::basic_string<T>
      3. UE4 の文字列型と用途
      4. FString のワイルドカードマッチと正規表現
    3. UE4/C++ における Unreal Header Tool の役割と制約
      1. Unreal Header Tool の役割
      2. Unreal Header Tool の制約
      3. ENUM のフラグ化とリフレクションの恩恵
    4. UE4/C++ における入門的な「メモリーアロケーションガベージコレクション
      1. NewObjectCreateDefaultSubObject 及び C++new の違い
      2. UActorComponentRegisterComponent 及び AttachToComponent または SetupAttachment
      3. UObject とメンバー変数における UPROPERTY() UHT マクロのガベージコレクターに対する効果
    5. UE4/C++ における「ムーブセマンティクス」
      1. MoveTemp, MoveCopystd::move
      2. Forwardstd::forward
    6. UE4/C++ における「型変換」
      1. Cast<T>, CastChecked<T>dynamic_cast<T> 及び RTTI
    7. UE4/C++ における「アサーション
      1. UE4 アサーション各種と static_assert / assert (<cassert>) の挙動
    8. UE4/C++ における「例外処理」の注意点
      1. ビルドプロファイルとプラットフォームに与える制約
    9. UE4/C++ における入門的な「数学」ライブラリーの std 互換性と独自性
      1. FMath における C++ CRT 互換層
      2. FMath における Generic Platform 層
      3. FMath における Platform Native 層
      4. FMath の真価
      5. FMath に含まれる型
    10. UE4/C++ における入門的な「コンテナー」ライブラリーの std 互換性
      1. TArray<T>std::deque<T> / std::vector<T> / std::queue / std::stack
      2. TMap<K,V>std::unordered_map<K,V>
      3. その他の UE4 コンテナーライブラリー
    11. UE4/C++ における入門的な「スレッド」ライブラリーの std 互換性
      1. TPromise<T>std::promise<T>
      2. TFuture<T>std::future<T>
      3. FRunnableThreadstd::thread
    12. UE4/C++ における「アトミック」ライブラリーの std 互換性
      1. 主なメンバー関数の対応
    13. UE4/C++ における「並列処理」ライブラリーと std / OpenMP / Intel-TBB / Microsoft-PPL
      1. ParallelForfor_each ( C++17 Parallelism ) #pragma omp parallel for / parallel_for ( TBB ) / parallel_for ( PPL )
    14. UE4/C++ における「ファンクター」ライブラリーと std 互換性
      1. TFunctionRef<T>std::function<T>
  2. プロフェッショナル C++er のための UE4/C++ リアル・ベーシック
    1. UE4/C++ におけるガベージコレクターの操作
      1. 任意の UObject* の参照カウントを確認する
      2. 強制的にガベージコレクターを動作させる
    2. UE4/C++ における「相互排除と同期」ライブラリーと std 互換性
      1. FCriticalSection クリティカルセクション型と std::mutex
      2. FRWLock 読み書きロック型と std::shared_mutex
    3. UE4/C++ における基礎的な「HTTP」ライブラリー
      1. FHttpModule
    4. UE4/C++ における「JSON」ライブラリー
      1. FJsonValue 抽象型
      2. UE4/C++ JSON ライブラリーによる読み書き
    5. UE4/C++ における「ファイルシステム」ライブラリー
      1. UE4ファイルシステムライブラリーで .json ファイルを根こそぎ読み出す
    6. エンジンプラグインのリビルド
      1. エンジンプラグインのリビルド

本論

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 uint32CHAR32
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 を使っておけば善い。その理由としては以下を挙げる。

  1. TNumericLimits<T> には Min(), Max(), Lowest() の3つの constexpr 関数しか定義されていない。用途にもよるが C++er が一般に <limits> を用いる際に必要な多くの機能が不足している。
  2. 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_tUE4 ライブラリー定義の WIDECHARUTF-16LE の文字を扱い、 C++ レベルでの互換性のため補助的に charUE4 ライブラリー定義の 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. FStringstd::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 で扱うと考えて善い。

UE4FString は内部的には wchar_t ベースの文字列を扱い、 basic_string<T> よりもやや高機能に作られている。 wchar_t (≃ TCHARWIDECHAR )文字列からの 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つ。

  1. std emptyUE4 IsEmpty
  2. std clearUE4 Empty

命名が下手で関数名から動作の違いがわからないものの例として trim 系について動作を詳しく解説すると次の通り。

  • TrimStartInline: 内部バッファーを置き換える。戻り値は無い。
  • TrimStart: 内部バッファーを置き換えないが内部的に新しい文字列オブジェクトを生成して TrimStartInline を使っている
  • Trim: 内部バッファーを置き換え、戻り値としても結果を出力する。実装詳細が TrimStartInline よりも効率良い。

こうした詳細について API Reference を頼りたいと考えるのは C++er として当然の思考だが、残念ながら UE4API Reference の整備ははっきり言ってお粗末な状態で、十分に動作を理解できる解説がある項目の方が少ない。

他の例では、 ReplaceCharWithEscapedChar の対象となる文字群は一体何なのか、引数を省略した場合はどのような挙動になるのか、そうした重要なドキュメンテーションが欠陥している事が多い。そこで、結局 C++er は実装詳細を確認する事になる。

調べたい内容について API Reference が未整備に見えたなら、あるいは明らかに言葉足らずに見えたなら、速やかに諦めて手元のソースコードを読もう。幸い、ソースコードは比較的綺麗で C++er にとっては比較的簡単に読み解けるコーディングになっている。

UE4ソースコードは開発環境を整えていれば、例えば Windows ならば Program Files/Epic Games/UE_4.18/Engine/Source/ などバージョンごとのディレクトリーに格納されている。現在プロジェクトで使用中のソースコードを参照する場合はこのローカルファイルを読むのが楽だろう。また、インストールしていないバージョンのソースコードGitHubEpicGames / UnrealEngine で読むのが楽だろう。

話が軽くそれかけけたが、ともあれ UE4FStringstd::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 の文字列型として前項で FStringstd::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::MatchesWildcardAPI 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 の役割

  1. リフレクションの構築 ( ← これが主機能 )
    • UE4Editor / Blueprint が C++ 実装と親和性が高いのはこれのおかげ。
    • もちろん、 C++ コード中でもここで構築されるリフレクション機能を使用する事はできる。
      • 例: enum class を文字列から探したり、 C++ コードからは不明な Blueprint のマテリアルを使ったり。
  2. 補助実装の付与
    • 例: enum class にビット演算の operator 群を付与するマクロ ENUM_CLASS_FLAGS など。

1.3.2. Unreal Header Tool の制約

UHT はユーザーの C++ ソースコードを読んで適切な基底型を作成して継承させるなどして実行時のリフレクション機能を組み込んだりする手前、ある程度の規則に従ってソースコードを書く必要性、野生の C++er にとっては「制約」が生じる。

そこで、本記事の趣旨に従い、一般的な C++er が UHT 対応において戸惑う可能性のある事項を整理する。

  1. UE4 フレームワークに組み込んで使用可能な USTRUCT() マクロを付与した struct では
    1. 基底型が USTRUCT() マクロ付きの struct 型に制限される。
    2. 多重継承が不能となる。
    3. template型も継承不能となる。
    4. private protected 継承不能となる。
    5. 型名の先頭1文字が F に制限される。
    6. UFUNCTION() マクロを付与したメンバー関数は定義できない。
  2. UE4 フレームワークに組み込んで使用可能な UCLASS() マクロを付与した class では
    1. 1つ目の基底型は必ず UObject の派生型に制限される。
      • 2つ目以降に定義する基底型は制約を受けず、多重継承、tempalte型の継承も可能。
    2. private protected 継承不能となる。
    3. 型名の先頭1文字が1つ目の基底型に基づいた U または A に制限される。
  3. UE4 フレームワークに組み込んで使用可能な UENUM() マクロを付与した enum class では
    1. 基底型が事実上 uint8 に制限される。( UENUM() マクロに BlueprintType を与えなければ制限されないが、ユースケース的にまず無い )
    2. 型名の先頭1文字が E に制限される。
  4. UCLASS() のメンバー関数を UE4 フレームワークで扱えるよう指定する UFUNCTION() マクロでは
    1. シグニチャーに含まれる型が原則的に USTRUCT() UCLASS UENUM または int32 等の UE4 フレームワークの対応した型に制限される。
      • ユーザー定義の template 型は使えない。( UE4 ライブラリーに組み込まれた TArray 等は使用可能 )
    2. using typedef による型エイリアスは扱えない。
  5. USTRUCT() または UCLASS() のメンバー変数を UE4 フレームワークで扱えるよう指定する UPROPERTY() マクロでは
    1. 型が原則的に USTRUCT() UCLASS UENUM または int32 等の UE4 フレームワークの対応した型に制限される。
      • ユーザー定義の template 型は使えない。( UE4 ライブラリーに組み込まれた TArray 等は使用可能 )
    2. using typedef による型エイリアスは扱えない。

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. NewObjectCreateDefaultSubObject 及び 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 が注意する点は次の通り:

  1. UObject 派生型の ctorUE4 フレームワークにより実行時にリフレクションの静的なオブジェクトを生成するため特別な扱いを受ける。この都合、
    1. UObject 派生型の ctor では NewObject<T> は使用できない。
    2. UObject 派生型の ctor では CreateDefaultSubobject<T> を使用する。
      • 引数は基本的には "適当な名前" を渡せば良い。例: CreateDefaultSubobject< UHoge >( TEXT( "hoge" ) )
  2. NewObject<T> ではオブジェクト管理上の親子関係のため、基本的には UObject 派生型の this を使う。 例: NewObject< UHoge >( this )
  3. NewObject<T> CreateDefaultSubobject<T> で生成しただけでは UE4 フレームワークガベージコレクションの管理対象とは「ならない」。
    • §1.4.3 を参照。
  4. UObject 以外の型全般についてはヒープアロケーションが必要な場合は

    1. 基本的に new を行う。
    2. NewObject<T>CreateDefaultSubobject<T> は使用できない。(これらは UHT で正常に前処理された UObject 派生型にしか使えない。)
  5. 参考

    1. NewObject | Unreal Engine
    2. CreateDefaultSubobject | Unreal Engine
    3. Garbage Collection Overview - Epic Wiki

1.4.2. UActorComponentRegisterComponent 及び AttachToComponent または SetupAttachment

UE4C++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 が理解しておくと良い注意点は次の通り。

  1. RegisterComponentガベージコレクションとは関係ない。
  2. RegisterComponentUActorComponent によるコンポーネントの親子関係の紐付けのためにある。
    • RegisterComponent しないとコンポーネントの親子関係に基づく多くの機能の対象とならなず、メッシュの表示なども行われない。
  3. CreateDefaultSubobject<T>RegisterComponent の機能も内包しているので CreateDefaultSubobject<T> する場合は "たいていは" RegisterComponent する必要は無い。

主に言いたかった事は始めに挙げた「 RegisterComponentガベージコレクションとは関係ない。」です。 NewObject<T>, CreateDefaultSubobject<T> で生成したオブジェクトとガベージコレクションは次項で触れます。

1.4.3. UObject とメンバー変数における UPROPERTY() UHT マクロのガベージコレクターに対する効果

UE4 のガベージコレクターがどのように管理対象のオブジェクトの参照カウントを管理するか、この答えには UE4 のリフレクション、 UHT のマクロの解説と注意点を示す必要がありました。本項でようやく、 UE4UObject 派生型のオブジェクトがどのように UE4ガベージコレクションで管理されるかを C++er 向けに記述します。

  1. UHT で UPROPERTY() マクロを定義したメンバー変数は実行中にガベージコレクションの監視対象に入る。
    • UPROPERTY() 付きのメンバー変数であれば TVector<T>, TMap<K,V> 等の UE4 が特別にサポートするテンプレートコンテナー型を介しても監視対象に入る。
  2. UObjectBaseUtility::AddToRoot によりガベージコレクションの管理対象へ明示的に手管理で入れる事も "いちおう" できる。
    • 但し、通常一般的にこの手段を用いる事は無い。

つまり、 UE4ガベージコレクションNewObject<T>, CreateDefaultSubobject<T> したオブジェクトを加える為には、多くの場合にはその生成を行ったクラスに UPROPERTY() マクロ付きで定義したメンバー変数を用意しておき、生成後にそのメンバー変数へ代入する必要がある。

UE4 のガベージコレクターはデフォルトの設定では 60 秒毎に全てのオブジェクトの参照カウントを UE4UObject 派生型とリフレクションに基づいてカウントし、参照カウントが無いオブジェクトについて破棄を行う。

さしあたり §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, MoveCopystd::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::moveMoveTemp の挙動の違いについて触れる。

  • std::move を用いた場合:
    • なんでも rvalue-reference にして返してくれる。
  • MoveTemp を用いた場合:
    • lvalue-reference が入ると static_assert して教えてくれる。
    • const が入ると static_assert して教えてくれる。

何れも基本的な機能は同等だが、 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. Forwardstd::forward

Forwardstd::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 ログ出力により停止

UE4Cast<T>UObject 派生型の型安全なダウンキャストに配慮した実装になっている。 UObject に仕込まれた GetInterfaceAddress / StaticClass から型安全なダウンキャストを判断し、安全では無い場合には nullptr を返す。

Cast<T> にはエラーログ機能付き版の CastChecked<T> もあり、 DO_CHECK CPPマクロが有効な場合には入力または出力が nullptr あるいは bool への暗黙的なキャスト結果が fasle となるような場合には Fatal レベルのエラーログ UE4 のログ機能を通してを出力しつつ停止する。

ほかに、入力が nullptr ではなく入力の GetClass と出力型の StaticClassoperator== 的に一致する場合にのみキャストを行い、失敗した場合は 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 を有効化したい状況には次のような事情が考えられる。

  1. 使用したいライブラリーが RTTI を前提に書かれている。
  2. UObject 派生型以外の型についてデバッグ目的のロギング等で typeid を使用したい。
  3. UObject 派生型以外のユーザー定義型に対して dynamic_cast を使用した設計のコードを記述したい。

本稿では知識としての紹介に留め、これ以上の具体的なユースケースやポリシーについては言及しないが事情に対して現実的な解決策として "必要" ならば使用せざるを得まい。

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++ ときどき ごはん、わりとてぃーぶれいく☆