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

USAGI.NETWORKのなかのひとのブログ。主にC++。

Real Unreal Engine C++ 2017-12 (part-2/5)

はてなブログの記事あたりの容量制限のため前の部分 §1.8. までは前の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2

1.9. UE4/C++ における入門的な「数学」ライブラリーの std 互換性と独自性

1.9.1. FMath における C++ CRT 互換層

C++er は "事足りる限り" は通常 <cmath> を用いて数学処理を実装する。

UE4 では多くの数学的な実装は FMath 型に static メンバー関数として実装されている。実はこの Fmathソースコードを辿ると、 "Runtime/Core/Public/HAL/PlatformCrt.h" へ辿り着き、 math.h#include される。

#include <new>      // <-- <new> も有効になるので placement new 構文など使用可能になる
#include <wchar.h>
#include <stddef.h> // <-- size_t はここ。
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <math.h>   // <-- 本項で触れる互換性
#include <float.h>  // <-- ついでに読んでいる
#include <string.h>

と、云うわけで、 C++er は UE4 プロジェクトでも <cmath> の機能をそのまま使用して数学的な処理を記述「することも」できる。

但し、 <cmath>処理系依存で挙動が変わる機能もあり、また実行速度に最適ではない実装が採用されている事もあり、 UE4 では FMath 型を定義し、 static なメンバー関数として <cmath> の薄いラッパーから独自の再実装、 <cmath> にない機能の実装などを整理している。

よって、 UE4/C++er は特定の単一あるいは十分に <cmath> で十分に製品の目的に適う場合を除き、 UE4 ではイッパンに FMath に定義される数学関数を使用する。一般的にゲーム用途では FMath の実装で十分であるが、用途や製品の設計によっては double, long double, 複素数、疎行列などを扱う必要もあるかもしれない。そうした場合には FMath で不足する機能を <cmath>template 実装で補ったり、 Eigen をプロジェクトへ取り込んだりする必要が生じる。

念の為、整理しておく。

  • UE4/C++er は事足りる限りは FMath を使う。
  • long double など FMath に定義されないが <cmath> ならば扱える機能が必要な場合は <cmath> の実装を使う。
  • FMath, <cmath> では対応が困難な機能は、外部ライブラリーを取り込んで使えば良い。
    • 例えば UE4四元数float 実装しかないので double が必要なら Eigen::Quaterniond の使用を検討するなど。

次項の前に少し <cmath> と定数とコンパイルオプション -D 相当の渡し方について、ついでに紹介する。

<cmath> と言えば、ベテランの C++er にはよく知られる _USE_MATH_DEFINESC++ 規格外の定数値がある。 _USE_MATH_DEFINES はユーザーが事前に手作業で定義していなければ UE4 では定義されない。特に Microsoft プラットフォームの C++er はよく把握しているように、 M_PIM_E などの頻出する数学定数の #define 値は定義されない。 M_PI などの定数にについては UE4 にも独自の定義 PI などが使用可能なよう定義されるので通常はそちらを使用する。ふだん、これら非標準ながら "事実上一般化" している <cmath> の定数群に慣れている C++er は注意すると良い。

UE4 で定義される数学的な定数について確認したければ UnrealMathUtility.h Floating point constants を眺めると良い。

さて、コンパイルオプションレベルで _USE_MATH_DEFINES を定義できないのか、と言えば実はできる。 "{your-project-name}.Build.cs" に次のように記述すれば任意の定義をコンパイルオプション -D で渡せる。

// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する
Definitions.Add( "_USE_MATH_DEFINES" );

DefinitionsList<T> 型で実装されている。詳細については ModuleRules.cs DefinitionsVCToolChain.cs を読むと良い。

と、紹介して終わりたいのだが、実は件の UE4FMath#include の流れで <math.h> へ辿り着くのは UE4 プロジェクトのソースコードの翻訳単位よりも前なので、もしこの方法で M_PIM_SQRT1_2 を使いたければ、愚直に .cpp で Definitions またはソース内での #define を適当に行い #include <math.h> するのが最も簡単だったりする。

C++er にとって、特に開発中、デバッグ用途で Definitions はしばしば有用な事があるので覚えておくと良い。

1.9.2. FMath における Generic Platform 層

FMath の実装では <cmath> の薄いラッパーによる実装もあるが、 C++ CRT の数学機能には地味に実装依存な挙動もあり、しばしば UE4 では次項の Generic Platform 層の独自実装(≃ UE4 のサポート対象プラットフォームであれば同様の挙動を示す)も混在すると前項で紹介した。本稿では UE4 が Generic Platform 層として CRT 互換層をラップしたり、独自実装を定義し、 UE4 の展開するプラットフォーム群に対して一般に同じ挙動を速度フォーカスした機能群について std 慣れした C++er にわかりやすいよう比較を交えて整理する。

関数名については、おおよそは <cmath>sin に対して UE4FMath では Sin のように先頭が大文字に変化する程度。

以下に std 慣れした C++er にわかりやすいよう実装の概略を挙げつつ最終的には FMath となる FGenericPlatformMath の基礎的な数学関数群を整理する。なお実装の概略はあくまでも概略であり、実装詳細そのものではない。

FGenericPlatformMath 関数 一般的に云う効果 C++er に優しい)
実装の概略と蛇足
int32 TruncToInt(float F) 整数キャスト (int32)F
float TrucToFloat(float F) 整数キャスト+浮動小数点数キャスト (float)(int32)F
int32 FloorToInt(float F) (int32)floorf(F)
float FloorToFloat(float F) floorf(F)
double FloorToDouble(double F) floor(F)
int32 RoundToInt(float F) 四捨五入 (int32)floor(F+0.5f)
// C++11 round 不使用
float RoundToFloat(float F) 四捨五入 floorf(F+0.5f)
// C++11 round 不使用
double RoundToDouble(double F) 四捨五入 floor(F+0.5f)
// C++11 round 不使用
int32 CeilToInt(float F) 天井 (int32)ceilf(F)
float CeilToFloat(float F) 天井 ceilf(F)
double CeilToDouble(double F) 天井 ceil(F)
float Fractional(float Value) 符号付き小数部
(+値→[0..1),-値→[-1..0))
Value - (float)(int32)Value
float Frac(float Value) 小数部([0..1)) Value - floorf(Value)
float Modf(const float InValue, float* OutIntPart) 整数部と小数部への分解 modff(Invalue, OutIntPart)
double Modf(const double InValue, double* OutIntPart) 整数部と小数部への分解 modf(Invalue, OutIntPart)
float Exp( float Value ) 底eの指数 expf(Value)
float Exp2( float Value ) 底2の指数 powf(2.f, Value)
// C++11 exp2 不使用
float Loge( float Value ) 底eの対数 logf(Value)
float LogX( float Base, float Value ) 任意の底の対数 logf(Value)/logf(Base)
float Log2( float Value ) 底2の対数 logf(Value) * 1.4426950f
// C++11 log2 不使用
float Fmod(float X, float Y) 浮動小数点数の剰余 // fmod 不使用, fabsf 使用
// , 独自のエラー通知あり
float Sin( float Value ) 正弦 sinf(Value)
float Asin( float Value ) 安全な値域[-1..+1]へのクランプ付き逆正弦 asinf( (Value<-1.f) ? -1.f : ((Value<1.f) ? Value : 1.f) )
float Sinh(float Value) 双曲線正弦 sinhf(Value)
float Cos( float Value ) 余弦 cosf(Value)
float Acos( float Value ) 安全な値域[-1..+1]へのクランプ付き逆余弦 acosf( (Value<-1.f) ? -1.f : ((Value<1.f) ? Value : 1.f) )
float Tan( float Value ) 正接 tanf(Value)
float Atan( float Value ) 正接 atanf(Value)
float Atan2( float Y, float X ) 勾配の軸成分の逆正接 // CRT 実装依存バグ対策と高速化のため独自実装
//, CRT 比で誤差7.15255737e-7 以下
float Sqrt( float Value ) 平方根 sqrtf(Value)
float Pow( float A, float B ) 任意の底の指数 powf(A, B)
float InvSqrt( float F ) 平方根の逆数 1.0f/ sqrtf(F)
float InvSqrtEst( float F ) 平方根の逆数 // 高速版のはずだけどこの層の実装では
// InvSqrt(F) を呼んでいるだけ
bool IsNaN( float A ) NaN判定 (((uint32)&A) & 0x7FFFFFFF) > 0x7F800000
// isnan 不使用
bool IsFinite( float A ) 有限判定 (((uint32)&A) & 0x7F800000) != 0x7F800000
// isfinite 不使用
bool IsNegativeFloat(const float& A) 負判定 ( ((uint32)&A) >= (uint32)0x80000000 )
// signbit 不使用
bool IsNegativeDouble(const double& A) 負判定 ( ((uint64)&A) >= (uint64)0x8000000000000000 )
// signbit 不使用
int32 Rand() プラットフォーム依存の
線形合同法の疑似乱数の生成
rand()
void RandInit(int32 Seed) プラットフォーム依存の
線形合同法の系の初期化
srand(Seed)
float FRand() プラットフォーム依存の
線形合同法擬似乱数を元にした
簡易的な非負正規化浮動小数点数
Rand() / (float)RAND_MAX
void SRandInit( int32 Seed ) 線形合同法を非負正規化浮動小数点数
直接生成に特殊化した擬似乱数生成器の初期化
// 大域変数 GSrandSeed の値を代入するだけ
int32 GetRandSeed() 線形合同法を非負正規化浮動小数点数
直接生成に特殊化した擬似乱数生成器に種値の取得
// 大域変数 GSRandSeed の値を返すだけ
float SRand() 線形合同法を非負正規化浮動小数点数
直接生成に特殊化した擬似乱数生成器による乱数生成
// GSRandSeed*196314165+907633515のビット列を
// floatの非負正規化領域へマスクし
// 小数部を取得する擬似乱数生成器
uint32 FloorLog2(uint32 Value) 底が2の対数の2進数床 // 0,1,2,3,4 --> 0,0,1,1,2,2 if (Value >= 1<<16) { Value >>= 16; pos += 16; }
// 的な繰り返しにより算出
uint64 FloorLog2_64(uint64 Value) 底が2の対数の2進数床 // 0,1,2,3,4 --> 0,0,1,1,2,2 if (Value >= 1ull<<32) { Value >>= 32; pos += 32; }
// 的な繰り返しにより算出
uint32 CountLeadingZeros(uint32 Value) 2進数で先頭から連続する0の個数を取得 if (Value == 0) return 32; return 31 - FloorLog2( Value )
uint64 CountLeadingZeros64(uint64 Value) 2進数で先頭から連続する0の個数を取得 if (Value == 0) return 64; return 63 - FloorLog2_64( Value )
uint32 CountTrailingZeros(uint32 Value) 2進数で末尾から連続する0の個数を取得 // 右シフトを繰り返し末尾1ビットを検出
uint32 CeilLogTwo( uint32 Arg ) 底が2の対数の2進数天井 // 0,1,2,3,4,5 --> 0,0,1,2,2,3 (32 - CountLeadingZeros(Arg - 1)) & (~(((int32)(CountLeadingZeros(Arg) << 26)) >> 31))
uint64 CeilLogTwo64( uint64 Arg ) 底が2の対数の2進数天井 // 0,1,2,3,4,5 --> 0,0,1,2,2,3 (64 - CountLeadingZeros(Arg - 1)) & (~(((int32)(CountLeadingZeros(Arg) << 57)) >> 63))
uint32 RoundUpToPowerOfTwo(uint32 Arg) 底が2の指数の2進数天井 // 0,1,2,3,4,5 --> 1,1,2,4,4,8 1 << CeilLogTwo(Arg)
uint32 MortonCode2( uint32 x ) 2次元のモートン符号化 // ビットの &, <<, ^ により実装
uint32 ReverseMortonCode2( uint32 x ) 2次元のモートン復号化 // ビットの &, >>, ^ により実装
uint32 MortonCode3( uint32 x ) 3次元のモートン符号化 // ビットの &, <<, ^ により実装
uint32 ReverseMortonCode3( uint32 x ) 3次元のモートン復号化 // ビットの &, >>, ^ により実装
float FloatSelect( float Comparand, float ValueGEZero, float ValueLTZero ) 1つの比較対象を0以上か判定し他の2つの値の何れかを選択 v >= 0.f ? a : b
double FloatSelect( double Comparand, double ValueGEZero, double ValueLTZero ) 1つの比較対象を0以上か判定し他の2つの値の何れかを選択 v >= 0. ? a : b
T Abs( const T A ) 絶対値 v >= 0 ? v : -v // std::abs 不使用
T Sign( const T A ) 正、負、零の3値へ正規化する v > 0 ? 1 : v < 0 ? -1 : 0
T Max( const T A, const T B ) 2つの値から大きい方を選択 a >= b ? a : b // std::max 不使用
T Min( const T A, const T B ) 2つの値から小さい方を選択 a <= b ? a : b // std::min 不使用
T Min(const TArray<T>& Values, int32* MinIndex = NULL) 配列から最も小さい値とインデックスを取得 // 単純に for で全探索する実装
T Max(const TArray<T>& Values, int32* MaxIndex = NULL) 配列から最も大きい値とインデックスを取得 // 単純に for で全探索する実装
int32 CountBits(uint64 Bits) ハミング重み // std::bitset::count 不使用
float Abs( const float A ) 絶対値 // std::fabsf 使用

以上の FMath の Generic Platform 層の実装では、ほとんどの実装で std の <cmath> の薄いラッパーとしたり、実装詳細へ応用しないよう実装されている。これは多くはポリシーによるものだが、一部 "蛇足" として表中に記載したように一般的な実装よりも速度最適化された実装とするためのもの(例: Atan2 )、実用上の安全性を向上したもの(例: Asin )もある。

UE4/C++er は特別の事情が無ければ、基礎的な数学関数については <cmath> よりも FMath を用いると良い。

なお、 std にあるが UE4 FMath に無い数学関数は以下の通り:

  1. 双曲線; ( sinh だけは FMath にもある)
    1. cosh, tanh
    2. asinh, acosh, atanh
  2. 指数, 対数
    1. expm1
    2. log10, log1p
  3. 仮数, 指数
    1. ldexp, frexp
    2. ilogb, logb
    3. scalbn, scalbln
  4. 冪乗, 冪根, 絶対値
    1. cbrt
    2. hypot
  5. 誤差, γ
    1. erf, erfc
    2. tgamma, lgamma
  6. C++17 で追加される高度な数学関数群
    1. assoc_laguerre, assoc_legendre, beta, comp_ellint_1, comp_ellint_2, comp_ellint_3, cyl_bessel_i, cyl_bessel_j, cyl_bessel_k, cyl_neumann, ellint_1, ellint_2, ellint_3, expint, hermite, laguerre, legendre, riemann_zeta, sph_bessel, sph_legendre, sph_neumann
  7. 剰余
    1. reminder, remquo
  8. 浮動小数点数
    1. nan, nanf, nanl
    2. fpclassify
    3. isinf, isnormal
  9. 融合積和演算
    1. fma

これらが必要な場合は既存実装を組み立てるか、自作するか、あるいは素直に std 版の実装を使えばよい。

逆に、 std に無いが FMath に Generic Platform 層として組み込まれている機能は次の通り:

  1. 冪根
    1. InvSqrt
  2. 2進数演算
    1. FloorLog2, FloorLog2_64
    2. CountLeadingZeros, CountLeadingZeros64, CountTrailingZeros
    3. CeilLogTwo, CeilLogTwo64, RoundUpToPowerOfTwo
    4. CountBits
  3. 浮動小数点数
    1. IsNegativeFloat, IsNegativeDouble
    2. FloatSelect
    3. Sign
  4. 最大, 最小
    1. T Min( TArray<T>, int32* ), T Max( TArray<T>, int32* )
  5. 擬似乱数; ( Rand, RandInit は std の rand, srand が対応。)
    1. FRand
    2. SRandInit, GetRandSeed, SRand
  6. モートン符号
    1. MortonCode2, ReverseMortonCode2
    2. MortonCode3, ReverseMortonCode3

地味に2進数処理を中心に "チョットベンリ" 的な実装がやや充実している。

FMath擬似乱数機能は Rand 系にせよ Srand 系にせよ、特に優れた利点は無いので、一般的な C++er は化石を温かく見守る気持ちで眺めつつ、 std の <random> 実装を使えば良い。

また、生きてきた分野にもよるが、モートン符号については知らない C++er も少なくないかもしれない。そこで、本記事の執筆と新人くんへ解説にも使う手前、日本語版 WikipediaZ階数曲線 として English 版の解説を基に執筆したので興味があれば確認されたい。 MortonCode2 は入力値を 2 次元用のモートンコード、すなわち 0-Bit 目を含めた偶数ビットへ展開、 ReverseMortonCode2 はその逆変換を行い、 MortonCode3, ReverseMortonCode3 は 3 次元用に展開するビットが 3 bit ごとになったバージョンでそれぞれ最大で 65,536 * 65,536 分割された 2 次元空間または 1,024 * 1,024 * 1,024 分割された 3 次元空間に対して使用できる。

1.9.3. FMath における Platform Native 層

Generic Platform 層の上に、 Platform Native 層があり、プラットフォームごとに Generic Platform 層よりも高効率な実装が可能な場合にはより高効率な実装が使われるよう FGenericPlatformMath 型から継承し、例えば Windows プラットフォームであれば FWindowsPlatformMath 型が定義される。

この仕組みにより、最終的に FMath で使用される数学関数は、各プラットフォームで可能な限り速度最適化された実装が採用されるよう設計されている。・・・設計上は。

と、いうのも実は開発リソースと優先順序のためか、 UE4 "Runtime/Core/Public/{platform}/{platform}(Platform)Math.h" を見比べてみると、 Android, iOS, HTML5 向けの FMath の基礎的な実装についてのプラットフォーム最適化は UE-4.18 の執筆現在、残念ながら実質無いに等しい。

そこで、本稿では紹介程度に留める意味で Windows プラットフォーム向けの最適化実装 FWindowsPlatformMath を例に具体的にどのようなプラットフォーム最適化実装が行われているのかやんわりと整理するに留める。同様の最適化はいわゆる PC 系プラットフォームに対して定義があり、 Windows のほかには Linux, Mac に対して同等の最適化が施される。

Windows, Linux, MacF{PlatformName}PlatformMath の実装では、 "Public/Core/Math/UnrealPlatformMathSSE.h" から InvSqrtInvSqrtEst の最適化版が読み込まれる。 Generic 層では "名ばかりの高速版" だった InvSqrtEst もここで意味を持つようになる。

この機能は一般に C++er が特別の需要が無く実装を書くならば 1 / std::sqrt( x ) となる。実装は著者の知る限りでは浮動小数点数の型に応じて必要な精度を満たすようニュートン法により実装される。

ここで、C++ 標準から x64 の CPU 命令へ視点を移すと、平方根_mm_sqrt_ss 系、 平方根の逆数は _mm_rsqrt_ss 系の SSE 命令で得られる。しかし、これら SSE 命令の平方根関数及び平方根逆数関数の群は 12 bits 程度の精度の結果しか得られない。つまり、 10 進数の実数にしてせいぜい3桁程度しか信頼できる値を得られない。( _mm_rsqrt_ss 命令の相対誤差は |相対誤差| ≤ 1.5 * pow(2,-12)。 詳細は Intel® 64 and IA-32 Architectures Software Developer Manuals を参照すると良い。)

そこで、より精度の高い平方根の逆数が必要な場合には _mm_rsqrt_ss の結果を基に Newton-Raphson 法により精度を向上する手法が用いられる。 UE4 における UnrealPlatformMathSSE で最適化される InvSqrtInvSqrtEst_mm_rsqrt_ss + Newton-Raphson 法により実装されている。

// ===== 以下は InvSqrt, InvSqrtEst 共通 =====

// 導関数と Newton-Raphson 法のイテレーションの展開
//    v^-0.5 = x
// => x^2 = v^-1
// => 1/(x^2) = v
// => F(x) = x^-2 - v
//    F'(x) = -2x^-3
//
//    x1 = x0 - F(x0)/F'(x0)
// => x1 = x0 + 0.5 * (x0^-2 - Vec) * x0^3
// => x1 = x0 + 0.5 * (x0 - Vec * x0^3)
// => x1 = x0 + x0 * (0.5 - 0.5 * Vec * x0^2)

// Step 0: 準備
const __m128 fOneHalf = _mm_set_ss(0.5f);
__m128 Y0, X0, X1, X2, FOver2;
float temp;

// Step 1: _rsqrt_ss により 12 bits 精度の平方根の逆数を得る
Y0 = _mm_set_ss(F);
X0 = _mm_rsqrt_ss(Y0);  // 1/sqrt estimate (12 bits)
FOver2 = _mm_mul_ss(Y0, fOneHalf);

// Step 2.1: Newton-Raphson 法による1回目のイテレーション
X1 = _mm_mul_ss(X0, X0);
X1 = _mm_sub_ss(fOneHalf, _mm_mul_ss(FOver2, X1));
X1 = _mm_add_ss(X0, _mm_mul_ss(X0, X1));

// ===== 以下は InvSqrt のみ =====

// Step 2.2: Newton-Raphson 法による2回目のイテレーション
X2 = _mm_mul_ss(X1, X1);
X2 = _mm_sub_ss(fOneHalf, _mm_mul_ss(FOver2, X2));
X2 = _mm_add_ss(X1, _mm_mul_ss(X1, X2));

感覚として掴みやすいようより具体的な実行速度と10進数における有効桁数の比較として整理する:

出処 method #/sec (O0) #/sec (O3) vs. (O3) 有効桁数
std 1.0f / sqrt( (float) count ) 17,043,072 23,573,025 1.000 7
UE4 UnrealPlatformMathSSE::InvSqrt( (float)count ) 10,777,476 22,967,873 0.974 7
UE4 UnrealPlatformMathSSE::InvSqrtEst( (float)count ) 13,050,090 23,568,536 1.000 6
SSE rsqrt 16,812,451 24,343,710 1.033 2

実行速度の計測は uint64counter0 からインクリメントしつつ、その counter 値を各種の平方根逆数関数へ入れ、結果を floatsum へ逐次 += するループを std::chrono::steady_clock で 20 秒間行い、 count / 20 により秒あたりの処理回数とした。

また、以下に各種の平方根逆数関数で 1/√2float で計算した結果について、 32-Bit の全ビットと10進数の実数としての値を std::wstringstream<< std::fixed << std::setprecision(10) の上で operator<< した結果を示す:

出処 method 32-Bit 数値(10桁)
std sqrt 00111111001101010000010011110011 0.7071067691
UE4 UnrealPlatformMathSSE::InvSqrt 00111111001101001111100000000000 0.7069091797
UE4 UnrealPlatformMathSSE::InvSqrtEst 00111111001101010000010011110011 0.7071067691
SSE rsqrt 00111111001101010000010011110010 0.7071067095

出処を SSE としている rsqrtInvSqrt の Newton-Raphson 法なしの結果。

今回の std::sqrt は MSVC++ 2017 の cl.exe-19.11.25547 for x64 の結果だが、恐らく最終的には UE4 の SSE 手書き最適化版と同様の機械語が生成されていると思われる。本記事の主目的はその詳細ではないため…というかアドベンカレンダーの期日の都合もあり割愛するが、興味があれば逆アセンブルして読んで見ると面白そうだ。

UE4 プラットフォーム最適化版の InvSqrtInvSqrtEst 及び rsqrt については計測上の数値としては一応程度に有意な差が観測された。また、精度の問題も 1/√2 の結果から実数値としての感覚としても例示した。

プラットフォーム最適化と言いながらも少々歯切れが悪くなるが O0, O3 の速度計測結果も考慮するに、少なくとも MSVC++-19 で翻訳する限りにおいては std 実装に留めるのが最善だったようである。機会があれば詳細について機械語レベルで調べたい。

なお、蛇足として、 float 値の 32-Bit の UE4 コードでの出力は次の様に実装する:

float x = 1 / std::sqrt( 2.0f );
std::bitset<32> b( *(int32*)&x ); // reinterpret_cast を使いたければ使えば良い
std::wstringstream s;
s << std::fixed << std::setprecision( 10 ) << b;
UE_LOG( LogTemp, Log, TEXT( "%s" ), s.str().c_str() );

さて、ここからようやく FWindowsPlatformMath の本題に入る。最適化の多くは SSE 系の命令による。簡単な例では TructToInt の実装が _mmcvtt_ss2si( _mm_set_ss( x ) ) になるなど。

以下に FWindowsPlatformMath で実装が最適化される関数群について、 GenericPlatformMath 版との速度変化の計測とともに整理する:

関数 Generic(O3) #/sec Windows(O3) #/sec std(O3) #/sec
TruncToInt 23,696,272 24,095,555
TruncToFloat 25,038,371 24,842,574 22,460,595
RoundToInt 24,918,013 24,476,462
RoundToFloat 24,955,395 24,732,947 20,528,517
FloorToInt 25,086,680 24,391,177
FloorToFloat 24,184,578 23,300,533 25,007,228
CeilToInt 25,085,954 24,233,785
CeilToFloat 25,134,920 25,039,425 25,082,468
IsNaN 24,950,199 23,871,049 23,541,114
IsFinite 24,947,065 23,830,000 24,016,180
InvSqrt 24,405,314 24,278,375
InvSqrtEst 23,083,880 23,469,950
FloorLog2 25,135,630 24,765,317
CountLeadingZeros 24,110,845 21,307,304
CountLeadingZeros64 24,340,359 24,123,377
CountTrailingZeros 23,178,912 24,074,612
CeilLogTwo 24,430,826 24,274,930
RoundUpToPowerOfTwo 23,317,767 24,337,332
CeilLogTwo64 23,869,462 20,324,985
CountBits 24,806,175 23,414,599

これも厳密な計測ではないためであくまでも参考値程度ではあるが、これらもまた期待したほど Platform 最適化がうまく実装できているとは言えない結果が得られた。特に Generic 実装の方が翻訳後の機械語レベルでの最適化結果が明らかに良好な傾向のある関数もあり、 UE4 エンジン開発者たちには Android, iOS, HTML5 あるいは Switch や arm 系プロセッサーNeon も意識した最適化などもっと大真面目にこの分野にも取り組んで貰いたい。

さて、やや執筆前の予想とは異なる結果に少々の困惑が著者にもあるものの、一般的な C++er としてこの Platform Native 層は、 UE4FMath の階層として一応は存在している、という程度に知識として把握しておく程度で十分だろう。

はてなブログの記事あたりの容量制限のため続き §1.9.4. 以降は次の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2_p3

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

UE4: クラスファイルにたいてい仕込むログマクロの糖衣マクロ

UE4C++ コードファイルを追加する場合、特に UCLASS では規模がよほど小さくない限りたいてい定義するログマクロの定義と糖衣を紹介。

// MyHoge.h
// コンパイラーオプションで MYHOGE_ENABLE_LOG=1 など明示的に渡されない場合でも
// デバッグビルドの場合にはログマクロを ON にする
#if defined( UE_BUILD_DEBUG ) && ! defined( MYHOGE_ENABLE_LOG)
  #define MYHOGE_ENABLE_LOG 1
#endif

// ログマクロを有効にする場合
#if MYHOGE_ENABLE_LOG > 0
  /// ログONの場合のログマクロラッパー定義
  DECLARE_LOG_CATEGORY_EXTERN( MYHOGE, Log, All );
#endif
// .cpp
// ログマクロを有効にする場合
#if MYHOGE_ENABLE_LOG > 0
  // ログカテゴリー定義
  DEFINE_LOG_CATEGORY( MYHOGE );
  // このソース内でのみ有効なログマクロ書き糖衣マクロ
  #ifndef LOG
    #define LOG( LEVEL, BODY, ... ) \
      { UE_LOG( MYHOGE, LEVEL, TEXT( BODY ), __VA_ARGS__ ) }
  #endif
// ログOFFの場合のログマクロラッパー定義
#else
  #ifndef LOG
    #define LOG( ... ) 
  #endif
#endif

この仕込みを施したならば、 .cpp でログを仕込む際には LOG( "hoge %s %f", *something_string, something_float ) と書くだけでよい。UE_LOG{ }ブロックスコープにしてあるので、 if ( x ) LOG( "hoge" ) のように使っても翻訳に支障もない。MYHOGE_ENABLE_LOG を明示的に制御したい場合は "{your-project-name}.Build.cs に DefinitionsAdd すればよい。

UE4: UnrealWebServer-1.4 の GetData に文字列末尾が汚染されるバグを見つけたので応急対処法

1. UnrealWebServer

2. 問題

  • UnrealWebServer-1.4 の GetData API の返り値の文字列の末尾がランダムな文字群で汚染されるバグに遭遇した。高頻度で汚染される。
    • GetData API は POST のリクエストボディー全体を一括取得する機能

3. 原因

GetData の実装詳細を確認したところ、内部バッファーとして使用する固定長の char 配列が未初期のため発生する問題の可能性が高い事がわかった。

4. 修正

UnrealWebServer-1.4 を使用して GetData するニーズのある方が応急処置可能な必要最小限の diff を示します。

Private/Connection.cpp:

43c43
<       char post_data[4096];
---
>       char post_data[4096] = { 0 };
  • Note: UnrealWebServer は Marketplace で販売されている有料のエンジンプラグインのため、応急処置として必要最小限の diff 露出に留めます。

5. おまけ: 自家修正版エンジンプラグインのビルド&エンジンへの配置の仕方

  1. diff に基いてパッチする。
  2. cmd でも何でもいいので "C:\Program Files\Epic Games\UE_4.18\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="C:\Program Files\Epic Games\UE_4.18\Engine\Plugins\Marketplace\UnrealWebServerPlugin\UnrealWebServer.uplugin" -package="C:\Users\<your-account>\tmp\UnrealWebServerPlugin" などと唱える。
  3. 念の為、元のエンジンプラグイン一式 "C:\Program Files\Epic Games\UE_4.18\Engine\Plugins\Marketplace\UnrealWebServerPlugin\" をバックアップする。
  4. 自家ビルドの修正版エンジンプラグイン "C:\Users\<your-account>\tmp\UnrealWebServerPlugin" を元のエンジンプラグインがあった場所へ入れる。

6. 問題の報告と公式の更新について

この記事を書く前に開発元の Isara tech. にもバグ報告、原因、修正 diff を送ってあるのでそう遠くなくアップデートしてくれる、はず。

UE4: 4.17.2 -> 4.18.1 プロジェクトのアップデートで発生した Warning と Error と対処メモ

若干の調査は必要なものの何れも些細な単純な置き換えで済む問題だけで済んだ。

Warning

1. AddTorqueAddTorqueInRadians に置き換えよ

warning C4996: 'UPrimitiveComponent::AddTorque': Use AddTorqueInRadians instead. Please update your code to the new API before upgrading to the next release, otherwise your project will no longer compile.

void AddTorque
( FVector Torque // Torque to apply. Direction is axis of rotation and magnitude is strength of torque.
, FName BoneName = NAME_None // If a SkeletalMeshComponent, name of body to apply torque to. 'None' indicates root body.
, bool bAccelChange = false // If true, Torque is taken as a change in angular acceleration instead of a physical torque (i.e. mass will have no effect).
);
void AddTorqueInRadians
( FVector Torque // Torque to apply. Direction is axis of rotation and magnitude is strength of torque.
, FName BoneName = NAME_None // If a SkeletalMeshComponent, name of body to apply torque to. 'None' indicates root body.
, bool bAccelChange = false // If true, Torque is taken as a change in angular acceleration instead of a physical torque (i.e. mass will have no effect).
);

API Reference だけ見ても違いがわからない。違いについては予感としては「たぶんない」のだけど、「ない」と知る必要がある。単位や補助単位の変換が必要だとか何かとあり得ない事もない。

source: Runtime/Engine/Classes/Components/PrimitiveComponent.h

void AddTorque(FVector Torque, FName BoneName = NAME_None, bool bAccelChange = false)
{
  AddTorqueInRadians(Torque, BoneName, bAccelChange);
}

安心して単に置き換えるとしよう。

2. FPaths::GameDirFPath::ProjectDir に置き換えよ

warning C4996: 'FPaths::GameDir': FPaths::GameDir() has been superseded by FPaths::ProjectDir(). Please update your code to the new API before upgrading to the next release, otherwise your project will no longer compile.

source: Runtime/Core/Public/Misc/Paths.h

DEPRECATED(4.18, "FPaths::GameDir() has been superseded by FPaths::ProjectDir().")
static FORCEINLINE FString GameDir() { return ProjectDir(); }

これも単純な置き換えで構わない。

Error

  1. FFileHelper::LoadFileToString の第3引数の型 FFileHelper::EHashOptionsuint32 から暗黙的に変換不能となった

error C2664: 'bool FFileHelper::LoadFileToString(FString &,const TCHAR *,FFileHelper::EHashOptions)': cannot convert argument 3 from 'const uint32' to 'FFileHelper::EHashOptions'

4.18 に取り込まれた以下のコミットで Runtime/Core/Public/Misc/FileHelper.h の struct EHashOptions { enum Type {enum class EHashOptions { に変更されたので整数の値は直接設定不能になった。

素直に FFileHelper::EHashOptions::None など enum class 値に置き換えればよい。

UE4: Free Voxel Plugin の作者が Marketpalce にて $99.99 で売り出された Voxel Plugin に痛烈なコメントを付けている件

Marketplace に出品された問題のプラグインは↓の "Voxel Plugin"

f:id:USAGI-WRP:20171112130842p:plain

知らずに一見すると素晴らしいボクセル地形のプラグインと錯覚してしまい、 $99.99 の価格にも "これくらいなら" とポチってしまうかもしれない。

ところが、ポチる前によくコメントを見ると非常に有用で、愉快かつ痛烈なコメントを見つけられる。

f:id:USAGI-WRP:20171112130906p:plain

Hethger: What about the Free Voxel Plugin the community is working on? https://forums.unrealengine.com/community/released-projects/125045-free-voxel-plugin What are the improvements worth paying this price for?

Phyronnaz: I've tried the demo, and it seems to perform a lot worse than the free voxel plugin (edit is slow, low render distance). If you consider buying this, you should first check out the free voxel plugin, as it offers many additional features (materials, mesh/landscape/spline import, tesselation, load/save, LOD with infinite render distance ...). Disclaimer: Free Voxel Plugin creator

翻訳:

ヘザー「コミュニティー版の Free Voxel Plugin に比べてどう違うんだい? https://forums.unrealengine.com/community/released-projects/125045-free-voxel-plugin に対して $99.99 を払う付加価値がどこにあるのさ。」

フィロナズ「私がデモを試した限りでは Free Voxel Plugin より酷いシロモノに感じられたよ(編集が遅いし、レンダリング距離も短いし)。これを購入する気なら、それよりも先に Free Voxel Plugin を試してほしいね、それにはもっとたくさんの付加機能もあるし(マテリアル、メッシュ/ランドスケープ、スプラインのインポート、テッセレーション、ロードとセーブ、無限遠のLODレンダリング、ほかたくさん)。注:Free Voxel Plugin の作者より」

面白い、あるいはこのコメントのやりとりは有用だと思った UE4er はとりえあえず UP VOTE して来よう・x・

ちなみに、 Phyronnaz (フィロナズ)は本当に Free Voxel Plugin の開発者です。

f:id:USAGI-WRP:20171112132441p:plain

ちょうど、ボクセル地形を扱う用事が発生していたところでこのやり取りを見かけて少し面白い気持ちがしたので紹介でした。

参考:

  1. Voxel Plugin by CodeSpartan in Code Plugins - UE4 Marketplace
  2. Free voxel plugin - Unreal Engine Forums
  3. GitHub - Phyronnaz/MarchingCubes: Voxel plugin for Unreal Engine

MSVC++ BUG: C Preprocessor is not capable empty argument

MSVC++(2017; cl.exe-19.11.25547) で C プリプロセッサーのマクロが空の引数を受け付けない C++11/14/17, C99/11 に対する規格違反を報告しました。

概要

#define X( X0 ) something ## X0 () が定義される時、引数が空の呼び出し X() は「空のトークンの引数」を受け付ける事になった C++11 (C99) 以降の言語規格に従えば something() に展開されるべきだが、該当バージョンの現行最新 MSVC++ ではマクロの展開が期待通り行われるエラーとされる。

再現

// compile this code in the MSVC++-19.11.25547
// Expected: compilable
// Actual: not compilable; see the comment below

#include <cstdio>

#define Y( Y0, Y1 ) something ## Y0 ## Y1 ()
#define X( X0 ) something ## X0 ()

void something() { puts( "(n/a)" ); }
void somethingFoo() { puts( "Foo" ); }
void somethingBar() { puts( "Bar" ); }
void somethingFooBar() { puts( "FooBar" ); }

int main()
{
  Y( Foo, Bar );
  Y( Foo, ); // <-- C99 capable, MSVC++ is compilable
  Y( , Bar ); // <-- C99 capable, MSVC++ is compilable
  Y( , ); // <-- C99 capable, MSVC++ is compilable
  X( FooBar );
  X( ); // <-- C99 capable, MSVC++ is NOT compilable
}

この問題の影響

  1. 言語規格違反
  2. Y(,) パターンのような回避策を強いられる。
  3. Microsoft プラットフォームのため『だけ』に追加のコーディングコストを強いられる。

だるい・x・