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>
では対応が困難な機能は、外部ライブラリーを取り込んで使えば良い。
次項の前に少し <cmath>
と定数とコンパイルオプション -D
相当の渡し方について、ついでに紹介する。
<cmath>
と言えば、ベテランの C++er にはよく知られる _USE_MATH_DEFINES
と C++ 規格外の定数値がある。 _USE_MATH_DEFINES
はユーザーが事前に手作業で定義していなければ UE4 では定義されない。特に Microsoft プラットフォームの C++er はよく把握しているように、 M_PI
や M_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" );
Definitions
は List<T>
型で実装されている。詳細については ModuleRules.cs Definitions
や VCToolChain.cs を読むと良い。
と、紹介して終わりたいのだが、実は件の UE4 の FMath
の #include
の流れで <math.h>
へ辿り着くのは UE4 プロジェクトのソースコードの翻訳単位よりも前なので、もしこの方法で M_PI
や M_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 にわかりやすいよう比較を交えて整理する。
- Header: Runtime/Core/Public/GenericPlatform/GenericPlatformMath.h
- Class: FGenericPlatformMath
関数名については、おおよそは <cmath>
の sin
に対して UE4 の FMath
では 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
に無い数学関数は以下の通り:
- 双曲線; (
sinh
だけはFMath
にもある)cosh
,tanh
asinh
,acosh
,atanh
- 指数, 対数
expm1
log10
,log1p
- 仮数, 指数
ldexp
,frexp
ilogb
,logb
scalbn
,scalbln
- 冪乗, 冪根, 絶対値
cbrt
hypot
- 誤差, γ
erf
,erfc
tgamma
,lgamma
- C++17 で追加される高度な数学関数群
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
- 剰余
reminder
,remquo
- 浮動小数点数
nan
,nanf
,nanl
fpclassify
isinf
,isnormal
- 融合積和演算
fma
これらが必要な場合は既存実装を組み立てるか、自作するか、あるいは素直に std 版の実装を使えばよい。
逆に、 std に無いが FMath
に Generic Platform 層として組み込まれている機能は次の通り:
- 冪根
InvSqrt
- 2進数演算
FloorLog2
,FloorLog2_64
CountLeadingZeros
,CountLeadingZeros64
,CountTrailingZeros
CeilLogTwo
,CeilLogTwo64
,RoundUpToPowerOfTwo
CountBits
- 浮動小数点数
IsNegativeFloat
,IsNegativeDouble
FloatSelect
Sign
- 最大, 最小
T Min( TArray<T>, int32* )
,T Max( TArray<T>, int32* )
- 擬似乱数; (
Rand
,RandInit
は std のrand
,srand
が対応。)FRand
SRandInit
,GetRandSeed
,SRand
- モートン符号
MortonCode2
,ReverseMortonCode2
MortonCode3
,ReverseMortonCode3
地味に2進数処理を中心に "チョットベンリ" 的な実装がやや充実している。
FMath
の擬似乱数機能は Rand
系にせよ Srand
系にせよ、特に優れた利点は無いので、一般的な C++er は化石を温かく見守る気持ちで眺めつつ、 std の <random>
実装を使えば良い。
また、生きてきた分野にもよるが、モートン符号については知らない C++er も少なくないかもしれない。そこで、本記事の執筆と新人くんへ解説にも使う手前、日本語版 Wikipedia に Z階数曲線 として 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, Mac の F{PlatformName}PlatformMath
の実装では、 "Public/Core/Math/UnrealPlatformMathSSE.h" から InvSqrt
と InvSqrtEst
の最適化版が読み込まれる。 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
で最適化される InvSqrt
と InvSqrtEst
も _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 |
実行速度の計測は uint64
の counter
を 0
からインクリメントしつつ、その counter
値を各種の平方根逆数関数へ入れ、結果を float
の sum
へ逐次 +=
するループを std::chrono::steady_clock
で 20 秒間行い、 count / 20
により秒あたりの処理回数とした。
また、以下に各種の平方根逆数関数で 1/√2
を float
で計算した結果について、 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 としている rsqrt
は InvSqrt
の Newton-Raphson 法なしの結果。
今回の std::sqrt
は MSVC++ 2017 の cl.exe-19.11.25547 for x64 の結果だが、恐らく最終的には UE4 の SSE 手書き最適化版と同様の機械語が生成されていると思われる。本記事の主目的はその詳細ではないため…というかアドベンカレンダーの期日の都合もあり割愛するが、興味があれば逆アセンブルして読んで見ると面白そうだ。
UE4 プラットフォーム最適化版の InvSqrt
と InvSqrtEst
及び 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 層は、 UE4 の FMath
の階層として一応は存在している、という程度に知識として把握しておく程度で十分だろう。
(はてなブログの記事あたりの容量制限のため続き §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章構成で次の内容を記す。
それなりのボリュームとなるのでここでは先ず章立てのみ記した。より具体的には後の節の「目次」を参照されたい。
背景
今年はお仕事でも 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++ ときどき ごはん、わりとてぃーぶれいく☆)
UE4: クラスファイルにたいてい仕込むログマクロの糖衣マクロ
UE4 に C++ コードファイルを追加する場合、特に 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 に Definitions
で Add
すればよい。
UE4: UnrealWebServer-1.4 の GetData に文字列末尾が汚染されるバグを見つけたので応急対処法
1. UnrealWebServer
2. 問題
- UnrealWebServer-1.4 の
GetData
API の返り値の文字列の末尾がランダムな文字群で汚染されるバグに遭遇した。高頻度で汚染される。
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. おまけ: 自家修正版エンジンプラグインのビルド&エンジンへの配置の仕方
- diff に基いてパッチする。
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"
などと唱える。- 念の為、元のエンジンプラグイン一式 "C:\Program Files\Epic Games\UE_4.18\Engine\Plugins\Marketplace\UnrealWebServerPlugin\" をバックアップする。
- 自家ビルドの修正版エンジンプラグイン "C:\Users\<your-account>\tmp\UnrealWebServerPlugin" を元のエンジンプラグインがあった場所へ入れる。
6. 問題の報告と公式の更新について
この記事を書く前に開発元の Isara tech. にもバグ報告、原因、修正 diff を送ってあるのでそう遠くなくアップデートしてくれる、はず。
UE4: 4.17.2 -> 4.18.1 プロジェクトのアップデートで発生した Warning と Error と対処メモ
若干の調査は必要なものの何れも些細な単純な置き換えで済む問題だけで済んだ。
Warning
1. AddTorque
は AddTorqueInRadians
に置き換えよ
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.
- UPrimitiveComponent::AddTorque | Unreal Engine
- UPrimitiveComponent::AddTorqueInRadians | Unreal Engine
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::GameDir
は FPath::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
FFileHelper::LoadFileToString
の第3引数の型FFileHelper::EHashOptions
はuint32
から暗黙的に変換不能となった
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"
知らずに一見すると素晴らしいボクセル地形のプラグインと錯覚してしまい、 $99.99 の価格にも "これくらいなら" とポチってしまうかもしれない。
ところが、ポチる前によくコメントを見ると非常に有用で、愉快かつ痛烈なコメントを見つけられる。
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 の開発者です。
ちょうど、ボクセル地形を扱う用事が発生していたところでこのやり取りを見かけて少し面白い気持ちがしたので紹介でした。
参考:
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 }
- clang-5.0.0
-std=c++11 -pedantic-errors
: https://wandbox.org/permlink/7KxTfjQ4X52GmH2g
この問題の影響
- 言語規格違反
Y(,)
パターンのような回避策を強いられる。- Microsoft プラットフォームのため『だけ』に追加のコーディングコストを強いられる。
だるい・x・