CPP-QUIZ from Unreal 2017 ( part-2/2; 答え編 )
(この記事は前の投稿 CPP-QUIZ from Unreal 2017 ( part-2/2; 出題編 ) に対応する答え編です。)
2. こたえ
Q1. float
型の [ 0.0 .. 1.0 ]
の値を uint8
型の [ 0 .. 256 )
へ写像したかった
出題のソースコードでは、 "出力が 255
となる元の値の範囲" が 0
から 254
のそれぞれに対応する範囲に対して均等とならない問題が発生する。
例えば、値がグレイスケールカラーの輝度値を表す場合に出題のソースコードを用いると、入力が 1.0
未満の場合に本来よりも僅かに明るくシフトしてしまう結果となる。グレイスケールカラーで人間が目視するだけであれば、大きな問題にはならない事も多いが、変換を繰り返したり、その値での制御を一定時間続ける装置に適用したりすると問題と成り得る事もある。
出題のソースコードと in
と out
の関係の一部:
in | out |
---|---|
[ 0 / 255.f .. 1 / 255.f ) |
0 |
[ 1 / 255.f .. 2 / 255.f ) |
1 |
[ 2 / 255.f .. 3 / 255.f ) |
2 |
[ 254 / 255.f .. 255 / 255.f ) |
254 |
[ 255 / 255.f .. 255 / 255.f ] |
255 |
修正方法:
256
未満で最大の float
の数を掛けると [ 0 .. 256 )
に可能な限り均等に写像できる。
int out = in * 255.9999847412109375f;
おまけ: 255.9999847412109375f
はどこから来たか?
const int32 x = 0b0'10000110'11111111111111111111111; const float y = *(float*)&x; std::cout << std::fixed << std::setprecision( 32 ) << y << '\n';
Q2. 意図しない整数のオーバーフローと問題の遮蔽
出題のコードでは TV
で 32-Bit 以上の整数型、例えば int32
を扱うと問題が起こりうる。例えば、次のような意図しない結果が得られる:
auto x = Lerp ( std::numeric_limits< int32 >::max() , std::numeric_limits< int32 >::min() , 1.0f );
x
には +2,147,483,647
相当の値が期待されるが、実際には -2,147,483,648
が得られる。 b - a
は -2,147,483,648 - 2,147,483,647
= -1
となり、 ratio
= 1.0f
と乗算され -1.0f
、 これが -2,147,483,648
と乗算されると float
型の +2,147,483,648
となるが、 return
直前の (TV)
により int32
型へキャストされる。 +2,147,483,648
を int32
へキャストすると -2,147,483,648
になる。
std::cout << std::bitset< 32 >( (int32) +2'147'483'647 ) << ' ' << std::showpos << (int32) +2'147'483'647 << '\n' << std::bitset< 32 >( (int32) +2'147'483'648 ) << ' ' << (int32) +2'147'483'648 << '\n' << std::bitset< 32 >( (int32) +2'147'483'649 ) << ' ' << (int32) +2'147'483'649 << '\n' ;
01111111111111111111111111111111 +2147483647 10000000000000000000000000000000 -2147483648 10000000000000000000000000000001 -2147483647
この問題は TV
が整数型でも int8
, uint8
, int16
, uint16
の場合には表面化しない。 a
, b
が int
よりも狭いビット幅の整数型の場合に b - a
は signed int
ないし unsigned int
で計算され、結果もその計算に使われた型のままとなる。
int16 a = +32'767; int16 b = -32'768; auto c = b - a; std::cout << typeid( decltype( c ) ).name() << ' ' << std::to_string( c );
s s i -65535
b - a
が int16
ではなく int32
で計算され、結果も int32
となるので -65535
を余裕をもって扱えてしまう。結果、 int
未満のビット幅の整数型を Lerp
の TV
で扱う限りは問題が表面化せず、一見すると任意の整数型に対して意図通りに動作するように見えてしまう。
もし、 Lerp
に単体試験を網羅的に行うよう書いていたとしても、計算時間と "整数型" の概念を漠然と考えて、 int8
と uint8
、あるいは int16
と uint16
で試験を実装してしまっていたなら、この問題はその単体試験が成功してしまうために何かが起きた際に原因を探すのが少し難しくなる事もあるかもしれない。
この問題が起こらない Lerp
の実装例は次の通り:
return (TV)( a * ( 1 - ratio ) + b * ratio );
数学的には等価な ( a + ratio * ( b - a ) )
と ( a * ( 1 - ratio ) + b * ratio )
も現実の計算機では等価とならない事もある。
Q3. 浮動小数点数型
IsSameAltitude
の return
では宇宙船Aと宇宙船Bの人工惑星Oからのそれぞれの高度を計算し、最終的に ==
で float
値を比較している。この処理では float
型の値が完全に一致する必要があるが、ストーリーのニーズからは float
値としての完全な一致ではなく、ゲームのプレイヤーが認識する高度の数値として感覚的に妥当な一致が必要です。
出題の実装ではミニマップにはほとんど何も表示されないか、ほんの一瞬(たいていのゲームではそれは 16 ms や 33 ms くらいの視認できるかどうかわからないくらいに)だけ、極稀に何か表示され…た気がする…、そんなような現実が発生し、ミニマップにまともに同じ高度の宇宙船が映らないというバグチケットが(おそらく)上がる事になるでしょう。
この系は float
で十分に表現可能という条件から、宇宙船の高度は10進数にして高々7桁に収まるよう扱われます。 73.1
km のように。
しかし、 float
の値としては、 例えソースコード上で 73.1f
と書いたとしても、 std::cout << 73.1f;
は 73.1
と表示されるとしても、実際の値は 73.099'998'474'121'093'750
相当となります。一般的な x64 アーキテクチャーやそれと互換の処理系での float
は IEEE754/binary32 なので。
これはもちろん FVector
の x
, y
, z
それぞれについても、また operator-
の結果も、 Length
の計算の乗算や加算や std::sqrt
の結果も全てが計算機にとってはこのバイナリー表現の世界が現実となります。
よって、 例え宇宙船Aと宇宙船Bの高度がプリントエフデバッグやログ出力などで確認すると同じ 73.1
と出力される状況でも、 IsSameAltitude
は false
を返す意図せず多発、というよりほとんどの状況となる。
float x = 73.1f; float y = 73.1f + 4.0e-6f; std::cout << x << '\n' << y << '\n' << std::fixed << std::setprecision(32) << std::bitset< 32 >( *(int32*)&x ) << ' ' << x << '\n' << std::bitset< 32 >( *(int32*)&y ) << ' ' << y << '\n' ;
1.41421 73.1 73.1 01000010100100100011001100110011 73.09999847412109375000000000000000 01000010100100100011001100110100 73.10000610351562500000000000000000
この点を意識せずにデバッグすると原因の特定に少しだけ手間取るかもしれない。あるいは知識として把握していても、うっかり ==
で float
の比較を書いてしまい予期しないバグを発生させてしまう事は新人や極度に疲弊した状態のプログラマーにはしばしば生じる事があるし、コンパイラーもこの意図は汲んでエラーや警告を出してくれるほど今のところは親切でないし、一般的な処理系の float
は IEEE754/binary32 のままです。
この問題の対応としては、 IsNearlyEqual
を用意し、また、 IsSameAltitude
には許容誤差を明示的に実引数として渡せるようにする事です:
constexpr float default_error_tolerance = 1.0e-3f; template < typename T > bool IsNearlyEqual( const T a, const T b, const T error_tolerance = (T)default_error_tolerance ) { static_assert( std::is_floating_point< T >::value, "" ); return std::abs( a - b ) <= error_tolerance; } bool IsSameAltitude ( const FVector& a , const FVector& b , const FVector& o , const float error_tolerance = default_error_tolerance ) { auto o_to_a = a - o; auto o_to_b = b - o; auto altitude_of_a = o_to_a.Length(); auto altitude_of_b = o_to_b.Length(); return IsNearlyEqual( altitude_of_a, altitude_of_b, error_tolerance ); }
Q4. 遅すぎた 2 の指数 [ 1, 2, 4, 8, 16, 32, .. ]
の判定
template < typename T > bool IsPowerOfTwo( const T in ) { static_assert( std::is_integral< T >::value, "" ); return in == 0 ? false : ( ( in & ( in - 1 ) ) == 0 ); }
この実装は執筆時の wandbox で出題時と同様に簡易的に計測したところ Optimization=OFF で 5,041,452 #/sec (出題実装比 1.89 倍高速)、 Optimization=ON で 8,007,129 #/sec (出題実装比 1.94 倍高速)で動作した。
Q5. 正弦と余弦も速くしたい
象限を判定し最も精度良く計算可能な低角度域へシフトしつつマクローリン展開を必要な有効数字が十分に得られる程度計算する。
#define PI 3.14159265358979f std::tuple< float, float > SinCosB( const float angle_in_radians ) { float quotient = angle_in_radians * 0.5f / PI; if ( angle_in_radians >= 0.0f ) quotient = (float)( (int)( quotient + 0.5f ) ); else quotient = (float)( (int)( quotient - 0.5f ) ); float a = angle_in_radians - quotient * 2.0 * PI; float s = 0.0f; if ( a > 0.5f * PI ) { a = PI - a; s = -1.0f; } else if ( a < -0.5f * PI ) { a = -PI - a; s = -1.0f; } else s = +1.0f; float p = a * a; // [ 1 / 2!, 1 / 3!, 1 / 4!, .., 1 / 11! ] constexpr float f2 = 1.0f / 2.0f; constexpr float f3 = f2 / 3.0f; constexpr float f4 = f3 / 4.0f; constexpr float f5 = f4 / 5.0f; constexpr float f6 = f5 / 6.0f; constexpr float f7 = f6 / 7.0f; constexpr float f8 = f7 / 8.0f; constexpr float f9 = f8 / 9.0f; constexpr float f10 = f9 / 10.0f; constexpr float f11 = f10 / 11.0f; return std::make_tuple ( a * ( +1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 * p * ( -f11 ) ) ) ) ) ) , s * ( 1.0f + p * ( -f2 + p * ( +f4 + p * ( -f6 + p * ( +f8 + p * ( -f10 ) ) ) ) ) ) ); }
この実装は執筆時の wandbox で出題時と同様に簡易的に計測したところ Optimization=OFF で 2,898,145 #/sec (出題実装比 1.06 倍高速)、 Optimization=ON で 5,914,322 #/sec (出題実装比 1.45 倍高速)で動作した。
(追記: 2018-12-12)
↑めでたしめでたし、と思われたが、実は↑の「こたえ」のコードにも新たなバグが埋め込まれてしまっていました。
CPP-QUIZ Q5 のこたえ、マクローリン展開による sin/cos の高速化に潜んでいた大きな誤差を生じるバグ - C++ ときどき ごはん、わりとてぃーぶれいく☆
と、いうわけで、改めまして、正しくは、
#define PI 3.14159265358979f std::tuple< float, float > SinCosB( const float angle_in_radians ) { float quotient = angle_in_radians * 0.5f / PI; if ( angle_in_radians >= 0.0f ) quotient = (float)( (int)( quotient + 0.5f ) ); else quotient = (float)( (int)( quotient - 0.5f ) ); float a = angle_in_radians - quotient * 2.0 * PI; float s = 0.0f; if ( a > 0.5f * PI ) { a = PI - a; s = -1.0f; } else if ( a < -0.5f * PI ) { a = -PI - a; s = -1.0f; } else s = +1.0f; float p = a * a; // [ 1 / 2!, 1 / 3!, 1 / 4!, .., 1 / 11! ] constexpr float f2 = 1.0f / 2.0f; constexpr float f3 = f2 / 3.0f; constexpr float f4 = f3 / 4.0f; constexpr float f5 = f4 / 5.0f; constexpr float f6 = f5 / 6.0f; constexpr float f7 = f6 / 7.0f; constexpr float f8 = f7 / 8.0f; constexpr float f9 = f8 / 9.0f; constexpr float f10 = f9 / 10.0f; constexpr float f11 = f10 / 11.0f; return std::make_tuple ( a * ( +1.0f + p * ( -f3 + p * ( +f5 + p * ( -f7 + p * ( +f9 + p * ( -f11 ) ) ) ) ) ) , s * ( 1.0f + p * ( -f2 + p * ( +f4 + p * ( -f6 + p * ( +f8 + p * ( -f10 ) ) ) ) ) ) ); }
となります😃 こちらも @nekketsuuu さんからのご指摘が基でバグを発見できました。ありがとうございます😃
Q6. ニアリーイコール、再び
出題の実装には2つの問題が潜んでいる。
a=π/2
(=90°
),b=2π+π/2
(=360°+90°=450°
) に対してreturn
がfalse
となるが、一般的には何周多く回っても結果的に示す角度は等価なため、本来はtrue
が返ると期待される。a=0
(=0°
),b=2π - 1.0e-4f
(=360°-1.0e-4=359.99
)は標準の許容誤差e=1.0e-3f
範囲内の角度で周回もしていないがabs(a-b)
はabs( 0 - (2π-1.0e-4f) )
となり、-2π+1.0e-4f < e
はfalse
と評価されてしまう。本来はtrue
が返ると期待される。
対応として、 a
と b
の角度の差を正しく評価する FindDeltaAngleRadians
を定義する:
#define PI 3.14159265358979f /// 弧度法単位の角度の差を計算 /// @param a angle of A in radians /// @param b angle of B in radians /// @return a と b の角度の差 float FindDeltaAngleInRadians( const float a, const float b ) { auto d = b - a; if ( ! std::isfinite( d ) ) return d; while ( d > PI ) d -= 2 * PI; while ( d < -PI ) d += 2 * PI; return d; }
それから、 IsNearlyEqualAngleInRadians
の角度の差の計算に FindDeltaAngleInRadians
を用いる:
bool IsNearlyEqualAngleInRadians( const float a, const float b, const float e = 1.0e-3f ) { return std::abs( FindDeltaAngleInRadians( a, b ) ) < e; }
(2017-12-06 追記)
クイズの作成時に題意としては考慮していませんでしたが、後日「こんな答えも含まれるのでは?」と鋭いご指摘を頂き、確かにこのクイズの他の設問で扱っているような出題と答えからするとそれもクイズの答えの1つとして記述した方が自然と私も思い、頂いたご指摘の Tweet を引用する形で追記・紹介いたします😃
お、鋭いですねー😃 楽しんで頂けて嬉しいです。さて、ご指摘の件は今回クイズとしてQ6を用意した時点では出題者の意図としては考慮しませんでしたが、このクイズの他の設問からはこれも答え編で触れた方が自然ですね。Tweet引用で内挿しておきます。ありがとうございます。
— Usagi Ito (@USAGI_WRP) 2017年12月5日
ぴんと来ない方も Tweet に添付して頂いた wandbox で状況を確認できるコード を見ると、たいへん分かり易いと思います。ありがとうございます。
Q7. 人間が読みやすい "数値" にしたかった
出題の実装では次の3つの問題が発生する。
int32
型の値は999,999,999
を超える値も取り得る。例えば1234567890
をこの関数の実引数として与えると、出力は1234,567,890
となり期待される桁区切りが足りずに、奇妙で読み難い、意図しない出力が得られる。int32
型の値は負の値を取り得る。例えば-123456789
を与えると 出力は-123456789
となり桁区切りがまったく行われず、意図しない出力が得られる。- 世界は広い。日本やアメリカ合衆国の文化圏では数値の桁区切りが3桁ごとに
,
(カンマ)で行われる慣例が一般化しているが、例えばドイツやイタリアでは.
(ドット)を桁区切りに使い、,
を小数点に使う。フランスやロシアでは(空白文字)を桁区切りに使い、
,
を小数点に使う。スイスでは'
(シングルクォート)を桁区切りに使い、,
を小数点に使う。int32
は整数型なので小数点の考慮は必要無いが、少なくとも製品が使用される文化圏を限定できない場合には桁区切り文字は,
とは限らない事を考慮する必要がある。
(1) int32
型の値は [ -2,147,483,648 .. +2,147,483,647 ]
を取り得るので、桁区切りは3つ目まで考慮した実装とする必要がある。
// 出題の実装に倣うなら、3回目の桁区切りを追加すれば // (1) については意図通りの結果が得られるようになる。 if ( in > 999'999'999 ) { out = ',' + buffer.substr( buffer.size() - 3, 3 ) + out; buffer.resize( buffer.size() - 3 ); }
(2) は負の値も桁区切りの判定を正常に行えるよう修正する必要がある。
// 出題の実装を最小限の改修で済ませるならば if の条件に負の値の判定も || で盛り込めば // (2) については意図通りの結果が得られるようになる。 if ( in > 999 || in < -999 )
(3) は ,
を任意の文字へ変更可能に対応する。
std::string FormatIntToHumanReadable( const int32 in, const char splitter = ',' ) { // 中略 // out = splitter + buffer.substr( buffer.size() - 3, 3 ); // 後略 //
(1), (2), (3) の修正を加え、ついでに if
の3連続を while
1つに整理すると次の実装となる:
std::string FormatIntToHumanReadable( const int32 in, const char splitter = ',' ) { auto buffer = std::to_string( in ); std::string out; while ( buffer.size() > 3 && buffer[ buffer.size() - 4 ] != '-' ) { out = splitter + buffer.substr( buffer.size() - 3, 3 ) + out; buffer.resize( buffer.size() - 3 ); } out = buffer + out; return out; }
なお、このコードは "文字列処理" としては分かり易い側面もありますが、 C++ の実装としてはやや "暢気" な実装です。また、そもそも std::ostream
へロケールを設定して <<
するだけでよしなに桁区切りしてくれるとか、そういった指摘もあるかと思います。それらは出題の「この実装ではどのような問題が起こり得るだろうか」の題意からは外れますので、興味に応じてエクストラにお楽しみになられたら良いと思います。
Q8. 整数型、再びトラブる
同じ原因に起因するバグが2箇所に潜んでいる。
1 つは、 float intensity = std::abs( in )
の結果は常に正の数とはならない。例えば、 T=int32
, in=std::numeric_limits<int32>::min()
が関数へ与えられた場合、 intensity
は -2147483648.0f
となる。 std::abs( in )
は int
型以上のビット幅の符号付きの整数型に対して、その最小値が与えられた場合、プログラマーが数学的に意図する絶対値は返せず、結果的には最小値がそのまま返ってくる。これを float
型へ変換したものが intensity
へ保持され、結果、負の値を T
型の最大値である正の値で除算した値が return
され、意図しない -1
が得られてしまう事になる。
この関数の仕様は return
が [ 0 .. 1 ]
なので、 -1
が返る想定をしない何らかの処理がとんでもない動作を引き起こす可能性がある。
もう 1 つは、 intensity / std::numeric_limits< T >::max()
の結果が T
型の最大値と最小値の数学的な意味での絶対値が 1
だけ違うために 0
を中心として符号を除去した intensity
を T
型の最大値で除算してしまうと、元の値が負の値だった場合に意図した意図した値域の写像とならない。例えば、 T=int8
, in=-128
の場合、本来は 1.0f
が期待されるが、この出題の実装では 1.007874011993408203125f
となる。
この関数の実装上の問題は、以上の2点が起こり得る事。何れも符号付きの整数型の最小値に起因する。
また、この関数の仕様についても1点明確ではない問題が潜んでいる。 T
が符号付き整数型の場合に、負の強度の最大値は T
型の 最小値
として T
型の値の正、負それぞれの全域を強度とするのか、負の値については 最小値 + 1
までを正常値として正、負の値は符号が異なっても同じ強度とするのかが明確でない。
// 正は正、負は負でそれぞれの数学的な意味での絶対値が最大の値を最大の強度とする場合の修正例 template < typename T > float GetIntensityToUNormFloat( const T in ) { return in >= 0 ? in / (float)std::numeric_limits< T >::max() : in / (float)std::numeric_limits< T >::min() ; }
// 正も負も数学的な意味での絶対値が同じ値は同じ強度とし、 // 符号付き整数型の最小値は不正値とする場合の修正例 template < typename T > float GetIntensityToUNormFloat( const T in ) { check( std::is_unsigned< T >::value || in > std::numeric_limits< T >::min() ); return std::abs( in / (float)std::numeric_limits< T >::max() ); }
Q9. G.C.D.
5 つの実装例と簡易的な速度評価結果を紹介します。
(1) はじめに Unreal Engine 4.18 の実装を出題に併せて調整した実装:
template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) while ( b != 0 ) { T t = b; b = a % b; a = t; } return a; }
(2) while タイプの亜種:
template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) if ( b ) while ( ( a %= b ) && ( b %= a ) ); return ( a + b ); }
(3) std::function recursive タイプ:
template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) std::function< T( T, T ) > f; f = [&f]( auto a, auto b ){ return ( b != 0 ) ? f( b, a % b ) : a; }; return f( a, b ); }
(4) function recursive タイプ:
template < typename T > T GreatestCommonDivisor_Impl( T a, T b ) { return ( b != 0 ) ? GreatestCommonDivisor_Impl( b, a % b ) : a; } template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) return GreatestCommonDivisor_Impl( a, b ); }
(5) C++17er:
template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) // std::gcd がツールチェインごとにどのようなソースで実装されているかはさておき return std::gcd( a, b ); }
それぞれの簡易的な実行速度評価結果:
実装パターン | Optimization | #/sec |
---|---|---|
(1) UE4 while | OFF | 1,137,116 |
(1) UE4 while | ON | 1,495,536 |
(2) while 亜種 | OFF | 1,121,166 |
(2) while 亜種 | ON | 1,503,565 |
(3) std::function recursive | OFF | 313,537 |
(3) std::function recursive | ON | 1,236,848 |
(4) function recursive | OFF | 954,043 |
(4) function recursive | ON | 1,534,016 |
(5) C++17er | OFF | 960,103 |
(5) C++17er | ON | 1,481,497 |
Q10. L.C.M.
2 つの実装例と簡易的な速度評価結果を紹介します。
(1) はじめに Unreal Engine 4.18 の実装を出題に併せて調整した実装:
template < typename T > T LeastCommonMultiplier( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) // Q9 G.C.D. の実装を使う T gcd = GreatestCommonDivisor( a, b ); return gcd == 0 ? 0 : ( a / gcd ) * b; }
(2) C++17er:
template < typename T > T LeastCommonMultiplier( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) // std::gcd がツールチェインごとにどのようなソースで実装されているかはさておき return std::lcd( a, b ); }
実装パターン | Optimization | #/sec |
---|---|---|
(1) UE4 a / GCD * b | OFF | 1,126,620 |
(1) UE4 a / GCD * b | ON | 1,456,371 |
(2) C++17er | OFF | 889,107 |
(2) C++17er | ON | 1,418,852 |
おまけ: G.C.D. & L.C.M. の wandbox での簡易的な実行速度評価に使用したコード
G.C.D., L.C.M. 以外でも概ねは同様のコードで簡易的な実行速度評価を行っています。
#include <iostream> #include <limits> #include <type_traits> #include <chrono> #include <random> #include <array> #include <algorithm> #include <cassert> #define check(X) { assert(X); } using int8 = std::int8_t; using uint8 = std::uint8_t; using int16 = std::int16_t; using uint16 = std::uint16_t; using int32 = std::int32_t; using uint32 = std::uint32_t; using int64 = std::int64_t; using uint64 = std::uint64_t; // Q9 のこたえを入れた header file #include "GCD.h" // Q10 のこたえを入れた header file #include "LCM.h" int main() { std::array< uint64, 1024 > numbers; std::mt19937_64 rng( 0 ); std::generate( numbers.begin(), numbers.end(), rng ); double x = 0.0f; size_t count = 0; auto ta = std::chrono::steady_clock::now(); while ( std::chrono::steady_clock::now() <= ta + std::chrono::seconds( 10 ) ) { auto a = numbers[ count % numbers.size() ]; auto b = numbers[ ++count % numbers.size() ]; x += GreatestCommonDivisor( a, b ); //x += LeastCommonMultiplier( a, b ); } std::cout << ( count / 10 ) << ' ' << x << '\n'; }
CPP-QUIZ from Unreal 2017 ( part-1/2; 出題編 )
0. はじめに
この記事は C++ Advent Calendar 2017 の DAY 1 に寄稿したものです。😃
初日は楽しい記事が良いだろうと考え、クイズ形式で C++ コードを楽しむコンセプトで記事を書くことにしました。今年は UE4 の C++ 実装を眺める時間が業務でも多くなりましたので、 Unreal Engine のソースコードを基に C++ の Tips などをクイズ形式で紹介します。
Unreal Engine について
Unreal Engine は Epic Games が現在は OSS として github で公開しながら開発を続けている C++ の実装を基にしたゲームエンジン、フレームワークです。執筆現在の現行版は 4.18 です。この記事は 4.18 のソースコードを基に執筆します。
- https://github.com/EpicGames/UnrealEngine/tree/4.18
- Unreal Engine としてのソースコード
- 最終製品向けではないエンジンの開発用コードなども含む
- https://github.com/EpicGames/UnrealEngine/tree/4.18/Engine/Source/Runtime
- Unreal Engine を使用する開発者が製品用に使用するライブラリーのソースコードはこの辺り
実行環境について
Wandbox にて gcc-7.2.0 を Warning=ON, Optimization=OFF, Verbose=OFF, Don't Use Boost, Sprout=OFF, MessagePack=OFF, C++14, -pedantic-errors, Compiler options="" 設定を基本の実行環境とします。
また、この記事の内容は一般的な x64 アーキテクチャーの PC や互換性のある処理系を対象の範囲とします。これは、例えば float
は IEEE754/binary32 を前提とする事を意味します。
出題について
Unreal Engine の実装を基に問題を作成していますが、 Unreal Engine には依存しない C++ の問題となるよう調整しています。また、本記事の問題はあくまでも QUIZ として楽しめるよう工夫したもので、問題に登場するバグが実際の Unreal Engine のソースコードでバグとして対処されないまま埋め込まれているという事ではありません。
また、問題は C++ のコードを例示して出題していますが、 C++ の言語仕様に起因するクイズを中心とした C++ Quiz とはことなり、本記事ではもっと緩く C++ 以外でも起こり得る問題を広く扱います。
非常勤講師をしていた頃を思い出して、学生へ講座のはじまりに遊びを兼ねて出題するような気持ちで作りますので、 C++ 初心者、学生さんに楽しんで貰えれば嬉しく思います。
記法について
- 文章中で
template
仮引数を一般に詳細に明記せずとも意図が通じると思われる範囲内でstd::vector<T>
のように略記する事があります。 - 文章中で値域を
[ 0.0 .. 1.0 ]
のように表記する場合があります。端の括弧は端の値も含まれる場合には[
または]
を用い、 端の値が含まれない場合には(
または)
を用います。また、値の間に..
があれば明示された値で挟まれた任意の値を取り得る事を意味し、 2つ以上数値が,
で区切られて続いていれば,
で区切られた間隔で連続する事を意味し、..
の前後に具体的な数値が無い場合には文脈上の最小値または最大値、もしくは理論上の-∞
または+∞
へ続く意図です。 - 文章中では変数の値などの数値に
,
や+
を加えて読みやすく表現する場合があります。 - 文章中では C++ コードとしてではなく一般的な数式の表現として
a=2π+π/2
のような表現をする場合があります。 uint8
,int32
など UE4 で定義される型で、一般にその型名から定義が明白と考えられる型については特に解説なく用います。- ソースコード中に UE4 由来の
check()
マクロが登場する事があります。これは<cassert>
で使用可能となるassert()
マクロを#define check(X) { assert(X); }
と薄くラップしたものと同様と考えて下さい。簡易的な試験として括弧の中の式が実行時に評価され、false
の場合にはプログラムの動作が停止し問題が検出される一般的なアサーションとして用います。
こたえについて
答えが見えてしまうとクイズとして楽しみ難い方も多いと思います。そこで、クイズに対する著者が想定したこたえはこの記事に続けて投稿する別の記事として掲載し、この記事のおわりからリンクする事にします。
また、「ぐぐ」れば答えがすぐに見つかる問題もありますが、「クイズ」を楽しみたい方は答えがすぐに思い浮かばない問題にも、しばらくは自分の既存の知識と思考、それから wandbox でコード片を試すなどしてできるだけ答えを "現時点での自力" で見出そうとするとすぐにググってしまうよりも楽しく、また分からなかった問題についても知識として身につく事も増えるかもしれません。
もくじ
float
型の[ 0.0 .. 1.0 ]
の値をuint8
型の[ 0 .. 256 )
へ写像したかった- 意図しない整数のオーバーフローと問題の遮蔽
- 浮動小数点数型
- 遅すぎた 2 の指数
[ 1, 2, 4, 8, 16, 32, .. ]
の判定 - 正弦と余弦も速くしたい
- ニアリーイコール、再び
- 人間が読みやすい "数値" にしたかった
- 整数型、再びトラブる
- G.C.D.
- L.C.M.
1. CPP-QUIZ from Unreal
Q1. float
型の [ 0.0 .. 1.0 ]
の値を uint8
型の [ 0 .. 256 )
へ写像したかった
次の実装にはバグが潜んでいる。どのようなバグか。
/// float 型の [ 0.0 .. 1.0 ] の値を uint8 型の [ 0 .. 256 ) へ写像 /// @in [ 0.0 .. 1.0 ] /// @out [ 0 .. 256 ) uint8 Quantize8UnsignedByte( float in ) { int out = in * 255; check( out >= 0 ); check( out <= 255 ); return out; }
Hint: この方法で写像されて得られる uint8
型の 0
と 255
は巾筒な "幅" に写像されたものだろうか。
Q2. 意図しない整数のオーバーフローと問題の遮蔽
次の実装にはバグが潜んでいる。どのようなバグか。
/// [ 0 .. 1 ] の ratio を [ a .. b ] へ写像 /// @tparam TV a, b, return の型 /// @tparam TR ratio の型 /// @param ratio [ 0 .. 1 ] /// @param return [ a .. b ] template < typename TV, typename TR > TV Lerp( const TV a, const TV b, const TR ratio ) { static_assert( std::is_arithmetic_v< TV > , "" ) static_assert( std::is_arithmetic_v< TR > , "" ) check( ratio >= 0 ); check( ratio <= 1 ); return (TV)( a + ratio * ( b - a ) ); }
Hint: TV
が整数型の場合も意図した結果を得られるだろうか? uint8
, int8
, uint16
, int16
, ...
Q3. 浮動小数点数型
宇宙に浮かぶ天体としては小ぶりだがそこそこの重力のある人工の惑星の上を宇宙船が飛ぶゲームを作っているとする。この天体は十分に小さく、考慮すべき宇宙空間は float
型で問題無く扱える範囲内とする。
同じ高度に居る宇宙船同士はミニマップ(画面上に表示される小さな地図)に表示したい。そこで次の実装を行ったとする。
恐らくこれは前提のストーリーを考慮すれば意図通りの動作をせずバグチケットが上がる事になる。どのような問題が発生するだろうか。
/// ある座標を中心とした球面軌道上に浮かぶ2つの物体が同じ高度に居るか判定 /// @param a 位置 o を中心とする系に浮かぶ宇宙船 A の位置 /// @param b 位置 o を中心とする系に浮かぶ宇宙船 B の位置 /// @param o a, b が中心とする人工惑星 O の中心位置 /// @return true: 同じ高度にいる bool IsSameAltitude( const FVector& a, const FVector& b, const FVector& o ) { auto o_to_a = a - o; auto o_to_b = b - o; auto altitude_of_a = o_to_a.Length(); auto altitude_of_b = o_to_b.Length(); return altitude_of_a == altitude_of_b; }
但し、 FVector
型は次の通り:
// Note: 実際の UE4 の FVector 型はもっと多機能だが // さしあたり出題に必要最小限を定義する。 /// 3 次元の直交座標表現用のベクター型 struct FVector { float x = 0, y = 0, z = 0; float LengthSquared() const { return x * x + y * y + z * z; } FVector operator-( const FVector& t ) const { return FVector{ x - t.x, y - t.y, z - t.z }; } };
Hint: IEEE754/binary32
Q4. 遅すぎた 2 の指数 [ 1, 2, 4, 8, 16, 32, .. ]
の判定
次の実装が遅すぎて悲しみを覚えた。もっと高速に処理するためにはどのような実装を施せば良いだろうか。
/// 与えられた整数型の値が 2 の指数か判定する /// @tparam T 整数型 /// @param in 判定対象の値 /// @return true: 与えられた整数型の値は 2 の指数である template < typename T > bool IsPowerOfTwo( const T in ) { static_assert( std::is_integral< T >::value, "" ); return std::pow( (T)2, (T)std::log2( in ) ) == in; }
なお、執筆時点の wandbox で簡易的に測定したところ、この実装は、 Optimization=OFF で 2,659,708 #/sec 、 Optimization=ON で 4,122,557 #/sec 程度で動作した。
Note: この記事はクイズであり、バグ探しではないのでこういう出題もある。
Q5. 正弦と余弦も速くしたい
もっと速くしたい。実用上、正弦と余弦として理論的に正しく10進数で6桁程度の精度があれば std::sin
, std::cos
と厳密に数値が一致しなくて構わない。どうにかならないだろうか。
/// 正弦と余弦を取得 /// @param angle_in_radians 弧度法単位の角度 /// @return {正弦、余弦} std::tuple< float, float > SinCos( const float angle_in_radians ) { return std::make_tuple( std::sin( angle_in_radians ), std::cos( angle_in_radians ) ); }
なお、執筆時点の wandbox で簡易的に測定したところ、この実装は、 Optimization=OFF で 2,726,719 #/sec 、 Optimization=ON で 4,148,229 #/sec 程度で動作した。
Hint: 正弦、余弦の数学的特性
Q6. ニアリーイコール、再び
次の実装にはバグが潜んでいる。どのようなバグか。
/// 弧度法単位の角度の差が許容誤差以下か判定 /// @param a angle of A in radians /// @param b angle of B in radians /// @param e error_tolerance; default = 1.0e-3f /// @return true: a と b の角度の差は許容誤差以下である bool IsNearlyEqualAngleInRadians( const float a, const float b, const float e = 1.0e-3f ) { return std::abs( a - b ) < e; }
Hint: 問題が簡単過ぎて目が回ってきた。
Q7. 人間が読みやすい "数値" にしたかった
次のパワフルな実装を新人くんがしてくれた、とする。先輩であるあなたにはこのコードのマージを躊躇う理由が思い当たる。この実装ではどのような問題が起こり得るだろうか。
/// int32 型の整数値を人間が読みやすい桁区切りした文字列へ変換 /// @param in 数値 /// @return 数値を桁区切した文字列 std::string FormatIntToHumanReadable( const int32 in ) { auto buffer = std::to_string( in ); std::string out; if ( in > 999 ) { out = ',' + buffer.substr( buffer.size() - 3, 3 ); buffer.resize( buffer.size() - 3 ); } if ( in > 999999 ) { out = ',' + buffer.substr( buffer.size() - 3, 3 ) + out; buffer.resize( buffer.size() - 3 ); } out = buffer + out; return out; }
実装してくれた新人くんの PC の画面を見たらこのコードの実装を試験したであろうコード片が見えた:
int main() { std::cout << FormatIntToHumanReadable( 123456789 ) << "\n"; }
123,456,789
Hint: 実装が拙い、遅い、などはさておき、もっと致命的な問題が少なくとも2つ、あるいは3つは発生する可能性がある。
Q8. 整数型、再びトラブる
次の実装にはバグが潜んでいる。どのようなバグか。
/// 整数型の絶対値を強度として float 型の unorm 値へ変換 /// @tparam T 整数型 /// @param in 入力値 /// @return [ 0 .. 1 ] の強度値 template < typename T > float GetIntensityToUNormFloat( const T in ) { float intensity = std::abs( in ); return intensity / std::numeric_limits< T >::max(); }
Hint: 関数の各行に少なくとも1つ以上の問題が潜んでいる。
Q9. G.C.D.
新人くんが唸っている。助けてあげよう。
/// 最大公約数 /// @param a >= 0 の整数 /// @param b >= 0 の整数 /// @return a, b の最大公約数 template < typename T > T GreatestCommonDivisor( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) // 整数型 `T` の2つの値 `a`, `b` の // 最大公約数(= "Greatest Common Divisor" )を計算するアルゴリズムを実装したい // 但し、できるだけ高速に動作させたい }
Q10. L.C.M.
また、新人くんが唸っている。助けてあげよう。
/// 最小公倍数 /// @param a >= 0 の整数 /// @param b >= 0 の整数 /// @return a, b の最小公倍数 /// @notice 結果のオーバーフローは考慮しない template < typename T > T LeastCommonMultiplier( T a, T b ) { static_assert( std::is_integral< T >::value, "" ); check( a >= 0 ) check( b >= 0 ) // 整数型 `T` の2つの値 `a`, `b` の // 最小公倍数(= "Least Common Multiplier" )を計算するアルゴリズムを実装したい // 但し、できるだけ高速に動作させたい }
答え
Real Unreal Engine C++ 2017-12 (part-5/5)
(はてなブログの記事あたりの容量制限のため前の部分 §1.10. までは前の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-4/5) - C++ ときどき ごはん、わりとてぃーぶれいく☆)
1.11. UE4/C++ における入門的な「スレッド」ライブラリーの std 互換性
std | UE4 |
---|---|
promise |
TPromise |
future |
TFuture |
thread |
FRunnableThread |
PRIMISE-FUTURE-THERAD の std と UE4 の 比較は以前書いた記事がちょうどこのセクションに相当する。
本節ではこれを基にそれぞれを項として整理する。繋げるとどうなるかは上記の記事を参照されたい。
1.11.1. TPromise<T>
と std::promise<T>
promise
の扱いについては std と主な使い方についてはメンバー関数名も snake か Camel か程度の違いなので std 慣れした C++er が UE4 版の実装を使おうとして混乱する事は無い。
// std #include <future> void std_promise() { std::promise< int > p; auto f = p.get_future(); p.set_value( 123 ); } // UE4 #include "Runtime/Core/Public/Async/Future.h" void ue4_promise() { TPromise< int > p; auto f = p.GetFuture(); p.SetValue( 123 ); }
1.11.2. TFuture<T>
と std::future<T>
// std #include <future> void std_future( std::future< int >& f ) { // せっかくなので参考記事には無い処理完了をブロッキングせずに待つパターンの // 基礎的な実装要素を加えて紹介する。 if ( f.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready ) auto result = f.get(); } // UE4 #include "Runtime/Core/Public/Async/Future.h" void ue4_future( TFuture< int >& f ) { if ( f.IsReady() ) auto result = f.Get(); }
wait_for
, wait_until
ほど高機能ではないが、一般的に IsReady
だけでも事足りる。
1.11.3. FRunnableThread
と std::thread
// std void std_thread( std::promise< int >&& p ) { auto t = thread( [ =, p = move(p) ] () mutable { p.set_value( 123 ); } ); t.join(); // or detach() } // UE4 void ue4_thread( TPromise< int >&& p ) { struct r_type: public FRunnable { TPromise<int> p; r_type( TPromise<int>&& p_ ) : p( MoveTemp( p_ ) ) { } bool Init() override { return true; } uint32 Run() override { p.SetValue( 123 ); return 0; } void Stop() override { } } r( MoveTemp( p ) ); constexpr auto bAutoDeleteSelf = false; constexpr auto bAutoDeleteRunnable = true; auto t = FRunnableThread::Create ( &r , TEXT( "promise-future tester") , bAutoDeleteSelf , bAutoDeleteRunnable , 0 ); t.WaitForCompletion(); }
スレッドを生成する事だけを考えると UE4 の生スレッドの生成は少々面倒くさい。しかし、 FRunnableThread
は Suspend()
や Kill()
をメンバー関数として持っているので少しだけ高機能。
以上のようにして "野生のスレッド" を放つ事は UE4 のライブラリー機能としてもサポートされているが、実用上はそのような用途では std 実装を使った方が楽で future の待ち方にも工夫でき面倒も無くて良いだろう。もちろん、 std::promise
と FRunnableThread
を組み合わせても問題は無い。
UE4 では "野生のスレッド" ではなく、フレームワークが管理するメインスレッドとワーカースレッド群へ FAutoDeleteAsyncTask
を用いてフレームワークの制御下で非同期タスクを実行もできる。それについては以下の記事が参考になるだろう。FAsyncTask
や FAutoDelegateAsyncTask
についてはそちらを参照すると良いだろう。
1.12. UE4/C++ における「アトミック」ライブラリーの std 互換性
所属 | 型 | 要素型 | Header |
---|---|---|---|
std | atomic<T> |
<T> |
<atomic> |
UE4 | FThreadSafeBool |
bool |
Runtime/Core/Public/HAL/ThreadSafeBool.h |
UE4 | FThreadSafeCounter |
int32 |
Runtime/Core/Public/HAL/ThreadSafeCounter.h |
UE4 | FThreadSafeCounter64 |
int64 |
Runtime/Core/Public/HAL/ThreadSafeCounter64.h |
1.12.1. 主なメンバー関数の対応
std | UE4 | 効果 |
---|---|---|
Reset |
0 にする |
|
store operator= |
Set |
値を放り込む |
load |
値を取り出す | |
exchange |
値を取り替える | |
operator++ |
Increment |
1 増やす |
operator-- |
Decrement |
1 減らす |
fetch_addo perator+= |
Add |
任意の数を加算する |
fetch_sub operator-= |
Subtract |
任意の数を減算する |
fetch_and operator&= |
||
fetch_or `operator |
=` | | |
fetch_xor operator^= |
std
:UE4
:FThreadSafeBool
はFThreadSafeCounter
を継承して定義され、内部的にはint32
で0
と1
のみを扱うよう制限して実現している。FThreadSafeCounter
とFThreadSafeCounter64
は内部的にはFPlatformAtomics::InterlockedAdd
などに実装を依存している。int32
,int64
に対応しているので、一応実質的にはこれらでもuint16
やvoid*
も扱えないことは無い…
UE4 のアトミックを用いても UPROPERTY()
や UFUNCTION()
では使用できず特にメリットも無いので、ツールチェインの std
実装が信用できる場合は std
のアトミックを使えば良い。
参考:
- atomic - cpprefjp C++日本語リファレンス
- FThreadSafeBool | Unreal Engine
- FThreadSafeCounter | Unreal Engine
- FThreadSafeCounter64 | Unreal Engine
1.13. UE4/C++ における「並列処理」ライブラリーと std / OpenMP / Intel-TBB / Microsoft-PPL
所属 | 関数・プラグマ | Header |
---|---|---|
std | for_each |
<algorithm> <execution> |
OpenMP | #pragma omp parallel for |
|
Microsoft PPL | parallel_for |
<ppl.h> |
IntelTBB | parallel_for |
<tbb/parallel_for.h> |
UE4 | ParallelFor |
Runtime/Core/Public/Async/ParallelFor.h |
モダンな C++er は既に C++17 で <algorithm>
に <execution>
による並列処理機能が導入される事は熟知し、開発環境がこれに対応可能となる日を心待ちにしている。
とは言え、現状、現実には多くの C++er は古風でやや癖はあるものの簡単に使用できる OpenMP や、 Microsoft 環境向けだけであれば PPL 、 PC 向けのクロスプラットフォームであれば intel TBB 、 GCC だけであれば Parallel Mode など開発環境に合わせて並列化や並行処理のための機能を使用している。
UE4 でもそれらを使用する事は可能だが、 簡単なループ並列化については UE4 も ParallelFor
を用意している。本節では簡単で擬似的なソースコードで C++17 Parallelism, OpenMP, Intel TBB, Microsoft PPL について同等のコードを例示的に整理する。
1.13.1. ParallelFor
と for_each
( C++17 Parallelism ) #pragma omp parallel for
/ parallel_for
( TBB ) / parallel_for
( PPL )
// Note: このコードはほとんど実際に動きそうなコードを書いているが、擬似コードである。 // 実際には例示する example 群は同時に1つのソースコードではそのまま動作しない。 // それぞれの違いを理解しやすいよう1つにまとめたが、実行したい場合は // それぞれのライブラリーや環境に合わせて1つ1つ試すと良い。 #include <iostream> #include <cmath> #include <numeric> #include <vector> constexpr size_t n = 256; using value_type = double; using values_type = std::vector< value_type >; void process_value( const value_type v ) { std::cout << ( std::to_string( std::sqrt( v ) ) + '\n' ); } // std::for_each_n ( with C++17 Parallelism ) #include <algorithm> #include <execution> void example_std_for_each_cpp17_parallelism( const values_type& values ) { std::cout << "\n[ std::for_each with C++17 Parallelism ]\n"; // https://github.com/cplusplus/parallelism-ts // http://www.modernescpp.com/index.php/c-17-new-algorithm-of-the-standard-template-library // http://www.bfilipek.com/2017/08/cpp17-details-parallel.html std::for_each ( std::execution::par , values.begin() , values.end() , &process_value ); } void example_openmp( const values_type& values ) { std::cout << "\n[ OpenMP ]\n"; // official spec. http://www.openmp.org/specifications/ // ref 1 https://en.wikipedia.org/wiki/OpenMP // ref 2 http://bisqwit.iki.fi/story/howto/openmp/ // ref 3 (ja) http://tech.ckme.co.jp/openmp.shtml #pragma omp parallel for for ( auto i = values.cbegin(); i < values.cend(); ++i ) process_value( *i ); } #include <ppl.h> void example_microsoft_ppl( const values_type& values ) { std::cout << "\n[ Microsoft PPL ]\n"; // PPL https://msdn.microsoft.com/en-us/library/dd492418.aspx // parallel_for https://msdn.microsoft.com/en-us/library/dd728073.aspx parallel_for ( size_t( 0 ) , values.size() , [&]( auto index ) { process_value( values[ index ] ); } ); } #include <tbb/parallel_for.h> void example_intel_tbb( const values_type& values ) { std::cout << "\n[ Intel TBB ]\n"; // https://software.intel.com/en-us/node/506153 parallel_for ( size_t( 0 ) , values.size() , [&]( auto index ) { process_value( values[ index ] ); } ); } #include "Runtime/Core/Public/Async/ParallelFor.h" void example_ue4_parallel_for( const values_type& values ) { std::cout << "\n[ UE4 ParallelFor ]\n"; // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Async/ParallelFor/ ParallelFor ( values.size() , [&]( auto index ) { process_value( values[ index ] ); } ); } int main() { std::vector< double > values( n ); std::iota( p, p + n, 0 ); example_std_for_each_cpp17_parallelism( values ); example_openmp( values ); example_microsoft_ppl( values ); example_intel_tbb( values ); example_ue4_parallel_for( values ); }
UE4 は C++ で一般的に用いられる事の多い多くのライブラリー等に比べてより単純な ParallelFor
しか高レベル並列化機能を提供していないが、基本的には [ 0 .. N )
のレンジの並列化そのままで多くの場合には十分かつ必要に応じて少し与えるレンジやファンクターのキャプチャーに工夫をすればたいていの用途には簡単に応用できるため多くの場合には必要十分となる。
それでは必要不十分という場合には UE4 のタスク制御機能を応用したり、必要に応じてライブラリーを導入すると良い。また、恐らくそう遠く無く C++17 Parallelism 対応の開発環境も UE4 での開発環境一般としても、特に PC 向けであれば使用可能となるので、近い未来にはアルゴリズムについてはそちらも必要に応じて使用すると良い。
なお、 ParallelFor
には例では省略した最後の bool 型の引数が1つあり、デフォルトの false
では並列実行されるが、 true
を与えると逐次実行に動作が切り替わる機能もあり、これがデバッグに有用な事もしばしばあり便利である。
但し、 UE4 の ParallelFor
については扱われる index が int32
型である点に気をつける必要がある。 C++ 界の一般的な並行処理ライブラリーでは index は size_t
型である。
参考:
- C++ 標準の promise / future / thread に対応する UE4 標準の TPromise / TFuture / FRunnableThread の使い方 - C++ ときどき ごはん、わりとてぃーぶれいく☆
- future - cpprefjp C++日本語リファレンス
- thread - cpprefjp C++日本語リファレンス
- Using AsyncTasks - Epic Wiki
1.14. UE4/C++ における「ファンクター」ライブラリーと std 互換性
std::function
に相当する機能が UE4 ライブラリーにも実装されている。本節ではこれらについて std 慣れした C++er 向けに整理する。
1.14.1. TFunctionRef<T>
と std::function<T>
所属 | 関数 | Header |
---|---|---|
std | function |
<functional> |
UE4 | TFunction |
Runtime/Core/Public/Templates/Function.h |
UE4 | TFunctionRef |
Runtime/Core/Public/GenericPlatform/GenericPlatformProcess.h |
特徴 | std::function |
TFunction |
TFunctionRef |
---|---|---|---|
has operator= |
yes | yes | no |
has operator(bool) |
yes | yes | no |
has ownership of a functor | yes | yes | no |
a speed prioritized | no | no | yes |
ソースコードでの具体的な比較を以下に示す:
// ふつうの関数 -> ポインターは C++ ファンクターの一種 void something_function() { } void example() { using signature_type = void(); // ラムダエクスプレッション -> C++ ファンクターの一種 auto lambda = []{}; std::function< signature_type > f = lambda; if ( f ) // operator(bool) による呼び出し可能の確認に対応 f(); f = &something_function; // 代入によりファンクターの挿げ替えが可能 if ( f ) f(); // UE4: TFunction TFunction< signature_type > g = lambda; if ( g ) // operator(bool) による呼び出し可能の確認に対応 g(); g = &something_function; // 代入によりファンクターの挿げ替えが可能 if ( g ) g(); // UE4: TFunctionRef TFunctionRef< signature_type > h = lambda; h(); // とりあえず呼び出す事しかできないが、軽量な速度重視の実装になっている TFunctionRef< signature_type > h = lambda; }
std::function
互換相当の TFunction
については特に UE4 ならこちらを使うべきという程の強い動機はない。 TFunctionRef
は UE4 にしかない機能だが、ユーザーにとってはこちらもこれという程の使い所も無いだろう。
2. プロフェッショナル C++er のための UE4/C++ リアル・ベーシック
予め断っておかねばならない事がある。§1を書き終えた時点で11月23日も終わろうとしており、著者には時間的猶予が…無い。そこで§2は内容を大幅に予定よりも削減してしまった。
アドベンカレンダーにかぎらず、著者はこのブログで毎月数本の技術ブログ記事を執筆しているので、ここで書かれる予定だった記事も興味が失われない限りはいつか記事になると思う。さしあたり、気が向いたならブログを購読しておいて頂けると読者数のカウントが私にも見えて嬉しい。
2.1. UE4/C++ におけるガベージコレクターの操作
UE4 のガベージコレクターも UHT の力を借りるとは言えライブラリーとしての実装であり、 C++ コードからある程度は制御が可能になっている。本節ではありがちな要求について UE4/C++ での実現方法について整理する。
2.1.1. 任意の UObject*
の参照カウントを確認する
// o の参照カウントを取得 inline static int32 GetReferenceCount( UObject* o ) { check( o ) constexpr bool bInRequireDirectOuter = false; constexpr bool bInShouldIgnoreArchetype = true; constexpr bool bInSerializeRecursively = true; constexpr bool bInShouldIgnoreTransient = false; TArray< UObject* > refs; FReferenceFinder finder ( refs , o , bInRequireDirectOuter , bInShouldIgnoreArchetype , bInSerializeRecursively , bInShouldIgnoreTransient ); finder.FindReferences( o ); return refs.Num(); }
参考:
- FReferenceFinder | Unreal Engine
- [https://wiki.unrealengine.com/Garbage_Collection~Count_References_To_Any_Object:title]
2.1.2. 強制的にガベージコレクターを動作させる
GetWorld()->ForceGarbageCollection( true );
UWorld::ForceGarbageCollecion
は「強制的にガベージコレクターを動作させる」が、厳密な意味で「直ちに」ではない。 GC の監視タイマーは通常 60 秒ごとにガベージコレクトを行うが、このタイマーの残りを 0
にして、次回の(次フレームの前の) GC のタイマー判定タイミングでガベージコレクトを行わせる効果がある。
なお、当然ながら、この機能では参照カウントが有効なオブジェクトは削除されない。 UObject::ConditionalBeginDestroy
を呼びポインターを保持している UPROPERTY()
の値を nullptr
にしておけば削除される事になる。但し、 AActor
については ConditionBeginDestroy
の前に Destroy
を呼び忘れないよう注意されたい。
また、複数のオブジェクトから参照するが GC の参照カウントに影響する必要が無い場合は TWeakObjectPtr<T>
, TAutoWeakObjectPtr<T>
を用いておけば面倒が無い。
参考:
- TWeakObjectPtr | Unreal Engine
- TAutoWeakObjectPtr | Unreal Engine
- UWorld::ForceGarbageCollection | Unreal Engine
- https://wiki.unrealengine.com/Garbage_Collection_%26_Dynamic_Memory_Allocation#Counting_UPROPERTY.28.29_References_To_Any_Object
2.2. UE4/C++ における「相互排除と同期」ライブラリーと std 互換性
UE4 には 2 種類の相互排除と同期のためのライブラリー実装があるので std の同様の実装と使用例を挙げて整理する。
2.2.1. FCriticalSection
クリティカルセクション型と std::mutex
// std #include <mutex> { std::mutex m; // scoped lock; C++17 からは template 型が省略可能になる { std::lock_guard< decltype( m ) > lock( m ); } // manual operation m.lock(); m.unlock(); } // UE4 #include "Runtime/Core/Public/Misc/ScopeLock.h" { FCriticalSection m; // scoped lock { FScopeLock lock( &m ); } // manual operation m.Lock(); m.Unlock(); }
std 版では lock_guard
, adopt_lock
, unique_lock
など実装が UE4 よりも充実しているので、特別な事情が無く、 std 版を使用可能な場合は UE4/C++ でも std 版を使えば良い。
2.2.2. FRWLock
読み書きロック型と std::shared_mutex
既に C++17er となる準備のできている一般的な多くの C++er 、あるいは C++14er はプラットフォーム依存の API に頼らずとも shared_mutex
, shared_timed_mutex
を用いて Read / Write レベルのロック制御を実装している。
UE4 でもそれらに相当… "いちおう" は相当する FRWLock
がライブラリーで提供されている。
// std ( C++17 ) #include <mutex> { std::shared_mutex m; { std::shared_lock< decltype( m ) > read_lock( m ); } { std::lock_guard< decltype( m ) > write_lock( m ); } } // std ( C++14 ) #include <mutex> { std::shared_timed_mutex m; { std::shared_lock< decltype( m ) > read_lock( m ); } { std::lock_guard< decltype( m ) > write_lock( m ); } } // UE4 #include "Runtime/Core/Public/Misc/ScopeRWLock.h" { FRWLock m; // scoped lock { FRWScopeLock read_lock( m, FRWScopeLockType::SLT_ReadOnly ); } { FRWScopeLock write_lock( m, FRWScopeLockType::SLT_Write ); } // manual operation m.ReadLock(); m.ReadUnlock(); m.WriteLock(); m.WriteUnlock(); }
FRWLock
の挙動は次の通り:
- ReadLock -> ReadLock: 可能
- ReadLock した回数だけ ReadUnlock されるとロック状態が解除される
- ReadLock -> WriteLock: 解錠待ち(=ブロッキング、デッドロック注意)
- WriteLock -> WriteLock 解錠待ち(=ブロッキング、デッドロック注意)
- WriteLock -> ReadLock 解錠待ち(=ブロッキング、デッドロック注意)
- 過解錠の挙動はプラットフォーム依存(≃落ちる)
参考:
- shared_mutex - cpprefjp C++日本語リファレンス
- shared_timed_mutex - cpprefjp C++日本語リファレンス
- shared_lock - cpprefjp C++日本語リファレンス
- lock_guard - cpprefjp C++日本語リファレンス
- C++17, C++14, C++11 に併せた std::shared_mutex, std::shared_timed_mutex, std::mutex から mutex_type, read_lock_type, write_lock_type を扱う例 - Qiita
- https://github.com/EpicGames/UnrealEngine/blob/4.18/Engine/Source/Runtime/Core/Public/Windows/WindowsCriticalSection.h#L114
2.3. UE4/C++ における基礎的な「HTTP」ライブラリー
UE4 には HTTP Client のライブラリー実装があるので紹介する。
2.3.1. FHttpModule
そのまま紹介してもあまり面白くないので、少し工夫して実用的にラップした例を紹介する:
#pragma once #include "Runtime/Online/HTTP/Public/Http.h" #include "Runtime/Core/Public/Async/Future.h" #include "Async.h" namespace usagi { template < typename T > inline static TFuture< T > GetHTTP ( const FString& url , std::function< T ( FHttpRequestPtr, FHttpResponsePtr, bool ) > decoder = [] ( auto, auto, auto ) { return T(); } , const FString& method = TEXT( "GET" ) , const FString& user_agent = TEXT( "X-UE4-Agent" ) ) { TSharedPtr< TPromise< T > > p( new TPromise< T >() ); auto f = p->GetFuture(); auto http = &FHttpModule::Get(); auto request = http->CreateRequest(); request->OnProcessRequestComplete(). BindLambda ( [ = ] ( FHttpRequestPtr request , FHttpResponsePtr response , bool successful ) { UE_LOG( LogTemp, Log, TEXT( "result of http-request url: %s => %s" ), *url, successful ? TEXT( "Succeeded" ) : TEXT( "Failed" ) ); p->SetValue( decoder( request, response, successful ) ); } ); request->SetURL( url ); request->SetVerb( method ); request->SetHeader( TEXT( "User-Agent" ), user_agent ); request->ProcessRequest(); return f; } }
ここまで読み進んだ C++er ならこの程度のコードは難しくなく読めると思う。一応簡単に解説する。
- UE4 の HTTP Client 機能は 7 段階でリクエスト:
FHttpModule::Get
// Note: FHttpModule は singletonFHttpModule::CreateRequest
IHttpRequest::OnProcessRequestComplete
IHttpRequest::SetURL
IHttpRequest::SetVerb
IHttpRequest::SetHeader
IHttpRequest::ProcessRequest
- 実装上の工夫として
usagi::GetHTTP
では:- url と decoder を実引数で渡す
- decoder は Get 結果(テキストとは限らない)を望むオブジェクトへ変換するファンクター
- 関数自体は Future を返す
- 呼び出し元は
Future::IsReady
を確認してFuture::Get
する
- 呼び出し元は
- url と decoder を実引数で渡す
改良の余地はあるが、あまり複雑化しても FHttpModule
の紹介から遠ざかる(&わたしの執筆時間がもうない)こともあり、この程度の実装で紹介した。
C++er は大きなフレームワーク(Qt, POCO など)を使う場合で無ければ libcurl に頼るか boost::asio でソケットから組み立てるなど何れ何かと "めんどう" な HTTP 処理だが、 UE4(大きなフレームワークだが) では Client 処理についてはこのように簡単に扱えるようになっている。
なお、 HTTP Server を簡単に組み込みたい場合は Marketplace で有償販売されている UnrealWebServer など使うと実装の苦労が無く C++ に加えて Blueprint でも簡単に扱えて良い。
もちろん、必要に応じて boost::asio や nghttp2 などから組み込んでもよいだろう。特に blueprint 対応しなくて構わないならばそれほど "めんどう" でもない。
なお、 UE4 の HTTP ライブラリーはモジュール化されているため、 "{your-project-name}.Build.cs" の PublicDependencyModuleNames.AddRange
に , "Http"
を追加しておく必要がある点には注意されたい。
2.4. UE4/C++ における「JSON」ライブラリー
UE4 には JSON を扱うためのライブラリーが実装されている。少々癖があるが、 UE4 で JSON を扱うならば一度試す価値はある。
2.4.1. FJsonValue
抽象型
UE4 の JSON ライブラリーは、コード例を示す前に少し解説が必要と思われる。
全てはここから始まる。
FJsonValue
は抽象型なのでそのオブジェクトは存在しない。FJsonValue
から派生した-Array
,-Boolean
,-Null
,-Number
,-Object
,-String
の 6 つの型が具体的な JSON 値を保持するオブジェクトとして存在可能。TSharedPtr< FJsonValue > value = MakeSharable( new FJsonValueNull() );
の様に使う。
理屈としては以上で JSON のオブジェクトを UE4/C++ 上のオブジェクトとして表現する。加えて、 FJsonValue
と FJsonObject
は値をキャストして取得する機能があり、これによって具体的な値の取得も便利に可能となっている。
但し、 { "a": { "b": [ { "c": 123 } ] } }` に対して "a.b[0].c" のようにパスでアクセスする機能などハイカラなものは無いので、地味にこつこつオブジェクトの組み立てと戦いながら扱う必要がある。
また、 UE4 の JSON ライブラリーはモジュール化されているため、 "{your-project-name}.Build.cs" の PublicDependencyModuleNames.AddRange
に , "Json", "JsonUtilities"
を追加しておく必要がある点にも注意されたい。
2.4.2. UE4/C++ JSON ライブラリーによる読み書き
C++er には旧くは picojson や json11 などの JSON ライブラリーが、また最近では nlohman-json ライブラリーの利用も増えている。実際問題、 C++ のオブジェクトとして JSON を扱いたいだけならばそれらの方が扱いやすくて楽だが、本記事の趣旨の都合、 UE4 の JSON ライブラリーを使った JSON の読み書きを紹介する。
慎重な C++er は使い方を誤る事は無いと思うものの、 TSharedPtr
を介した、また値の取得についてはその const*
(スマートポインターの const ポインターを扱う設計なのだ…)を扱わなければならないので、少々の覚悟をしてから挑まないと混乱してしまう。
次の JSON を読んでログ出力する例を挙げる:
{ "aaa": 1.23 , "bbb": true , "ccc": "hoge" , "ddd": [ 1.23, true, "hoge" ] , "eee": { "ppp": "qqq" } }
// read a JSON /// @param in JSON string #include "Runtime/Json/Public/Serialization/JsonSerializer.h" void ue4_json_read( const FString in ) { // reader TSharedRef< TJsonReader<> > reader = TJsonReaderFactory<>::Create( *in ); // string --> a value of the root TSharedPtr< FJsonValue > root_value; if ( ! FJsonSerializer::Deserialize( reader, root_value ) || ! root_value.IsValid() ) return; // root: value --> root object const TSharedPtr< FJsonObject >* root_object; if ( ! root_value->TryGetObject( root_object ) ) return; // root: object --> a number value of the "aaa" field double value_of_aaa = 0; if ( ! (*root_object)->TryGetNumberField( TEXT( "aaa" ), value_of_aaa ) ) return; UE_LOG( LogTemp, Log, TEXT( "value_of_aaa=%f" ), value_of_aaa ) // root: object --> a bool value of the "bbb" field bool value_of_bbb = false; if ( ! (*root_object)->TryGetBoolField( TEXT( "bbb" ), value_of_bbb ) ) return; UE_LOG( LogTemp, Log, TEXT( "value_of_bbb=%s" ), ( value_of_bbb ? TEXT( "true" ) : TEXT( "false" ) ) ) // root: object --> a string value of the "ccc" field FString value_of_ccc; if ( ! (*root_object)->TryGetStringField( TEXT( "ccc" ), value_of_ccc ) ) return; UE_LOG( LogTemp, Log, TEXT( "value_of_ccc=%s" ), *value_of_ccc ) // root: object --> a array value of the "ddd" field const TArray< TSharedPtr< FJsonValue > >* value_of_ddd = 0; if ( ! (*root_object)->TryGetArrayField( TEXT( "ddd" ), value_of_ddd ) ) return; // foreach value_of_ddd ( array ) --> value --> { number | bool | string } for ( auto&& element_of_ddd : *value_of_ddd ) { double number_buffer = 0; bool bool_buffer = false; FString string_buffer; // value --> number if ( element_of_ddd->TryGetNumber( number_buffer ) ) UE_LOG( LogTemp, Log, TEXT( "number value in ddd=%f" ), number_buffer ) if ( element_of_ddd->TryGetBool( bool_buffer ) ) UE_LOG( LogTemp, Log, TEXT( "bool value in ddd=%f" ), bool_buffer ) if ( element_of_ddd->TryGetString( string_buffer ) ) UE_LOG( LogTemp, Log, TEXT( "string value in ddd=%f" ), *string_buffer ) } // root: object --> an object value of the "eee" field const TSharedPtr< FJsonObject >* value_of_eee; if ( ! (*root_object)->TryGetObjectField( TEXT( "eee" ), value_of_eee ) ) return; // Note: これまでの root object のように Field 指定して取得もできるが、 // TMap< FString, TSharedPtr< FJsonValue > > を直接操作もできる for ( auto&& pair_of_eee: (*value_of_eee)->Values ) { FString string_buffer; if ( pair_of_eee.Value->TryGetString( string_buffer ) ) UE_LOG( LogTemp, Log, TEXT( "string:string value in eee={%s:%s}" ), *pair_of_eee.Key, *string_buffer ) } }
扱っている対象が抽象型の Value なのか、あるいは Array または Object なのかを中心に、オブジェクトに対しては対象はポインターか、スマートポインターか、その中の何かか、気を使って組み立てる必要がある。幸い、型のエラーは翻訳時に検出されるが、コードの実装者は少々疲れることになるだろう。
さておき、次は同じ JSON オブジェクトを構築する例を示す:
// create a JSON /// @param in JSON string TSharedPtr< FJsonValue > ue4_json_create() { // Note: auto で受けたいが、 MakeShareable の return は // SharedPointerInternals::FRawPtrProxy< FJsonObject > で // そうなると operator-> も使えないのでだるい。 TSharedPtr< FJsonObject > root_object = MakeShareable( new FJsonObject ); root_object->SetNumberField( TEXT( "aaa" ), 1.23 ); root_object->SetBoolField( TEXT( "bbb" ), true ); root_object->SetStringField( TEXT( "ccc" ), TEXT( "hoge" ) ); TArray< TSharedPtr< FJsonValue > > ddd_array; ddd_array.Reserve( 3 ); ddd_array.Emplace( MakeShareable( new FJsonValueNumber( 1.23 ) ) ); ddd_array.Emplace( MakeShareable( new FJsonValueBoolean( true ) ) ); ddd_array.Emplace( MakeShareable( new FJsonValueString( TEXT( "hoge" ) ) ) ); // SetXXXField 系では右辺値参照はサポートされていない root_object->SetArrayField( TEXT( "ddd" ), ddd_array ); TSharedPtr< FJsonObject > eee_object = MakeShareable( new FJsonObject ); eee_object->SetStringField( TEXT( "ppp" ), TEXT( "qqq" ) ); root_object->SetObjectField( TEXT( "eee" ), eee_object ); return MakeShareable( new FJsonValueObject( root_object ) ); }
参考:
2.5. UE4/C++ における「ファイルシステム」ライブラリー
実行時にファイルシステムを覗きたい需要はままあるかと思う。一般に C++er は C++14er である現在まで長きに渡り filesystem について標準ライブラリーで十分には扱えない事に嘆いていた。しかし C++17er がその嘆きを過去のものにする日は既にそう遠くない。
UE4 にもファイルシステムライブラリーが実装されている。もう数日、あるいはもう少し、 UE4 の release ラインが公式に C++17 ビルドに対応するその日まで有用かもしれない簡単な例を掲載する事とした。 ".json" 拡張子を持つファイル群を特定のディレクトリーから実行時に根こそぎ読み出す UE4 のファイルシステムライブラリーでの実装例を紹介する。
2.5.1. UE4 のファイルシステムライブラリーで .json ファイルを根こそぎ読み出す
実装例兼参考リンク:
// UE4 { const FString my_json_dir = TEXT( "my_json_dir" ); TArray< FString > directories_to_skip; // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/HAL/FPlatformFileManager/ IPlatformFile& platform_file = FPlatformFileManager::Get().GetPlatformFile(); // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FLocalTimestampDirectoryVisitor/ FLocalTimestampDirectoryVisitor Visitor( platform_file, directories_to_skip, directories_to_skip, false ); // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FPaths/ // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FPaths/ProjectDir/index.html platform_file.IterateDirectory( *( FPaths::ProjectDir() / my_json_dir ), Visitor ); for ( TMap< FString, FDateTime >::TIterator TimestampIt( Visitor.FileTimes ); TimestampIt; ++TimestampIt ) { const FString file_path = TimestampIt.Key(); const FString file_name = FPaths::GetCleanFilename( file_path ); if ( FPaths::GetExtension( file_name, false ).Equals( TEXT( "json" ), ESearchCase::IgnoreCase) ) { FString json_text; if ( FFileHelper::LoadFileToString( json_text, *file_path, FFileHelper::EHashOptions::None ) ) // 前節で紹介した JSON の reader へ投げるなどお好み処理 ue4_json_read( json_text ); } } }
2.6. エンジンプラグインのリビルド
時折、 Marketplace で購入したエンジンプラグインのローカル環境での手修正、そして手リビルドをしたい場合があるかもしれない。私は最近もあった。
エンジン本体のローカル環境でのリビルドは公式にわかりやすい解説もあるし、 Linux でも Windows でも困らないが、エンジンプラグインのリビルドは方法を知らないとどうしたもんかと少々悩むかもしれないので最後に紹介する事とした。
2.6.1. エンジンプラグインのリビルド
- エンジンプラグインはディレクトリーごと 7z でもしてバックアップしてからソースの改変作業を開始する。
- ソースを改変する。
- リビルドしてパッケージを適当な dir へ吐き出す。
- "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\SomethingPlugin\Something.uplugin" -package="C:\Users\usagi\tmp\xxxxx"
- エンジンプラグインのディレクトリーの中身をリビルドした中身へ挿げ替える。
おわりに
§2 はお察しのように大幅に削る事になってしまった。しかしこれでも11月の任意に行動可能なプライベート時間は98%をこの執筆に費やした気分でいる。実際のところもっと時間をとれるかとも思ったが、現実に一日は24時間、仕事はきっちりやる(というかザンギョーもする)なかではなかなか他の趣味の時間をゼロにしても完全な内容には程遠い完成度の文書しか残せなかったことが悔やまれる。しかし、12月1日はもうすぐそこまで来ている。それにこの後わたしは C++ アドベンカレンダーの記事も書くことにしている。時間が、足りない。悲しい。
そして、そんなことをしてこの記事を書いていた11月は趣味で好き勝手に作る楽しいゲーム開発の時間はゼロにしていた。これもまたつらい。数時間前に STEAM からのメールを見て「フィリスのアトリエ」も DLC コンプリートで買ってしまい、詰みゲーも増えた。
私はもともと毎月何本か適当にこのブログで技術記事も執筆しているし、他に趣味のお茶の話と食べ物の話のブログもちらほら書いている。
日頃から技術ブログを書いている事もあり、せっかくアドベンカレンダーに参加するならば、といつもより頑張った記事でも書こうとしてしまい、正直これはたいへん疲れた。アドベンカレンダーは日頃情報発信する機会の少ない方にも情報発信する機会として有用という要素もあるし、私の場合は放って置いても技術ブログを書いているので、やっぱりアドベンカレンダーは今回を最後に引退しようかな、なんて思っています😅
ともあれ、今回書ききれなかった§2にあるべきような節に相当する内容なども含め、今後もちまちまこのブログでは記事を書きますので興味があれば、あるいはググった拍子になど適当に、購読やはてなスターやSNSでのいいねなど頂ければ嬉しいです。
では、みなさん良いクリスマスまで残り24日間と数時間、楽しんで参りましょう😃 私はひとあし先に、一仕事終えた気分で続く記事を楽しみに待つ事とします。
Real Unreal Engine C++ 2017-12 (part-4/5)
(はてなブログの記事あたりの容量制限のため前の部分 §1.9.4 までは前の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2_p3)
1.9.5 FMath
に含まれる型
FMath
には3次元の可視空間を扱う上で必要な形状や数値を扱うための型も定義されている。
数が多いので表に概要を整理する:
概要 | 型 |
---|---|
std::numeric_limits<T> のとてもしょぼい版。Min , Max , Lowest くらいしか実装されていないので std::numeric_limits<T> を使っておけばよい。 |
TNumericLimits |
任意精度整数 | TBigInt |
浮動小数点数のバイナリー表現を一般化する template 型 | TFloatPacker |
IEEE754/Binary16 相当の浮動小数点数型 | FFloat16 |
ソボル準乱数 | FSobal |
EUnit で定義される単位間の変換 |
FNumericUnit |
単位変換 | FUnitConversion |
FUnitConversion が単位の表示名などに使うための補助的な型 |
FUnitSettings |
D3D DirectX::XMVECTOR のエイリアス |
VectorRegister |
SSEレジスター __m128d のエイリアス |
VectorRegisterDouble |
SSEレジスター __m128i のエイリアス |
VectorRegisterInt |
随意型(オプショナル型)std::optiona<T> (C++17) や boost::optional<T> 相当だが operator bool , operator* の実装都合、 TOptional<T> は地味に扱いが面倒。 UE4 が C++17 標準対応完了すれば要らなくなる子。( TArray のようにBlueprintでも使用可能になるなどすればまた別だが・・・) |
TOptional |
成功値の型と失敗値の型を指定可能な TOptional のごつくなったようなやつ |
TValueOrError |
補間 | FInterpCurve FInterpCurveFloat FInterpCurveLinearColor FInterpCurvePoint FInterpCurveQuat FInterpCurveTwoVectors FInterpCurveVector FInterpCurveVector2D 注:FInterpCurve は F 始まりの型だが template |
球面座標 | TSHVector |
RGB色用の球面座標 | TSHVectorRGB |
3次元ベクター | FVector FIntVector |
2次元ベクター | FVector2D FIntPoint |
半精度浮動小数点数型の2次元ベクター | FVector2DHalf |
4次元ベクター | FVector4 FIntVector4 FUintVector4 |
線形色 RGBAを値域 [0..1] に正規化した表現 |
FLinearColor |
2 点を保持(するだけ) | TInterval FFloatInterval FInt32Interval |
2 点を保持(多機能) | TRange FAnimatedRange FDateRange FDoubleRange FFloatRange FInt8Range FInt16Range FInt32Range FInt64Range |
Rangeの内外の制御を行う型 | TRangeBound FDateRangeBound FDoubleRangeBound FFloatRangeBound FInt8RangeBound FInt16RangeBound FInt32RangeBound FInt64RangeBound |
Range 集合用の template 型 Add や Contains など集合に対する操作を行える。 |
TRangeSet |
2つの3次元ベクターの対 | FTwoVectors |
2つのベクターで表される角(かど) | FEdge |
立方体 | FBox |
球 | FSphere |
同一原点の軸平行直方体(AABB)と球の組み合わせ形状 | FBoxSphereBounds |
平面 | FPlane |
カプセル形状 | FCapsuleShape |
中心と軸方位と軸方位への広がり量を基に任意回転した立方体(の頂点位置)を表現する型 | FOrientedBox |
矩形 | FBox2D FIntRect |
uin32 範囲で行数と列数をそれぞれ任意に template で与えられる行列型 | TMatrix |
4x4行列 | FMatrix |
2x2行列 | FMatrix2x2 |
回転変換行列 | FRotationMatrix |
回転と平行移動の変換行列 | FRotationTranslationMatrix |
四元数を基にした回転の変換行列 | FQuatRotationMatrix |
四元数を基にした回転と平行移動の変換行列 | FQuatRotationTranslationMatrix |
逆回転変換行列 | FInverseRotationMatrix |
点を中心に回転する変換行列 | FRotationAboutPointMatrix |
拡大縮小変換行列 | FScaleMatrix |
拡大縮小と回転と平行移動の変換行列 | FScaleRotationTranslationMatrix |
変形(拡大縮小、回転、平行移動)変換行列 | FTranslationMatrix |
視線変換行列 | FLookAtMatrix |
正射影変換行列 | FOrthoMatrix |
遠近法変換行列 | FPerspectiveMatrix |
逆Z軸正射影変換行列 | FReversedZOrthoMatrix |
逆Z軸遠近法変換行列 | FReversedZParspectiveMatrix |
鏡像行列 ある平面で鏡像化するための変換行列 |
FMirrorMatrix |
行列を他の表現に変換するための変換器 | TransformConverter |
オイラー角に基づく回転型 | FRotator |
四元数による3次元の回転型 | FQuat |
四元数風の2次元の回転型 | FQuat2D |
3次元の拡大縮小を表す型 | FScale |
2次元の拡大縮小を表す型 | FScale2D |
3次元変形(拡大縮小、回転、平行移動) | FTransform |
2次元変形(拡大縮小、回転、平行移動) | FTransform2D |
2次元の剪断 | FShear2D |
自称「スレッドセーフで低ビットの品質の悪い擬似乱数生成器」 実装を見るにスレッドセーフではないし、線形合同法ベースだし、 C++er は <random> を使っておけばよいので存在を忘れても構わない何か。 |
FRandomStream |
暗号鍵用に整数の指数と剰余を保持する template 型 | TEncryptionKey |
この節の表を作っただけで力尽きて来たので、面白い小話などはまた別の機会に書く事にする。さしあたり、 C++er は FMath
に定義される機能で必要十分な限りにおいては FMath
の部品を再実装せず少々の癖はあるが上手く付き合えるよう、どのような機能の関数と型があるのか脳内にインデクシングしておくと良い。
なお、 UE4 の線形幾何ライブラリーの実装レベルは速度最適化の面から言っても、それほど優れた実装にはなっていない。例えば、 Eigen
のように template
が充実していたり、 expression template により評価を遅延していたり、などという工夫はほとんど無く、わりと単純に float
ベースで個々に実装されている。そういうわけで、 "必要十分な限りにおいては" と書いた。それについてもまた本記事の趣旨とは少々ずれるので別の機会があれば書く事にする。
1.10. UE4/C++ における入門的な「コンテナー」ライブラリーの std 互換性
一般的な std 慣れした C++er は UE4 コンテナーのメンバー変数の命名と機能とパラメーターに翻弄される。この節では std に慣れた C++er が UE4/C++ にうっかり翻弄され難くなるよう、予防接種的な視点にも注意しながら UE4 のコンテナー類を整理する。
1.10.1. TArray<T>
と std::deque<T>
/ std::vector<T>
/ std::queue
/ std::stack
所属 | 型 | Header |
---|---|---|
std | vector |
<vector> |
std | deque |
<deque> |
std | stack |
<stack> |
std | queue |
<queue> |
UE4 | TArray |
Runtime/Core/Public/Containers/Array.h |
UE4 | TQueue |
Runtime/Core/Public/Containers/Queue.h |
C++er は std::vector
/ std::stack
/ std::queue
がコンテナーアダプターであり、実装詳細は std::deque
コンテナーである事をよく知っている。また、多くのコンテナー類に対して操作を共通化するため <algorithm>
が用意され、ほとんどの操作はイテレーターを介したレンジで扱われる事もよく把握している。
UE4 にはそれらをひとまとめにしたような "便利" なコンテナーとして "マッチョ" な TArray
が定義されている。
そこで、この項では std::vector
/ std::stack
/ std::queue
と TArray
を対応させ、機能の有無や対応関係がわかりやすいよう整理する。また、 UE4 にはキュー専用に特化した TQueue
も定義されているので "ついで" 程度に含める事にする。
なお、「機能を組み合わせれば実現できる」ものは除外し(それは C++er なら脳内でわかること)、直接的にそのように振る舞う機能がある場合にのみ表に関数名を記載する。
効果 | std::deque |
std::vector |
std::stack |
std::queue |
TArray |
TQueue |
---|---|---|---|---|---|---|
[begin..end) の値群に置き換える |
assign |
assign |
||||
別のコンテナーの値群を追加する | Append operator+= |
|||||
最前に値を直接生成する | emplace_front |
|||||
最後に値を直接生成する | emplace_back |
emplace_back |
emplace |
emplace |
Emplace |
|
最前に値を入れる | push_front |
|||||
最後に値を入れる | push_back |
push_back |
push |
push |
Add AddDefaulted AddUninitialized AddUnique AddZeroed <Push> |
Enqueue |
最前の値を削除する | pop_front |
pop |
Pop |
Pop |
||
最後の値を削除する | pop_back |
pop_back |
pop |
|||
任意の位置へ値を直接生成する | EmplaceAt |
|||||
任意の位置へ値を内挿する | insert |
insert |
Insert InsertDefaulted InsertUninitialized InsertZeroed |
|||
値を全て削除する | clear |
clear |
Empty Reset |
|||
保持する値の数を変更する | resize |
resize |
SetNum SetNumUninitialized SetNumZeroed Init |
|||
保持する値の数を予約する | reserve |
Reserve |
||||
値を範囲で削除する | erase |
erase |
||||
値を値またはインデックスで削除する | Remove RemoveAll RemoveAllSwap RemoveAt RemoveAtSwap RemoveSingle RemoveSingleSwap RemoveSwap |
|||||
保持可能な値の数を実際の保持数に最適化する | shrink_to_fit |
shrink_to_fit |
Shrink |
|||
最前の値を取得する | front |
front |
front |
Peek Dequeue |
||
最後の値を取得する | back |
back |
top |
back |
Last Top |
|
任意の位置の値を範囲チェック付きで取得する | at |
at |
||||
任意の位置の値を取得する | operator[] |
operator[] |
operator[] |
|||
先頭の値のメモリーアドレスを取得 | data |
GetData |
||||
値を保持していない事を判定する | empty |
empty |
empty |
empty |
IsEmpty |
|
値の保持数が正常かアサーション( checkSlow )する | CheckInvariants |
|||||
保持する値の数を取得する | size |
size |
size |
size |
Num |
|
保持する値の総容量を取得する | CountBytes |
|||||
内部バッファーの数を取得する | capacity |
|||||
内部バッファーの空き数を取得する | GetSlack |
|||||
内部バッファーのメモリー容量を取得する | GetAllocatedSize |
|||||
値を保持可能な最大数を取得する | max_size |
max_size |
Max |
|||
最前のイテレーターを取得する | begin |
begin |
CreateIterator |
|||
最後のイテレーターを取得する | end |
end |
||||
最前のconstイテレーターを取得する | cbegin |
cbegin |
CreateConstIterator |
|||
最後のconstイテレーターを取得する | cend |
cend |
||||
最後からの始端のリバースイテレーターを取得する | rbegin |
rbegin |
||||
最後からの終端のリバースイテレーターを取得する | rend |
rend |
||||
最後からの始端のconstリバースイテレーターを取得する | crbegin |
crbegin |
||||
最後からの終端のconstリバースイテレーターを取得する | crend |
crend |
||||
別のコンテナーと値群を挿げ替える | swap |
swap |
swap |
swap |
Swap |
|
別のコンテナーと値群のメモリーを挿げ替える | SwapMemory |
|||||
アロケーターを取得する | get_allocator |
get_allocator |
||||
有効なインデックスか判定する | IsValidIndex |
|||||
有効なレンジか判定する | RangeCheck |
|||||
アドレスがコンテナーの値か判定する | CheckAddress |
|||||
値のインデックスを取得する | IndexOfByKey IndexOfByPredicate |
|||||
要素型のメモリー容量を取得する | GetTypeSize |
|||||
値が含まれるか判定する | Contains ContainsByPredicate |
|||||
最前から値を検索する | std::find |
Find FindByKey FindByPredicate FindItemByClass |
||||
最後から値を検索する | FindLast FindLastByPredicate |
|||||
フィルターしたコンテナーのコピーを取得する | std::copy_if |
FilterByPredicate |
||||
非安定ソートする | std::sort |
Sort |
||||
安定ソートする | std::stable_sort |
StableSort |
||||
シリアライズ結果を取得する | BulkSerialize |
|||||
保持する値群をヒープ構造化する | std::make_heap |
Heapify |
||||
保持する値群がヒープ構造化されているか判定する | std::is_heap |
VerifyHeap |
||||
ヒープ構造を前提に最前の値を削除する | std::pop_heap |
HeapPop HeapPopDiscard |
||||
ヒープ構造を前提に値を追加する | std::push_heap |
HeapPush |
||||
ヒープ構造を前提に任意の位置の値を削除する | HeapRemoveAt |
|||||
ヒープ構造を前提にソートする | std::sort_heap |
HeapSort |
この他に大きな相違として、 UE4 では TArray
と次節で紹介する TMap
、それと TSet
については template
であっても例外的に UE4Editor / Blueprint でも直接的な使用がサポートされている点、 UPROPERTY()
マクロの指定が可能な点がある。これは一般に C++ コードだけでは完結しない UE4 のプロジェクトの "開発" について、また UObject
派生型を要素型とする必要性において、しばしばコンテナーの実装に求められる重要な要件となる。
std 慣れした C++er が注意を要する TArray
を扱う上でのポイントは以下の通り:
deque
|vector
|stack
|queue
|SORT
|HEAP
|FIND
≃TArray
size
=Num
resize
≃SetNum
reserve
=Reserve
empty()
≃Num() > 0
とclear()
≃Empty()
の混乱回避。std::remove
+erase
( erase-remove idiom ) ≃Remove
data
=GetData
TArray
はUPROPERTY()
可能。- std アルゴリズムのレンジは生ポインターでも動作するので
TArray
にも適用はできる。 std::vector
におけるreserve
やcapacity
で扱う内部バッファーをTArray
では "Slack" (スラック)と用語を充てている。
1.10.2. TMap<K,V>
と std::unordered_map<K,V>
所属 | 型 | Header |
---|---|---|
std | unordered_map |
<unordered_map> |
UE4 | TMap |
Runtime/Core/Public/Containers/Array.h |
std の解説一般でもしばしば同じ節で扱われるように unordered_map
にはよく似た unordered_set
があり、 UE4 にもそれらと同じ関係の TMap
と TSet
がある。この記事は網羅的なクラスや API の紹介が趣旨ではないため(&そろそろアドベンカレンダーの期日も近づきすぎている都合もあり…)、冗長を避ける意味でも unordered_map
vs. TMap
のみを取り上げる。
効果 | std::unordered_map |
TMap |
TMap のメンバーの継承元 |
---|---|---|---|
別のコンテナーの値群を追加する | Append operator+= |
TMap |
|
キーと値の組みを直接生成する | emplace |
Emplace |
TMapBase |
キーと値の組みを挿入位置のヒント付きで直接生成する | emplace_hint |
||
キーと値の組みを入れる | insert |
Add |
TMapBase |
キーの値を取得または生成して取得する | operator[] |
FindRef |
TMapBase |
キーの値を取得する | at |
FindChecked operator[] |
TMapBase TMap |
キーの値を取得または生成してポインターを取得する | FindOrAdd |
TMapBase |
|
キーの値のポインターを取得する | Find |
TMapBase |
|
値のキーのポインターを取得する | FindKey |
TMapBase |
|
キーの組みのイテレーターを取得する | find equal_range |
||
キーの組みを削除しつつ値を取得する | FindAndRemoveChecked |
TMap |
|
キーの組みの有無を確認して存在すれば削除しつつ値を取得する | RemoveAndCopyValue |
TMap |
|
キーの組みを削除する | erase |
Remove |
TMapBase |
組みを全て削除する | clear |
Empty Reset |
TMapBase |
内部バッファーの数を予約する | reserve |
Reserve |
TMapBase |
内部バッファーのバケットの最小数を指定してバケット数を調整する | rehash |
||
内部バッファーのバケットの負荷率の最大値を設定または取得する | max_load_factor |
||
内部バッファーのバゲットの負荷率を取得 | load_factor |
||
内部バッファーの末尾の未使用領域を削除する | Shrink |
TMapBase |
|
内部バッファーの未使用領域を削除する | Compact |
TMapBase |
|
内部バッファーの未使用領域を使用領域の順序を保持して削除する | CompactStable |
TMapBase |
|
組みの順序をキーでソートする | KeySort |
TSortableMapBase |
|
組みの順序を値でソートする | ValueSort |
TSortableMapBase |
|
組みへのconstイテレーターを取得する | cbegin |
CreateConstIterator |
TMapBase |
組みの終端へのconstイテレーターを取得する | cend |
||
キーへのconstイテレーターを取得する | CreateConstKeyIterator |
TMapBase |
|
組みへのイテレーターを取得する | begin |
CreateIterator |
TMapBase |
組みの終端へのイテレーターを取得する | end |
||
キーへのイテレーターを取得する | CreateKeyIterator |
TMapBase |
|
キーの配列と数を取得する | GetKeys |
TMapBase |
|
組みの数を取得する | size |
Num |
TMapBase |
内部バッファーの数を取得する | bucket_count |
GetAllocatedSize |
TMapBase |
内部バッファーの最大数を取得する | max_size bucket_size |
||
キーが存在するか判定する | count |
Contains |
TMapBase |
保持する組みが空か判定する | empty |
||
キー群の配列を取得する | GenerateKeyArray |
TMapBase |
|
値群の配列を取得する | GenerateValueArray |
TMapBase |
|
内部バッファーを出力デバイス(ログ的なやつ)へ書き出す | Dump |
TMapBase |
|
シリアライズに要するメモリー容量を取得する | CountBytes |
TMapBase |
|
アロケーターを取得する | get_allocator |
||
要素を挿げ替える | swap |
||
ハッシュ関数オブジェクトを取得する | hash_function |
||
キー比較用オブジェクトを取得する | key_eq |
std::unordered_map
に比べ TMap
ではキー、値それぞれに着目した操作や列挙の実装が充実している。 std 慣れした C++er が留意すべき TMap
のポイントは次の通り:
unordered_map
のoperator[]
は存在しない要素は生成してくれるが、TMap
のoperator[]
は存在しない要素は生成してくれない(実行時エラーとなる)。その用途にはFindRef
を使う。TMap
は KEY, VALUE によるソートが可能。- メンバーの命名 →
TArray
と同様の注意が必要。 UPROPERTY()
可能。- インデックスアクセスも可能な連想配列として振る舞う(もちろん表面上は似てはいるが
std::unordered_map
の一般的な実装とTMap
の実装詳細はまったく異なる)
1.10.3. その他の UE4 コンテナーライブラリー
他の std に対応する UE4 コンテナーは以下の通り:
概要 | std | UE4 |
---|---|---|
ビット列コンテナー | std::vector<bool> |
TBitArray |
単方向リスト | std::forward_list<T> |
TList<T> |
ソート済み連想コンテナー | std::map<K,V> |
TSortedMap<K,V> |
ハッシュ集合 | std::unordered_set<T> |
TSet |
優先度付きキュー | std::priority_queue<T> |
FBinaryHeap<K,I> |
また、 他にも UE4 には std には無いコンテナー類も存在します。そのうちの一部は boost には同種のコンテナーが存在する。
概要 | boost | UE4 |
---|---|---|
既存の生配列を参照してサイズ変更を除く TArray<T> と同等の操作を可能にするビュー |
TArrayView |
|
循環バッファー | boost::circular_buffer<T> |
TCircularBuffer<T> |
ロックフリー循環キュー | boost::lockfree:queue |
TCircularQueue<T> |
ロックフリーポインターリスト | FLockFreePointerListFIFO FLockFreePointerListLIFORoot |
|
疎配列 | Boost.SparseVector |
SparseArray<T> |
バリアント集合 | TUnion<A,B,C,D,E,F> |
何れも前項までに紹介したコンテナー類よろしく std 慣れした C++er の脳を引っ掻き回すような命名規則や引数はおおよそ共通。また、一部には EPIC GAMES 以外が主体的に開発したソースも含まれさらなる混沌感も少々ある。
何れも使い所次第では単に TArray<T>
や TMap<T>
あるいは TSet<T>
、それに何らかの工夫を加えて使うよりも高効率であったり便利であったりはするものの、 UE4Editor / Blueprint からは使用不能。必要が生じたら使える程度に軽く脳にインデクシングしておけば善いでしょう。
(はてなブログの記事あたりの容量制限のため続き §1.11. 以降は次の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2_p5)
Real Unreal Engine C++ 2017-12 (part-3/5)
(はてなブログの記事あたりの容量制限のため前の部分 §1.9.3 までは前の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-2/5) - C++ ときどき ごはん、わりとてぃーぶれいく☆)
1.9.4. Fmath
の真価
前項では FMath
について少し残念な結果も見えたが、ともあれ、これまでの下層の実装の上に、 UE4/C++er が実際に多くの場合に数学関数のために触れる FMath
が実装される。
API Reference を見ると冒頭の Inheritance Hierarchy でも FPlatformMath
から派生している事がわかる。この FPlatformMath
は前項のようにプラットフォームごとに実装が特殊化されており、その基底型として FGenericPlatformMath
、また数学関数の最下層としては本節の始めの項のように C++ の CRT 互換層も #include
している。
さて、それでは FMath
型には何が定義されているかというと、主に線形幾何、そして範囲操作、補間、角度単位の変換などの関数群、そして区間、ベクター、回転、四元数、行列、任意精度整数、球面座標、IEEE754/binary16 実装など、主に線形幾何と補間、そして GPU 互換性が必要な場合向けの型が含まれる。
FMath
の機能詳細の全貌を明らかとする事は本記事の目的から逸脱する事、また FMath
の規模が大きい事、 std 慣れした C++er にとって特に互換性のある標準機能は無く、さしあたりこうした機能群が UE4 の FMath
に存在するという予備知識が得られれば十分である事などを理由に、本項では関数の概要を紹介するに留める。
効果 | UE4 |
---|---|
int32値 n を与え [0..n] に一様分布する乱数を得る注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 |
RandHelper |
FRandRange と同じ。 int32 版と誤用されないようオーバーロードしている。 |
RandRange |
実数区間 [a..b] に一様分布する擬似乱数を取得注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 | FRandRange |
bool 型の擬似乱数を得る 注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 |
RandBool |
長さ 1 の擬似乱数ベクターを得る注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 |
VRand |
中心方位と水平および垂直の弧度法による角度範囲に基づくコーン形状の方位を向くランダムなベクターを得る 注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 |
VRandCone |
直方体の稜線または内部に一様分布する擬似乱数の点を得る 注意: C++ CRT 線形合同法擬似乱数生成器ラッパーを内部的に使用。 |
RandPointInBox |
面法線の明らかな面へ入射するベクターの反射ベクターを取得 | GetReflectionVector |
値 TestValue が 値域 [MinValue .. MaxValue) か判定 |
IsWithin |
値 TestValue が 値域 [MinValue .. MaxValue] か判定 |
IsWithinInclusive |
2つの浮動小数点数型の値について差が誤差許容値以内か判定する デフォルトの誤差許容値は 1.0e-8f |
IsNearlyEqual |
浮動小数点数型の値と 0 との差が誤差許容値以内か判定するデフォルトの誤差許容値は 1.0e-8f |
IsNearlyZero |
整数について2の指数倍か判定 実装詳細の都合、負の値は false 、 0 , 1 は true を返す。 |
IsPowerOfTwo |
3つの値から最大値を選択 | Max3 |
3つの値から最小値を選択 | Min3 |
2乗 | Square |
制限された値域へ切り詰める | Clamp |
値 Grid の間隔に対し入力値を四捨五入的に吸着した値を取得 |
GridSnap |
繰り上げ除算 (10,1) -> 10, (10,2) -> 5, (10,3) -> 4, (10, 4) -> 3, .. |
DivideAndRoundUp |
繰り下げ除算 (10,1) -> 10, (10,2) -> 5, (10,3) -> 3, (10, 4) -> 2, .. |
DivideAndRoundDown |
底2の対数 実装詳細で static const float LogToLog2 = 1.f/Loge(2.f) をキャッシュし return Loge(x)*LogToLog2 しているため cl.exe の std::log2 比で 1.37 倍程度高速。 |
Log2 |
正弦と余弦の同時計算 cl.exe では O3 でも個別に計算するより 1.13 倍程度高速。 |
SinCos |
高速逆正弦(10進数で5桁まで安全な精度、O3条件下で2.2%程度Asinより高速) | FastAsin |
弧度法単位を度数法単位へ変換 | RadiansToDegrees |
度数法単位を弧度法単位へ変換 | DegreesToRadians |
弧度単位の角度を回転を考慮して制限された値域へ切り詰める | ClampAngle |
2つの与えられた度数法の角度の差を値域(-180..+180)で取得 | FindDeltaAngleDgrees |
2つの与えられた弧度法の角度の差を値域(-π..+π)で取得 | FindDeltaAngleRadians |
DEPRECATED -> FindDeltaAngleRadians に同じ。 |
FindDeltaAngle |
弧度法の角度を [-π..+π] の範囲の表現に変換する |
UnwindRaidnas |
度数法の角度を [-180..+180] の範囲の表現に変換する |
UnwindDegrees |
2つの度数法の角度を与え、一方を180°を超える回転を伴わない角度へ反転する この関数は四元数を使わずに回転を最適化したい場合用に用意されている |
WindRelativeAnglesDegrees |
度数法の角度を角度区間 [a..b] にクランプ。但し負の入力や周回には恐らく意図しない応答となる。 |
FixedTurn |
カルテシアン座標から極座標を得る | CartesianToPolar |
極座標からカルテシアン座標を得る | PolarToCartesian |
任意の軸方向の系で対象への向きから対象への相対的な方位角の余弦値と仰俯角の正弦値を計算GetAzimuthAndElevation に比べ O3 で 1.34 倍速程度高速 |
GetDotDistance |
任意の軸方向の系で対象への向きから対象への相対的な方位角(rad;右側が+)と仰俯角(rad;上側が+)を計算 | GetAzimuthAndElevation |
値を値域 [min .. max] から値域 [0 .. 1] へ写像"Pct"は"Percentage"の略記と思われるが実際には"Percentage"ではなく単純な比(はみ出し有りの unorm )である。 |
GetRangePct |
Lerp の FVector2D 版。 |
GetRangeValue |
値を値域 [in.x .. in.y] から値域 [out.x .. out.y] へクランプ有りで写像 |
GetMappedRangeValueClamped |
値を値域 [in.x .. in.y] から値域 [out.x .. out.y] へクランプ無しで写像 |
GetMappedRangeValueUnclamped |
値域 [a..b] について比 Alpha となる値を取得但しプラットフォームネイティブの整数型以上のビット幅の整数型の全域は扱えない。その必要があれば LerpStable を使う。 |
Lerp |
Lerp の整数型全域に対して安全な実装Lerp は実装詳細で b-a を計算するが、x64でこの計算は 32 bits 未満の a , b に対しては 32 bit 整数に拡張して行われるため、 int8 で a=-128 , b=0 , Alpha=1.0 を与えると b-a=+128 となり、結果的には (T)(a+Alpha*128) はオーバーフローして 0 となるから写像としては正しい結果が得られる。しかし、 int32 の a , b を同様に与える場合には b-a の時点でオーバーフローし 0 となるため (T)(a+Alpha*0) は Alpha=1.0 でも aとなる。これは int64等でも同様に発生する。 LerpStableはこの問題の発生しない実装。なお、 Lerpと LerpStableの速度差は O0 でも O3 でもほとんど無いので特別な事情が無ければ LerpStable` を使っておけば良い。 |
LerpStable |
二次元の補間結果を得る | BiLerp |
3次 Hermite 補間した点の値を計算 | CubicInterp |
3次 Hermite 補間した点の1次微分値を計算 | CubicInterpDerivative |
3次 Hermite 補間した点の2次微分値を計算 | CubicInterpSecondDerivative |
底 Alpha 指数 Exp の指数関数で得られる値域 [0..1] を値域 [a..b] へ写像 |
InterpEaseIn |
1 - InterpEaseIn |
InterpEaseOut |
InterpEaseIn とInterpEaseOut を1:1で合成 |
InterpEaseInOut |
値域 [0..1] を分解能 Step で分割した離散値へ入力値 Alpha 吸着し値域 [a..b] へ写像 |
`InterpStep |
値域 [0..1] で表される比の入力値 Alpha を-cos(Alpha*π/2)+1 関数で補正し値域 [a..b] へ写像 |
InterpSinIn |
1 - InterpSinIn と等価 |
InterpSinOut |
InterpSinIn とInterpSinOut を1:1で合成と等価 |
InterpSinInOut |
値域 [0..1] で表される比の入力値 Alpha を-exp2(-10*Alpha)+1 関数で補正し値域 [a..b] へ写像 |
InterpExpoIn |
1 - InterpExpoIn |
InterpExpoOut |
InterpEaseIn とInterpEaseOut を1:1で合成 |
InterpExpoInOut |
値域 [0..1] で表される比の入力値を円関数で補正し値域 [a..b] へ写像 |
`InterpCircularIn |
1 - InterpCircularIn |
InterpCircularOut |
InterpCircularIn とInterpCircularOut を1:1で合成 |
InterpCircularInOut |
回転 FRotator において Lerp と基本的に同様、但し最短経路ではなく値域に応じて180°以上の経路を取る | LerpRange |
3次 Catmull-Rom スプライン補間 | CubicCRSplineInterp |
3次 Catmull-Rom スプライン補間、フェイルセーフ機能付き | CubicCRSplineInterpSafe |
InterpTo の 3次元ベクター FVector による正規化法線版 |
VInterpNormalRotationTo |
InterpConstantTo の 3次元ベクター FVector 版 |
VInterpConstantTo |
InterpTo の 3次元ベクター FVector 版 |
VInterpTo |
InterpConstantTo の 2次元ベクター FVector2D 版 |
Vector2DInterpConstantTo |
InterpTo の 2次元ベクター FVector2D 版 |
Vector2DInterpTo |
InterpConstantTo の回転 FRotator 版 |
RInterpConstantTo |
InterpTo の回転 FRotator 版 |
RInterpTo |
float 用の区間 [a..b] にクランプされる速度倍率制御付き線形補間 |
FInterpConstantTo |
事実上 FInterpConstantTo と同じ(実装詳細は異なる) |
FInterpTo |
InterpTo の線形色 FLinearColor 版 |
CInterpTo |
周波数 (Hz) と位相シフト [0..1] と時刻 (sec) から振幅範囲 [0..1] の正弦波の振幅値を得る |
MakePulstingValue |
線分と平面の交差判定 | LinePlaneIntersection |
遠近法変換行列、視野変換行列、視点、有効な球形の範囲からシザー領域の必要性の判定と領域の計算を行う | ComputeProjectedSphereScissorRect |
平面と軸平行直方体(AABB)の交差判定 | PlaneAABBIntersection |
球と軸平行直方体(AABB)との交差判定 | SphereAABBIntersection |
点と直方体の交差判定 | PointBoxIntersection |
線分と直方体の交差判定 | LineBoxIntersection |
掃引直方体と直方体の交差判定 | LineExtentBoxIntersection |
線分と球形状の交差判定 | LineSphereIntersection |
球とコーン形状の交差判定 | SphereConeIntersection |
三次元の2点を結ぶ線分上で他の任意の1点に最近接する点を得る OnSegment2Dと同じ結果となるが、命名から推量するに線分と直線などの違いが設計段階ではあったのかもしれない。 |
ClosestPointOnLine ClosestPointOnSegment |
三次元の2点を結ぶ無限遠の直線上で他の任意の1点に最近接する点を得る | ClosestPointOnInfiniteLine |
3つのFPlane平面が交わるか判定し return を返し、交わる場合には交わる交点 I を与える |
IntersectPlanes3 |
2つのFPlane平面が交わるか判定し return を返し、交わる場合には交わる直線上の1点 I と直線の向き D を与える |
IntersectPlanes2 |
点と直線の距離 | PointDistToLine |
二次元の2点を結ぶ線分上で他の任意の1点に最近接する点を得る | ClosestPointOnSegment2D |
点と線分の距離 2乗次元の値を取得できれば事足りる場合は PointDistToSegmentSquared を使う方が速度的に有利 |
PointDistToSegment |
点と線分の距離の2乗PointDistToSegment が sqrt を要する分だけこちらの方が高速 |
PointDistToSegmentSquared |
2つの線分上で互いに最接近となる2点の座標を得る 但し、線分の何れかまたは両方が長さ 0 の場合は正しい結果を得られない。その可能性がある場合は SegmentDistToSegmentSafe を使う。 |
SegmentDistToSegment |
SegmentDistToSegment の線分の長さが 0 でも正しく計算可能な実装 |
SegmentDistToSegmentSafe |
2点を通る直線が平面と交差する位置を直線の2点に対する比として取得 交差しない場合は -inf が返る 例: 直線 {(0,0,1),(0,0,-1)} を FPlane(0,0,1,0) へ与えた場合、 0.5 が得られる。 |
GetTForSegmentPlaneIntersect |
線分と平面が交差するか判定し、交差する場合はその座標も与える | SegmentPlaneIntersection |
線分と三角面が交差するか判定し、交差する場合はその座標も与える | SegmentTriangleIntersection |
3次元上の2つの線分がXY平面に投影された場合に交差するか判定し、交差する場合はその座標も与える | SegmentIntersection2D |
三次元の3点からなる三角形稜線上で他の任意の1点に最近接する点を得る | ClosestPointOnTriangleToPoint |
三次元の4点からなる四面体表面上で他の任意の1点に最近接する点を得る | ClosestPointOnTetrahedronToPoint |
球に対し1つの点と正規化された方位からなる半直線との交点を計算 | SphereDistToLine |
コーン形状に点が含まれるか判定 | GetDistanceWithinConeSegment |
配列で与える点群が同一の平面から一定の距離内にあるか判定 | PointsAreCoplanar |
偶数丸め | RoundHalfToEven |
浮動小数点数型の値を小数部の絶対値が 0.5 以上であれば 0 から遠ざかる方向、それ以外の場合は 0 へ近づく方向の整数へ丸める |
RoundHalfFromZero |
浮動小数点数型の値を小数部の絶対値が 0.5 以下であれば 0 へ近づく方向、それ以外の場合は 0 から遠ざかる方向の整数へ丸める |
RoundHalfToZero |
浮動小数点数型の値を 0 から遠ざかる方向の整数へ丸める |
RoundFromZero |
0 方向への丸め |
RoundToZero |
-∞ 方向への丸め |
RoundToNegativeInfinity |
+∞ 方向への丸め |
RoundToPositiveInfinity |
int32値に桁区切りのカンマを付けた文字列を返す 但しバグがあり正の999,999,999以下の値にしか正常な挙動を期待できない https://goo.gl/sfv5zo |
FormatIntToHumanReadable |
4bytesを単位としてメモリー空間に対して書き込みと読み込みを行い成否を判定する | MemoryTest |
文字列の数式を評価 対応演算子: + - / % * @ 対応数値文字: 0..9 と . で構成される実数文字列 括弧: ( ) L"1+2+3-4-5-6*7*8*9" -> -3027 Note: documentation bug |
Eval |
ComputeBaryCentric2D の簡易計算実装版法線計算, 共線のcheckなどを端折っている O3で1.59倍速程度に速い |
GetBaryCentric2D |
三次元の3点のからなる三角形に対し他の任意の1点の重心座標を得る | ComputeBaryCentric2D |
三次元の4点からなる四面体に対し他の任意の1点の重心座標を得る | ComputeBaryCentric3D |
値域 [0..1] で表される比 X を一般的なスムーズステップ関数を適用した値を [a..b] へ写像した値を取得注意: 入力値について clamp 処理を実装していないため [0..1] を超える X に対する clmap された応答を期待してはならない |
SmoothStep |
任意の連続したメモリー領域から特定ビットを抽出 | ExtractBoolFromBitfield |
任意の連続したメモリー領域の特定ビットへ書き出し | SetBoolInBitField |
FVector で与えるXYZの拡大縮小と float のMagnitude から単一の拡大縮小倍率を float で得る |
ApplyScaleToFloat |
float 型の入力値の値域 [0..1] を uint8 型の出力 [0..255] へ写像 |
Quantize8UnsinedByte |
float 型の入力値の値域 [0..1] を int8 型の出力 [-128..+127] へ写像 |
Quantize8SinedByte |
最大公約数 | GreatestCommonDivisor |
最小公倍数 | LeastCommonMultiplier |
UE4/C++er はこれらの実装が FMath
にあり、必要に応じて独自に実装する事なく使用可能な事を把握しておくとよい。
(はてなブログの記事あたりの容量制限のため続き §1.9.5 以降は次の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-4/5) - 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>
では対応が困難な機能は、外部ライブラリーを取り込んで使えば良い。
次項の前に少し <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++ ときどき ごはん、わりとてぃーぶれいく☆)