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

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

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 未満の場合に本来よりも僅かに明るくシフトしてしまう結果となる。グレイスケールカラーで人間が目視するだけであれば、大きな問題にはならない事も多いが、変換を繰り返したり、その値での制御を一定時間続ける装置に適用したりすると問題と成り得る事もある。

出題のソースコードinout の関係の一部:

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,648int32 へキャストすると -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, bint よりも狭いビット幅の整数型の場合に b - asigned 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 - aint16 ではなく int32 で計算され、結果も int32 となるので -65535 を余裕をもって扱えてしまう。結果、 int 未満のビット幅の整数型を LerpTV で扱う限りは問題が表面化せず、一見すると任意の整数型に対して意図通りに動作するように見えてしまう。

もし、 Lerp に単体試験を網羅的に行うよう書いていたとしても、計算時間と "整数型" の概念を漠然と考えて、 int8uint8、あるいは int16uint16 で試験を実装してしまっていたなら、この問題はその単体試験が成功してしまうために何かが起きた際に原因を探すのが少し難しくなる事もあるかもしれない。

この問題が起こらない Lerp の実装例は次の通り:

  return (TV)( a * ( 1 - ratio ) + b * ratio );

数学的には等価な ( a + ratio * ( b - a ) )( a * ( 1 - ratio ) + b * ratio ) も現実の計算機では等価とならない事もある。

Q3. 浮動小数点数

IsSameAltitudereturn では宇宙船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 なので。

これはもちろん FVectorx, y, z それぞれについても、また operator- の結果も、 Length の計算の乗算や加算や std::sqrt の結果も全てが計算機にとってはこのバイナリー表現の世界が現実となります。

よって、 例え宇宙船Aと宇宙船Bの高度がプリントエフデバッグやログ出力などで確認すると同じ 73.1 と出力される状況でも、 IsSameAltitudefalse を返す意図せず多発、というよりほとんどの状況となる。

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つの問題が潜んでいる。

  1. a=π/2=90° ), b=2π+π/2=360°+90°=450°) に対して returnfalse となるが、一般的には何周多く回っても結果的に示す角度は等価なため、本来は true が返ると期待される。
  2. 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 < efalse と評価されてしまう。本来は true が返ると期待される。

対応として、 ab の角度の差を正しく評価する 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 を引用する形で追記・紹介いたします😃

ぴんと来ない方も Tweet に添付して頂いた wandbox で状況を確認できるコード を見ると、たいへん分かり易いと思います。ありがとうございます。

Q7. 人間が読みやすい "数値" にしたかった

出題の実装では次の3つの問題が発生する。

  1. int32 型の値は 999,999,999 を超える値も取り得る。例えば 1234567890 をこの関数の実引数として与えると、出力は 1234,567,890 となり期待される桁区切りが足りずに、奇妙で読み難い、意図しない出力が得られる。
  2. int32 型の値は負の値を取り得る。例えば -123456789 を与えると 出力は -123456789 となり桁区切りがまったく行われず、意図しない出力が得られる。
  3. 世界は広い。日本やアメリカ合衆国の文化圏では数値の桁区切りが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 を中心として符号を除去した intensityT 型の最大値で除算してしまうと、元の値が負の値だった場合に意図した意図した値域の写像とならない。例えば、 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++ コードを楽しむコンセプトで記事を書くことにしました。今年は UE4C++ 実装を眺める時間が業務でも多くなりましたので、 Unreal Engineソースコードを基に C++ の Tips などをクイズ形式で紹介します。

Unreal Engine について

Unreal EngineEpic Games が現在は OSS として github で公開しながら開発を続けている C++ の実装を基にしたゲームエンジンフレームワークです。執筆現在の現行版は 4.18 です。この記事は 4.18 のソースコードを基に執筆します。

  1. https://github.com/EpicGames/UnrealEngine/tree/4.18
  2. https://github.com/EpicGames/UnrealEngine/tree/4.18/Engine/Source/Runtime

実行環境について

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="" 設定を基本の実行環境とします。

  1. https://wandbox.org/

また、この記事の内容は一般的な x64 アーキテクチャーの PC や互換性のある処理系を対象の範囲とします。これは、例えば float は IEEE754/binary32 を前提とする事を意味します。

出題について

Unreal Engine の実装を基に問題を作成していますが、 Unreal Engine には依存しない C++ の問題となるよう調整しています。また、本記事の問題はあくまでも QUIZ として楽しめるよう工夫したもので、問題に登場するバグが実際の Unreal Engineソースコードでバグとして対処されないまま埋め込まれているという事ではありません。

また、問題は C++ のコードを例示して出題していますが、 C++ の言語仕様に起因するクイズを中心とした C++ Quiz とはことなり、本記事ではもっと緩く C++ 以外でも起こり得る問題を広く扱います。

非常勤講師をしていた頃を思い出して、学生へ講座のはじまりに遊びを兼ねて出題するような気持ちで作りますので、 C++ 初心者、学生さんに楽しんで貰えれば嬉しく思います。

記法について

  1. 文章中で template 仮引数を一般に詳細に明記せずとも意図が通じると思われる範囲内で std::vector<T> のように略記する事があります。
  2. 文章中で値域を [ 0.0 .. 1.0 ] のように表記する場合があります。端の括弧は端の値も含まれる場合には [ または ] を用い、 端の値が含まれない場合には ( または ) を用います。また、値の間に .. があれば明示された値で挟まれた任意の値を取り得る事を意味し、 2つ以上数値が , で区切られて続いていれば , で区切られた間隔で連続する事を意味し、 .. の前後に具体的な数値が無い場合には文脈上の最小値または最大値、もしくは理論上の -∞ または +∞ へ続く意図です。
  3. 文章中では変数の値などの数値に ,+ を加えて読みやすく表現する場合があります。
  4. 文章中では C++ コードとしてではなく一般的な数式の表現として a=2π+π/2 のような表現をする場合があります。
  5. uint8, int32 など UE4 で定義される型で、一般にその型名から定義が明白と考えられる型については特に解説なく用います。
  6. ソースコード中に UE4 由来の check() マクロが登場する事があります。これは <cassert> で使用可能となる assert() マクロを #define check(X) { assert(X); } と薄くラップしたものと同様と考えて下さい。簡易的な試験として括弧の中の式が実行時に評価され、 false の場合にはプログラムの動作が停止し問題が検出される一般的なアサーションとして用います。

こたえについて

答えが見えてしまうとクイズとして楽しみ難い方も多いと思います。そこで、クイズに対する著者が想定したこたえはこの記事に続けて投稿する別の記事として掲載し、この記事のおわりからリンクする事にします。

また、「ぐぐ」れば答えがすぐに見つかる問題もありますが、「クイズ」を楽しみたい方は答えがすぐに思い浮かばない問題にも、しばらくは自分の既存の知識と思考、それから wandbox でコード片を試すなどしてできるだけ答えを "現時点での自力" で見出そうとするとすぐにググってしまうよりも楽しく、また分からなかった問題についても知識として身につく事も増えるかもしれません。

もくじ

  1. float 型の [ 0.0 .. 1.0 ] の値を uint8 型の [ 0 .. 256 )写像したかった
  2. 意図しない整数のオーバーフローと問題の遮蔽
  3. 浮動小数点数
  4. 遅すぎた 2 の指数 [ 1, 2, 4, 8, 16, 32, .. ] の判定
  5. 正弦と余弦も速くしたい
  6. ニアリーイコール、再び
  7. 人間が読みやすい "数値" にしたかった
  8. 整数型、再びトラブる
  9. G.C.D.
  10. 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 型の 0255 は巾筒な "幅" に写像されたものだろうか。

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. FRunnableThreadstd::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 の生スレッドの生成は少々面倒くさい。しかし、 FRunnableThreadSuspend()Kill() をメンバー関数として持っているので少しだけ高機能。

以上のようにして "野生のスレッド" を放つ事は UE4 のライブラリー機能としてもサポートされているが、実用上はそのような用途では std 実装を使った方が楽で future の待ち方にも工夫でき面倒も無くて良いだろう。もちろん、 std::promiseFRunnableThread を組み合わせても問題は無い。

UE4 では "野生のスレッド" ではなく、フレームワークが管理するメインスレッドとワーカースレッド群へ FAutoDeleteAsyncTask を用いてフレームワークの制御下で非同期タスクを実行もできる。それについては以下の記事が参考になるだろう。FAsyncTaskFAutoDelegateAsyncTask についてはそちらを参照すると良いだろう。

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:

    • FThreadSafeBoolFThreadSafeCounter を継承して定義され、内部的には int3201 のみを扱うよう制限して実現している。
    • FThreadSafeCounterFThreadSafeCounter64 は内部的には FPlatformAtomics::InterlockedAdd などに実装を依存している。
      • 実装詳細は HAL/PlatformAtomics.h で定義され、もし気になるならば、 Runtime/Core/Public/Windows/WindowsPlatformAtomics.h などを参照すると善い。
        • 例えば Windows の場合は最終的には <intrin.h> から Win32 API::_InterlockedExchangeAdd 等が使われる。(MSDN)
    • int32, int64 に対応しているので、一応実質的にはこれらでも uint16void* も扱えないことは無い…

UE4 のアトミックを用いても UPROPERTY()UFUNCTION() では使用できず特にメリットも無いので、ツールチェインの std 実装が信用できる場合は std のアトミックを使えば良い。

参考:

  1. atomic - cpprefjp C++日本語リファレンス
  2. FThreadSafeBool | Unreal Engine
  3. FThreadSafeCounter | Unreal Engine
  4. 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 でもそれらを使用する事は可能だが、 簡単なループ並列化については UE4ParallelFor を用意している。本節では簡単で擬似的なソースコードC++17 Parallelism, OpenMP, Intel TBB, Microsoft PPL について同等のコードを例示的に整理する。

1.13.1. ParallelForfor_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 );
}

UE4C++ で一般的に用いられる事の多い多くのライブラリー等に比べてより単純な ParallelFor しか高レベル並列化機能を提供していないが、基本的には [ 0 .. N ) のレンジの並列化そのままで多くの場合には十分かつ必要に応じて少し与えるレンジやファンクターのキャプチャーに工夫をすればたいていの用途には簡単に応用できるため多くの場合には必要十分となる。

それでは必要不十分という場合には UE4 のタスク制御機能を応用したり、必要に応じてライブラリーを導入すると良い。また、恐らくそう遠く無く C++17 Parallelism 対応の開発環境も UE4 での開発環境一般としても、特に PC 向けであれば使用可能となるので、近い未来にはアルゴリズムについてはそちらも必要に応じて使用すると良い。

なお、 ParallelFor には例では省略した最後の bool 型の引数が1つあり、デフォルトの false では並列実行されるが、 true を与えると逐次実行に動作が切り替わる機能もあり、これがデバッグに有用な事もしばしばあり便利である。

但し、 UE4ParallelFor については扱われる index が int32 型である点に気をつける必要がある。 C++ 界の一般的な並行処理ライブラリーでは index は size_t 型である。

参考:

  1. C++ 標準の promise / future / thread に対応する UE4 標準の TPromise / TFuture / FRunnableThread の使い方 - C++ ときどき ごはん、わりとてぃーぶれいく☆
  2. future - cpprefjp C++日本語リファレンス
  3. thread - cpprefjp C++日本語リファレンス
  4. 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 ならこちらを使うべきという程の強い動機はない。 TFunctionRefUE4 にしかない機能だが、ユーザーにとってはこちらもこれという程の使い所も無いだろう。

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();
}

参考:

  1. FReferenceFinder | Unreal Engine
  2. [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> を用いておけば面倒が無い。

参考:

  1. TWeakObjectPtr | Unreal Engine
  2. TAutoWeakObjectPtr | Unreal Engine
  3. UWorld::ForceGarbageCollection | Unreal Engine
  4. 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 の挙動は次の通り:

  1. ReadLock -> ReadLock: 可能
    • ReadLock した回数だけ ReadUnlock されるとロック状態が解除される
  2. ReadLock -> WriteLock: 解錠待ち(=ブロッキングデッドロック注意)
  3. WriteLock -> WriteLock 解錠待ち(=ブロッキングデッドロック注意)
  4. WriteLock -> ReadLock 解錠待ち(=ブロッキングデッドロック注意)
  5. 過解錠の挙動はプラットフォーム依存(≃落ちる)

参考:

  1. shared_mutex - cpprefjp C++日本語リファレンス
  2. shared_timed_mutex - cpprefjp C++日本語リファレンス
  3. shared_lock - cpprefjp C++日本語リファレンス
  4. lock_guard - cpprefjp C++日本語リファレンス
  5. C++17, C++14, C++11 に併せた std::shared_mutex, std::shared_timed_mutex, std::mutex から mutex_type, read_lock_type, write_lock_type を扱う例 - Qiita
  6. 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 ならこの程度のコードは難しくなく読めると思う。一応簡単に解説する。

  1. UE4 の HTTP Client 機能は 7 段階でリクエスト:
    1. FHttpModule::Get // Note: FHttpModule は singleton
    2. FHttpModule::CreateRequest
    3. IHttpRequest::OnProcessRequestComplete
    4. IHttpRequest::SetURL
    5. IHttpRequest::SetVerb
    6. IHttpRequest::SetHeader
    7. IHttpRequest::ProcessRequest
  2. 実装上の工夫として usagi::GetHTTP では:
    1. url と decoder を実引数で渡す
      • decoder は Get 結果(テキストとは限らない)を望むオブジェクトへ変換するファンクター
    2. 関数自体は Future を返す
      • 呼び出し元は Future::IsReady を確認して Future::Get する

改良の余地はあるが、あまり複雑化しても 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 を扱うためのライブラリーが実装されている。少々癖があるが、 UE4JSON を扱うならば一度試す価値はある。

2.4.1. FJsonValue 抽象型

UE4JSON ライブラリーは、コード例を示す前に少し解説が必要と思われる。

全てはここから始まる。

  1. FJsonValue は抽象型なのでそのオブジェクトは存在しない。
  2. FJsonValue から派生した -Array, -Boolean, -Null, -Number, -Object, -String の 6 つの型が具体的な JSON 値を保持するオブジェクトとして存在可能。
  3. TSharedPtr< FJsonValue > value = MakeSharable( new FJsonValueNull() ); の様に使う。
    • Null 以外は ctor で値を放り込める。
    • 但し、 Array は TArray< TSharedPtr< FJsonValue > > 、 Object は TSharedPtr< FJsonObject > を放り込む必要がある。
      • FJsonObject は public メンバーに TMap< FString, TSharedPtr< FJsonValue > > を持ち、 key-valueJSON の値として便利にキャストする機能を持った特別の型

理屈としては以上で JSON のオブジェクトを UE4/C++ 上のオブジェクトとして表現する。加えて、 FJsonValueFJsonObject は値をキャストして取得する機能があり、これによって具体的な値の取得も便利に可能となっている。

但し、 { "a": { "b": [ { "c": 123 } ] } }` に対して "a.b[0].c" のようにパスでアクセスする機能などハイカラなものは無いので、地味にこつこつオブジェクトの組み立てと戦いながら扱う必要がある。

また、 UE4JSON ライブラリーはモジュール化されているため、 "{your-project-name}.Build.cs" の PublicDependencyModuleNames.AddRange, "Json", "JsonUtilities" を追加しておく必要がある点にも注意されたい。

2.4.2. UE4/C++ JSON ライブラリーによる読み書き

C++er には旧くは picojson や json11 などの JSON ライブラリーが、また最近では nlohman-json ライブラリーの利用も増えている。実際問題、 C++ のオブジェクトとして JSON を扱いたいだけならばそれらの方が扱いやすくて楽だが、本記事の趣旨の都合、 UE4JSON ライブラリーを使った 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 ) );
}

参考:

  1. Dom | Unreal Engine

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. エンジンプラグインのリビルド

  1. エンジンプラグインディレクトリーごと 7z でもしてバックアップしてからソースの改変作業を開始する。
  2. ソースを改変する。
  3. リビルドしてパッケージを適当な 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"
  4. エンジンプラグインディレクトリーの中身をリビルドした中身へ挿げ替える。

おわりに

§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> は地味に扱いが面倒。 UE4C++17 標準対応完了すれば要らなくなる子。( TArray のようにBlueprintでも使用可能になるなどすればまた別だが・・・)
TOptional
成功値の型と失敗値の型を指定可能な TOptional のごつくなったようなやつ TValueOrError
補間 FInterpCurve
FInterpCurveFloat
FInterpCurveLinearColor
FInterpCurvePoint
FInterpCurveQuat
FInterpCurveTwoVectors
FInterpCurveVector
FInterpCurveVector2D注:FInterpCurveF 始まりの型だが 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 型 AddContains など集合に対する操作を行える。 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::queueTArray を対応させ、機能の有無や対応関係がわかりやすいよう整理する。また、 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 を扱う上でのポイントは以下の通り:

  1. deque | vector | stack | queue | SORT | HEAP | FINDTArray
  2. sizeNum
  3. resizeSetNum
  4. reserveReserve
  5. empty()Num() > 0clear()Empty() の混乱回避。
  6. std::remove + erase ( erase-remove idiom ) ≃ Remove
  7. dataGetData
  8. TArrayUPROPERTY() 可能。
  9. std アルゴリズムのレンジは生ポインターでも動作するので TArray にも適用はできる。
  10. std::vector における reservecapacity で扱う内部バッファーを 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 にもそれらと同じ関係の TMapTSet がある。この記事は網羅的なクラスや 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 のポイントは次の通り:

  1. unordered_mapoperator[] は存在しない要素は生成してくれるが、 TMapoperator[] は存在しない要素は生成してくれない(実行時エラーとなる)。その用途には FindRef を使う。
  2. TMap は KEY, VALUE によるソートが可能。
  3. メンバーの命名 → TArray と同様の注意が必要。
  4. UPROPERTY() 可能。
  5. インデックスアクセスも可能な連想配列として振る舞う(もちろん表面上は似てはいるが 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 にとって特に互換性のある標準機能は無く、さしあたりこうした機能群が UE4FMath に存在するという予備知識が得られれば十分である事などを理由に、本項では関数の概要を紹介するに留める。

効果 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の指数倍か判定
実装詳細の都合、負の値は false0, 1true を返す。
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 整数に拡張して行われるため、 int8a=-128, b=0, Alpha=1.0 を与えると b-a=+128 となり、結果的には (T)(a+Alpha*128) はオーバーフローして 0 となるから写像としては正しい結果が得られる。しかし、 int32a, b を同様に与える場合には b-a の時点でオーバーフローし 0 となるため (T)(a+Alpha*0)Alpha=1.0 でもaとなる。これはint64等でも同様に発生する。LerpStableはこの問題の発生しない実装。なお、LerpLerpStableの速度差は 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
InterpEaseInInterpEaseOut を1:1で合成 InterpEaseInOut
値域 [0..1] を分解能 Step で分割した離散値へ入力値 Alpha 吸着し値域 [a..b]写像 `InterpStep
値域 [0..1] で表される比の入力値 Alpha を-cos(Alpha*π/2)+1 関数で補正し値域 [a..b]写像 InterpSinIn
1 - InterpSinInと等価 InterpSinOut
InterpSinInInterpSinOut を1:1で合成と等価 InterpSinInOut
値域 [0..1] で表される比の入力値 Alpha を-exp2(-10*Alpha)+1 関数で補正し値域 [a..b]写像 InterpExpoIn
1 - InterpExpoIn InterpExpoOut
InterpEaseInInterpEaseOut を1:1で合成 InterpExpoInOut
値域 [0..1] で表される比の入力値を円関数で補正し値域 [a..b]写像 `InterpCircularIn
1 - InterpCircularIn InterpCircularOut
InterpCircularInInterpCircularOut を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乗
PointDistToSegmentsqrt を要する分だけこちらの方が高速
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の拡大縮小と floatMagnitude から単一の拡大縮小倍率を 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> では対応が困難な機能は、外部ライブラリーを取り込んで使えば良い。
    • 例えば UE4四元数float 実装しかないので double が必要なら Eigen::Quaterniond の使用を検討するなど。

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

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

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

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

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

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

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

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

1.9.2. FMath における Generic Platform 層

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1.9.3. FMath における Platform Native 層

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

0. はじめに

この記事は Unreal Engine 4 (UE4) その2 Advent Calendar 2017 の DAY 1 に寄稿したものです。😃

1. 概要

std 慣れしたプロフェッショナルとしてある程度の経験と知識のあるベテランの C++er でも UE4 フレームワークC++ ソースコードと対峙する上では、事前に知っておくと混乱の予防接種となるコツは多く、 API Reference や公式の解説も入門向けには整備が善いが、 C++er 向けには十分とは言い難い。

そこで、本記事では一般的なプロフェッショナルレベルの C++er を対象に、2章構成で次の内容を記す。

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

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

背景

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

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

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

執筆時点の環境等

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

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

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

目次

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

本論

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

1.1. UE4/C++ の数値型と std 互換性

1.1.1. 数値型

C++11 以降で取り込まれた int8_t などのライブラリー定義と同様の型の定義が UE4 では int8 のように行われ使用できる。

所属 Header
std int8_t, int_fast8_t など <cstdint>
std size_t, nullptr_t <cstddef>
UE4 int8, int32 など Runtime/Core/Public/GenericPlatform/GenericPlatform.h

std と UE4 それぞれで具体的に定義される型の対応関係は以下の通り。

type std UE4
unsigned char uint8_t uint8
CHAR8
unsigned short int uint16_t uint16
CHAR16
unsigned int uint32_t uint32CHAR32
TYPE_OF_NULL
unsigned long long uint64_t uint64
signed char int8_t int8
ANSICHAR
signed short int int16_t int16
signed int int32_t int32
signed long long int64_t int64
wchar_t WIDECHAR
decltype(nullptr) nullptr_t TYPE_OF_NULLPTR

整数以外については、言語組み込み型の float, double, long double に対応する UE4 の独自定義は無く、 UE4 でも必要に応じて言語組み込み型をそのまま使う。

UE4 がライブラリー定義する整数型のうち uin8, int32 それに言語組み込み型の単精度浮動小数点数型の float は UE4Editor / Blueprint でも使用可能な UPROPERTY()UFUNCTION() で直接使用可能な型として重要となる。 UPROPERTY(), UFUNCTION() については §1.3 で触れるので必要に応じて参照されたい。

UE4/C++ でも一般には任意の型を用いて構わないが、整数型については UE4 ライブラリーの定義する型は名前も簡便かつ直感的なため、翻訳単位として UE4 から独立したソースコードを除いては UE4 ライブラリーの定義する整数型のエイリアスを使うと善い。

1.1.2. TNumericLimits と std::numeric_limits

一般的な C++er は少々冗長であっても製品の必要に応じてしばしば <limits> に定義される std::numeric_limits<T> を用いる。

UE4 にも対応するライブラリー実装が "一応" ある。

所属 Header
std numeric_limits<T> <limits>
UE4 TNumericLimits<T> Runtime/Core/Public/Math/NumericLimits.h

numeric_limits<T> については std を使っておけば善い。その理由としては以下を挙げる。

  1. TNumericLimits<T> には Min(), Max(), Lowest() の3つの constexpr 関数しか定義されていない。用途にもよるが C++er が一般に <limits> を用いる際に必要な多くの機能が不足している。
  2. TNumericLimits<T> の template 特殊化は std 版より少なく、例えば long double, bool, char, wchar_t 等に汎用的に使用できないため、それらの使用頻度が例えマイナーであったとしても必要となった際にそれらについてだけ std 版を使うのはプロジェクト単位でのソースコードの整合性を欠く可能性がある。

さしあたり、 UE4 版もあるが std 版よりも優先的に使う意味が無い事だけ覚えておくと混乱を避ける意味で善い。

1.2. UE4/C++ の文字・文字列型と std 互換性

1.2.1. UE4 の文字エンコーディング

C++11 以降 C++er は char, wchar_t, char16_t, char32_t の文字型を用いて製品のプラットフォームに併せて、あるいは言語に併せて高度な文字エンコーディングの対応を行える。

UE4 では文字エンコーディングUTF-16LE で扱う。但し 基本多言語面 のコードポイントのみに限定されサロゲートペアの必要な文字は対象としない。

このため、 UE4Editor は新たに C++ クラスを作成するとファイルの文字エンコーディングを UTF-16LE で生成してくれる。この事は UE4Editor に頼らず独自にシェルから、あるいはお好みのテキストエディターからソースコードファイルを作成する際、特に日本語を扱う可能性がある製品においては注意する必要がある。

UE4 では C++ 標準の wchar_tUE4 ライブラリー定義の WIDECHARUTF-16LE の文字を扱い、 C++ レベルでの互換性のため補助的に charUE4 ライブラリー定義の ANSICHAR との変換もサポートしている。なお、 UE4 では char16_t を使用していない。

// UE4 文字
wchar_t moji_1 = L'あ';
TCHAR moji_2 = TEXT( 'あ' );
// UE4 生の文字列
wchar_t moji_retsu_1[] = L"あいう\0";
TCHAR moji_retsu_2[] = TEXT( "あいう\0" );
// 一般には Windows SDK で定義される TCHAR / TEXT(X) 相当する定義が UE4 でも提供され、
// 少なくとも UE4 の開発では TCHAR / TEXT(X) は wchar_t / L ## X に展開される。

C++er レベルではこの知識はしばしば UE4 の文字列と他の系での文字列との相互変換が必要な際の基本知識として役立つ。但し、多くの C++er が今時時別な理由も無く生の文字列を扱わずに std::basic_string<T> を使うように、 UE4 ではそれに対応する FString を用いる事になる。 FString については次項を参照されたい。

また、本件についてより詳しくは日本語の丁寧な公式の解説 文字列の取り扱い/文字エンコード も参照されたい。

1.2.2. FStringstd::basic_string<T>

所属 Header
std basic_string<T> <string>
UE4 FString Runtime/Core/Public/Containers/UnrealString.h

C++er が一般的に文字列に対して basic_string<T> を用いる事に対応するように、UE4 ではそれに当たる文字列型として FString を用いる。 UE4 を扱うソースコードでは文字列は基本的に FString で扱うと考えて善い。

UE4FString は内部的には wchar_t ベースの文字列を扱い、 basic_string<T> よりもやや高機能に作られている。 wchar_t (≃ TCHARWIDECHAR )文字列からの ctor のほか、 char (= ANSICHAR ) 文字列からの ctor も備えているので UE4 フレームワークの下層に char, std::string 実装しか文字列出力の無いライブラリーを使用する際も、 "概ね" は、その出力を UE4 フレームワーク側へ持ち込むためにややこしい文字列変換処理を書く必要は無い。

機能 std::basic_string<T> FString
ctor( const char* ) T=char なら可 可能
ctor( const wchar_t* ) T=wchar_t なら可 可能
ctor( const char16_t* ) T=char16_t なら可
ctor( const char32_t* ) T=char32_t なら可
整数を文字列化した文字列オブジェクトを得る std::to_string FromInt
浮動小数点数型を文字列化した文字列オブジェクトを生成する std::to_string SanitizeFloat
文字を文字列オブジェクトへ変換する Chr
文字を指定回数繰り返した文字列オブジェクトへ変換する ChrN
{0} から始まる配置インデックス文字列を置換するフォーマット文字列を生成する Format
printf互換のフォーマット文字列を生成する Printf
文字列オブジェクトの配列を結合した文字列オブジェクトを得る Join
文字列を挿げ替える operator=
assign
operator=
インデックス位置の文字を取得 operator[] operator[]
True, Yes, On / False, No, Off 文字列から bool 値を得る ToBool
整数値をカンマ桁区切り付きで文字列オブジェクト化する FormatAsNumber
文字列を追加 operator+=
append
operator+=
Append
AppendChars
文字を追加 operator+=
append
push_back
operator+=
AppendChar
整数を文字列化して追加 std::to_string + operator+= AppendInt
パス区切り文字を考慮した文字列の追加を行う operator/=
PathAppend
文字・文字列を内挿する insert InsertAt
範囲の文字を削除 erase RemoveAt
終端から文字列を削除 RemoveFromStart
先頭から文字列を削除 RemoveFromEnd
文字列を空にする clear Empty
Reset
タブ文字を空白文字へ置き換える ConvertTabsToSpaces
順方向イテレーター先頭を取得 begin CreateIterator
順方向イテレーター終端を得る end
逆方向イテレーター先頭を得る rbegin
逆方向イテレーター終端を得る rend
const 順方向イテレーター先頭を取得 cbegin CreateConstIterator
const 順方向イテレーター終端を得る cend
const 逆方向イテレーター先頭を得る crbegin
const 逆方向イテレーター終端を得る crend
先頭から文字列を検索 find Find
Contains
終端から文字列を検索 rfind Find
先頭から文字を検索 find_first_of FindChar
終端から文字を検索 find_last_of FindLastChar
先頭から文字を否定検索 find_first_not_of
終端から文字を否定検索 find_last_not_of
先頭からファンクターを用いて検索 FindLastCharByPredicate
ワイルドカード文字(*,?)を含む文字列に合致するか判定する MatchesWildcard
空か判定する empty IsEmpty
インデックスが範囲内か判定する IsValidIndex
数字のみからなる文字列を格納しているか判定する IsNumeric
他の文字列と比較 compare Compare
文字列が等しいか判定する operator== operator==
Equals
文字列先頭の部分文字列を比較する StartsWith
文字列終端の部分文字列を比較する EndsWith
文字列先頭から部分文字列を指定文字数だけ取得する Left
文字列先頭から部分文字列を{長さ-指定文字数}だけ取得する LeftChop
文字列終端から部分文字列を指定文字数だけ取得する Right
文字列終端から部分文字列を{長さ-指定文字数}だけ取得する RightChop
部分文字列を取得 substr Mid
部分文字列を文字列で置換した文字列を生成する Replace
文字列範囲を文字列で置換する replace
部分文字列を文字列で置換する (find+replace) ReplaceInline
先頭に空白文字を追加した文字列オブジェクトを生成する LeftPad
終端に空白文字を追加した文字列オブジェクトを生成する RightPad
\\, \n, /r, \t, \', \" についてエスケープ処理した文字列を生成する ReplaceCharWithEscapedChar
\\, \n, /r, \t, \', \" についてエスケープ解除した文字列を生成する ReplaceEscapedCharWithChar
ダブルクォート文字 "エスケープ処理した文字列を生成する ReplaceQuotesWithEscapedQuotes
小文字化した文字列オブジェクトを生成する std::transform + std::tolower ToLower
小文字化する ToLowerInline
大文字化した文字列オブジェクトを生成する std::transform + std::toupper ToUpper
大文字化する ToUpperInline
先頭から空白文字群を削除した文字列オブジェクトを生成する TrimStart
先頭から空白文字群を削除する TrimStartInline
Trim
終端から空白文字群を削除した文字列オブジェクトを生成する TrimEnd
終端から空白文字群を削除する TrimEndInline
TrimTrailing
先頭と終端の両方から空白文字群を削除した文字列オブジェクトを生成する TrimStartAndEnd
先頭と終端の両方から空白文字群を削除する TrimStartAndEndInline
終端から null 文字 \0 を削除する TrimToNullTerminator
部分文字列で分割した左側・右側からなる2つの文字列を取得する Split
バイト列を10進数で文字列化した文字列オブジェクトを得る FromBlob
バイト列を16進数で文字列化した文字列オブジェクトを得る FromHexBlob
10進数ブロブ文字列をバイト列領域へ書き出す ToBlob
16進数ブロブ文字列をバイト列領域へ書き出す ToHexBlob
先頭と終端の両方からダブルクォート文字 " を除去した文字列オブジェクトを生成する TrimQuotes
逆方向に読み出した文字列を生成する Reverse
逆方向に読み出した文字列に置換する ReverseString
ANSI文字配列へシリアライズする SerializeAsANSICharArray
分割文字列を指定して文字列を文字列の配列に分割する ParseIntoArray
復帰文字・改行文字群により文字列を文字列の配列に分割する ParseIntoArrayLines
ホワイトスペースと任意の分割文字群を指定して文字列を文字列の配列に分割する ParseIntoArrayWS
文字列オブジェクト配列から空の文字列を除去する CullArray
文字数を取得 size Len
保持可能な最大文字数を取得する max_size
文字列の長さを変更する resize
内部バッファーの容量を得る capacity GetAllocatedSize
内部バッファーのポインターを得る data
c_str
operator*()
GetCharArray
内部バッファーの容量を指定する reserve
内部バッファーを使用中の文字数へ切り詰める shrink_to_fit Shrink
他のオブジェクトと中身を入れ替える swap
アロケーターを取得 get_allocator
生の文字列を対象領域へ複製する copy

できるだけ前後で似た機能が並ぶように整理した。 UE4 のライブラリーは一部のメンバーの命名が C++ 標準に対して紛らわしかったり、そもそも命名が下手なものがあり、注意を要したり、似たような関数名で効果を混乱する恐れがあるものがある。

他のコンテナー類にも共通する C++ 標準に対して紛らわしいメンバーの代表格は次の2つ。

  1. std emptyUE4 IsEmpty
  2. std clearUE4 Empty

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

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

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

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

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

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

話が軽くそれかけけたが、ともあれ UE4FStringstd::basic_string に比べ平易に扱いやすく多機能に作られていて便利が良い。また、数値型でも触れた UPROPERTY() などの指定による UE4Editor / Blueprint で使用可能な文字列型でもある。 std::basic_string は残念ながら UE4Editor / Blueprint では直接扱えないので、それを用いる場合には必要に応じて FString( my_std_string.c_str() ) のような薄いラッピングが必要になる。

少なくとも std 版文字列にある機能に対して不足を感じる事は無いし、もし、そういった事があれば生の文字列を経由したり、ポインターのレンジを <algorithm> で処理するなどの対応もできる。特別な理由が無い限り、 UE4 プロジェクトでの文字列型は FString を使うと考えて良い。

1.2.3. UE4 の文字列型と用途

UE4 の文字列型として前項で FStringstd::basic_string と比較して紹介した。その他にも UE4 ではおよそ文字列に相当するオブジェクトが用途別にいくつか定義されている。

用途と特徴 Header
一般的な文字列。C++erが文字列操作を行う場合はおおよそこれを中心として使い、必要なら他の文字列型とは変換して扱う。 FString Runtime/Core/Public/Containers/UnrealString.h
高速・軽量な参照中心の用途向け。開発・実行中に頻繁に使用されるアセット名などで使われる。大文字小文字を区別しない。 FName Runtime/Core/Public/UObject/NameTypes.h
エンドユーザーが直接入出力する用途向け。安全のため原則的に const な設計。 FText Runtime/Core/Public/Internationalization/Text.h
FString の実装詳細でも一部使用される <cstring> 互換・ラッパー層。さしあたりはいちおうあるよ、程度の認識で構わない。 FCString Runtime/Core/Public/CString.h

これらについて概要以上の知識が必要になった際には公式ドキュメントの 文字列の取り扱い を読むと善い。 FCString については API Reference の Runtime/Core/Public/CString.h から TCString を辿ると善い。 FCString::Atof など時折有用な事がある。

1.2.4. FString のワイルドカードマッチと正規表現

C++ 標準には文字列の「ワイルドカード」マッチ機能は無いが、「正規表現」は std::regex がある。

UE4 で頻繁に用いる文字列型 FString にはメンバー関数実装のワイルドカードマッチがある。また、 FString を用いた正規表現を行う FRegexPattern / FRegexMatcher がある。

基本的な扱い方は以下の通り。

// std regex
auto example_std_regex( const std::wstring& input )
{
  std::wregex pattern( L"^hoge.*piyo$" );
  std::wcout
    << L"std regex: input={" << input << L"} match={"
    << std::boolalpha << std::regex_match( input, result, pattern )
    << L'}' << std::endl;
}

int main()
{
  example_std_regex( L"hoge fuga piyo" );
  example_std_regex( L"hoge fuga piyo fuga" );
}
std regex: input={hoge fuga piyo} match={true}
std regex: input={hoge fuga piyo fuga} match={false}

同様に UE4 では std::cerr に替えて UE4 標準のログ出力マクロを使用すると:

// UE4 FString::MatchesWildcard
auto example_ue4_wildcard( const FString& input )
{
  FString pattern( TEXT( "hoge*piyo$" ) );
  UE_LOG
  ( LogTemp, Log
  , TEXT( "UE4 FString::MatchesWildcard: input={%s} match={%s}" )
  , *input
  , input.MatchesWildcard( pattern ) ? TEXT( "true" ) : TEXT( "false" )
  );
}
// UE4 FRegexPattern / FRegexMatcher
auto example_ue4_regex( const std::wstring& input )
{
  FString input;
  FRegexPattern pattern( TEXT( "^hoge.*piyo$" ) );
  FRegexMatcher m( pattern, input );
  UE_LOG
  ( LogTemp, Log
  , TEXT( "UE4 FString::MatchesWildcard: input={%s} match={%s}" )
  , *input
  , m.FindNext() ? TEXT( "true" ) : TEXT( "false" )
  );
}

注意点として、 FString::MatchesWildcardAPI Reference でも警告が表示されているとおり、一般に速度重視の UE4 のライブラリー機能としては例外的に「遅い」実装に留まっている。? * 式のワイルドカードはパターンの記述が簡単という点では便利だが、特に速度的に不利な要因となりそうな場合には可能な他の合致判定を採用するのが良い。実際、ソースを眺めると速度最適化に努力されていないことがわかるので興味があれば " Runtime/Core/Private/Containers/String.cpp" を覗いてみると良い。

参考として以下に著者環境で同じ UE4 プロジェクト、同じ実行条件で std::chrono::steady_clock::now() で得られる時刻が開始から 1 sec 未満の間、 while ループで入力値 L"hoge fuga piyo" に対してパターン L"?oge * piy?" または等価な正規表現での合致判定を何回行えるか、アバウトに計測したデータを示す。

出処 method #/sec (O0) #/sec (O2) vs. (O2)
UE4 MatchesWildcard 373,411 382,446 1.000
UE4 FRegexPattern + FRegexMatcher 90,041 86,302 0.226
UE4 static FRegexPattern + FRegexMatcher 419,414 439,156 1.148
std std::wregex + std::regex_match 169,407 345,582 0.904
std static std::wregex + std::regex_match 294,024 968,259 2.532
Mehrdad wildcard 3,770,052 8,557,528 22.376
Win32 PathMatchSpec 960,855 1,150,534 3.008

この表だけ掲載すれば一般的な C++er には関連の実行コスト事情、何をどう使うべきか理解できたと思う。なお、この "アバウトな計測" のソースは次のように実装している。

// FString の構築コストは計測外
FString input( L"hoge fuga piyo" );
FString wildcard_pattern( L"?oge * piy?" );
FString regex_pattern( L".oge .* piy." );
// UE4 FString::MachesWildcard の例
{
  uint64 count = 0;
  const auto end = std::chrono::steady_clock::now() + std::chrono::seconds( 1 );
  while ( std::chrono::steady_clock::now() < end )
  {
    input.MatchesWildcard( wildcard_pattern );
    ++count;
  }
  UE_LOG( LogTemp, Log, TEXT( "count(wildcard)={%d}" ), count );
}

同様のアバウトな計測を合致判定コアをそれぞれ次のように変えたものを用意した。

// UE4 Regex
FRegexPattern p( regex_pattern );
FRegexMatcher m( p, input );
m.FindNext();
// UE4 Regex(static)
static FRegexPattern p( regex_pattern );
FRegexMatcher m( p, input );
m.FindNext();
// std regex
#include <regex>
std::wregex p( *regex_pattern );
std::regex_match( *input, p );
// std regex(static)
#include <regex>
static std::wregex p( *regex_pattern );
std::regex_match( *input, p );
// Mehrdad wildcard
// https://stackoverflow.com/questions/3300419/file-name-matching-with-wildcard/12231681#12231681
wildcard( *wildcard_pattern, *input );
// win32 PathMatchSpec
#include <Shlwapi.h>
#pragma comment(lib, "shlwapi.lib")
PathMatchSpec( *input, *wildcard_pattern );

ワイルドカードの合致判定処理くらい、いくらでも誰かが公開してくれたものがあるだろうと stackoverflow を眺めたところ Mehrdad の「どうぞご自由に」とライセンスされたコードが使いやすくバグも無さそうだったので比較に入れてみたら桁違いにさいつよ過ぎた。

ちなみに、本項とは直接関係ないが、表では計測を O0 と O2 で行った事になっている。これは "{your-project-name}.Build.cs" へ次のように明示的に翻訳時の最適化制御するオプションを設定すれば簡単に固定(変更)できる。

// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する
// O0
OptimizeCode = CodeOptimization.Never;
// O2
OptimizeCode = CodeOptimization.Always;

この設定は他にも Enum があるので必要に応じてエンジンソースの ModuleRules.cs public enum CodeOptimization を参照すると良い。(残念ながらこの周りの機能はしばしば更新されており、読み物としてのドキュメントでは設定方法やフラグのシンボル名が微妙に違っていたりしてあまり役に立たない。)

1.3. UE4/C++ における Unreal Header Tool の役割と制約

UE4/C++ コードを含むプロジェクトのビルドプロセスで、 C++ ツールチェインの前に UE4ソースコード群、特にヘッダーファイル群に対して前処理を行うツール。AnswerHub などでは略称の UHT と記載される事もままある。

1.3.1. Unreal Header Tool の役割

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

1.3.2. Unreal Header Tool の制約

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

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

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

UHT は完全に近い C++ の機能に対する互換性の対応よりも実用的な UE4 での C++ コード親和性の現実的なコストでの必要最小限程度の確保を目的としている様子がある。このため、リフレクションの設計都合ではなくパーサーが貧弱なせいでは、あるいは UE4 エンジン側でそう多くないコストで実装・対応できますよね、と思うような制約も発生するが、慣れるしかない。諦めろ・x・

1.3.3. ENUM のフラグ化とリフレクションの恩恵

C++er にとって C# などの言語を羨ましく感じる事の1つに enum class の不便がある。 Unreal Header Tool で前処理する UENUM() ではそんな不便を少しばかり解消できる機能もある。

1つには、 enum class のビットフラグ化。これについては下記の記事に以前書いたので興味があればそちらを参照して欲しい。

ここでは、もう1つ、リフレクションの恩恵を紹介する。

// definition
UENUM()
enum class ESomething: uint8
{ aaa, bbb, ccc };

// enum class -> string
FString to_string( ESomething in )
{
  UEnum* pe = FindObject< UEnum >( ANY_PACKAGE, L"ESomething" );
  check( pe );
  return pe->GetNameStringByIndex( (int32)in ) );
}

// string -> enum class
ESomething to_something( const FString& in )
{
  UEnum* pe = FindObject< UEnum >( ANY_PACKAGE, L"ESomething" );
  check( pe )
  auto n = pe->GetValueByName( FName( *in ) );
  check( n != INDEX_NONE )
  return (ESomething)n;
}

諸事情により本記事のボリュームを修正したところ、書くところが無くなったのでここでついでのように触れた。興味があれば API Reference の UEnum を参照すると良い。

1.4. UE4/C++ における入門的な「メモリーアロケーションガベージコレクション

一般的な C++er が UE4/C++er にクラスチェンジする場合、往々にして UE4ガベージコレクションUE4 オブジェクトの生成について知っておく必要があります。

1.4.1. NewObjectCreateDefaultSubObject 及び C++new の違い

一般的な C++er は実行時のメモリーアロケーション(ヒープアロケーション)に new あるいは事情によっては malloc を使用する。

UE4 ではライブラリー、フレームワークとしてのレベルでガベージコレクションをサポートする都合から、 NewObject<T> 関数、またその亜種とも言える CreateDefaultSubobject<T> を使用する機会が頻出する。

UE4/C++ においてメモリーアロケーションに使う関数の基本的な選択基準は次の通り:

対象 使用箇所 関数
UHT の UCLASS() マクロ付きの UObject 派生型 UObject 派生型の ctor ではない場所 NewObject<T>
UHT の UCLASS() マクロ付きの UObject 派生型 UObject 派生型の ctor CreateDefaultSubobject<T>
その他の型 どこでも new

UE4 プロジェクトの C++ コードでも言語組み込みの new を使う事はまま有り得る。それについては通常の C++ 一般と同様の状況となる。これについては本記事では特に解説しないが、もしそのような機会がある場合には、 "Public/HAL/UnrealMemory.h" に定義される UE4 版の Malloc, Realloc, Free ベースの低レベルのメモリー管理機能について知っておくと役立つかもしれない。 UE4 の用途では C++new, delete は仮想関数テーブルやコンストラクター、デストラクターのコストが余分な事もままある点についても留意したい。

NewObject<T>CreateDefaultSubobject<T> について C++er が注意する点は次の通り:

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

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

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

1.4.2. UActorComponentRegisterComponent 及び AttachToComponent または SetupAttachment

UE4C++er が NewObject<T>, CreateDefaultSubobject<T> する機会が多い型に UObject 派生型の UActorComponent 型の派生型 UStaticMeshComponent, URadialForceComponent, UAudioComponent のような「コンポーネント派生型」がある。本稿とは直接は関係無いが C++er が UE4/C++er になるにあたり誤解しかねない点があるので、 NewObject<T>, CreateDefaultSubobject<T> と併せて補足する。

多くの場合、 NewObject<T> したコンポーネントのオブジェクトは RegisterComponent を直後に行う。 RegisterComponent について C++er が理解しておくと良い注意点は次の通り。

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

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

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

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

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

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

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

さしあたり §1 チュートリアル&予防接種 としてはこの程度の解説に留める。より詳細なガベージコレクターの挙動や管理については §2.1 を参照されたい。

1.5. UE4/C++ における「ムーブセマンティクス」

C++er によって C++11 以降は既に常識となった機能の1つに右辺値参照とムーブセマンティクスがあある。

UE4 では std の move に対応する MoveTemp がライブラリー機能として用意されているが、 std::move とは挙動が異なるため C++er が UE4/C++er にクラスチェンジするために必要な知識について触れる。

1.5.1. MoveTemp, MoveCopystd::move

所属 関数 Header
std move <memory>
std forward <memory>
UE4 MoveTemp Runtime/Core/Public/Templates/UnrealTemplate.h
UE4 CopyTemp Runtime/Core/Public/Templates/UnrealTemplate.h
UE4 Forward Runtime/Core/Public/Templates/UnrealTemplate.h

まず、 std::moveMoveTemp の挙動の違いについて触れる。

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

何れも基本的な機能は同等だが、 MoveTemp では static_assert により落ちるケースが定義されている。注意深く必要な右辺値参照化をよく理解して使用できる一般的な C++er にとってはどうと言うことはないだろう。

UE4 では MoveTemp の他に CopyTemp も提供されている。 CopyTemp は l-value-reference に対する挙動が MoveTemp とは異なる。把握しやすいよう std::move, MoveTemp, CopyTemp の挙動の違いを以下に整理する。

function rvalue-reference lvalue-reference const lvalue-reference
std::move rvalue-reference rvalue-reference const lvalue-reference
MoveTemp rvalue-reference static_assert static_assert
CopyTemp rvalue-reference const lvalue-reference const lvalue-reference

なお、実際問題としては std 版のムーブセマンティクスを用いても UE4/C++ として問題が生じる事はない。

1.5.2. Forwardstd::forward

Forwardstd::forward の互換実装である。MoveTemp, MoveCopy とは違い、それ以上でもそれ以下でも無い。違わない事を知る事も大切と考え項を設けた。

1.6. UE4/C++ における「型変換」

C++er は必要に応じて C++ スタイルのキャスト static_cast<T> など、あるいはポリシーによっては冗長な記述の必要ない単純なケースでは C スタイルのキャスト (T) を用いて、静的型付けの恩恵を受けながらマシンネイティブな言語としての C++ においてオブジェクトのセマンティクスをプログラマーが意図通りに操作しバイナリーレベルでの論理的で高度なプログラミングを施… C++ の一般論はこのくらいにしておこう・x・

1.6.1. Cast<T>, CastChecked<T>dynamic_cast<T> 及び RTTI

UE4 にはライブラリーレベルのキャスト実装として Cast<T> が提供されている。

Cast<T>dynamic_cast<T>UE4 版と考えると C++er にとっては理解が速い。しかし、 Cast<T> についてはムーブセマンティクスのように C++ レベルで提供される機能と UE4 で提供される機能の何れを使用しても構わないわけでは「ない」ので注意が必要である。

先ず、 UE4 では dynamic_cast<T> は事実上使えない。デフォルトで RTTI は無効化されるからだ。一般論としては悪役にされる事もままあるダウンキャストを前提とした設計も合理的な理由がある場合、特にゲーム用途のライブラリーでは有用な事もある。 UE4 でもしばしばダウンキャストは有用に用いられるが、 RTTI を前提とした dynamic_cast<T> は実行時コストのオーバーヘッドが UE4 の目指す低レイテンシーフレームワークの実装と相容れないため、 UE4 では RTTI を無効化しつつも独自のリフレクションに基づいた高速で安全なダウンキャスト機能として Cast<T> を提供している。

ダウンキャストについて C スタイル、 C++ スタイル、 UE4 の 3 つについて整理する。

出処 実装 入力ポインターの制限 使用可能な条件 効果 失敗時の挙動
C (T) 任意のオブジェクト 常に使用可能 モリー構造の実体によらず常にキャスト (常に失敗しない)
C++ dynamic_cast<T> 任意のオブジェクト RTTIが有効 RTTIにより安全な場合にのみキャスト nullptr 相当の値が返る
UE4 Cast<T> UObject 派生オブジェクト UE4を使用 UE4のリフレクションにより安全な場合のみキャスト nullptr 相当の値が返る
UE4 CastChecke<T> UObject 派生オブジェクト UE4を使用 UE4のリフレクションにより安全な場合のみキャスト UE_LOG への Fatal ログ出力により停止

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

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

ほかに、入力が nullptr ではなく入力の GetClass と出力型の StaticClassoperator== 的に一致する場合にのみキャストを行い、失敗した場合は nullptr を出力する ExactCast も使用できる。

以下に UE4 Cast<T> / CastChecked<T>, C++ RTTI dynamic_cast<T>, C (T) についてダウンキャストを UE4 プロジェクトで速度的に評価した結果を整理する。

出処 method #/sec (O0) #/sec (O2) vs. (O2) 安全性
UE4 Cast<T> 16,984,356 23,152,004 1.030 Bad
UE4 Cast<T> + if 16,269,809 22,481,783 1.000 Good
UE4 CastChecked<T> 16,446,959 23,379,950 1.040 Good
C++ std::dynamic_cast<T>( x ) 15,470,261 20,449,124 0.910 Bad
C++ std::dynamic_cast<T>( x ) + if 15,470,788 20,670,679 0.919 Good
C (T) 17,233,758 24,727,323 1.100 Worst
C (T) + marking + if 17,729,100 24,630,186 1.096 No-bad

それぞれこのあと明示する実装をコアとして std:chrono::steady_clock で 1 秒間にダウンキャストとメンバー変数のインクリメントを処理できた回数を評価した。安全性について Bad としたものはそもそも安全なダウンキャストシステムの使い方として間違いだが表では単純な処理性能の指標として "いちおう" 載せた。通常は Good とした何れかを用いる。

単純な C スタイルのキャストでは安全なダウンキャストはできないため、少なくともそのままでは安全なダウンキャスト用途には使用できず、安全性としては Worst である。但し、表の最後に設けた、(T) + marking + if のように簡単な仕込みを基底型、派生型に施せば実用上の実際問題としては十分に実用に耐える高速な型 "やや" 安全なダウンキャストシステムを作る事はできる。キャスト対象のオブジェクトが基底型か派生型であれば意図通りに動作するが実装上はそれ以外の何かのアドレスを渡してもキャストできるし、偶然そのオブジェクトが基底型で定義した mark と同じアドレスに mark_type の値とバイナリー的に同等の値を読み出せる場合(たいていは何かしら読み出せてしまうだろう)は危険であり、安全性について Good とは言い難いため、 No-bad という微妙な表現に留めた。基本的にはこれも比較用の参考値として見て欲しい。

// .h

// Cスタイル用の細工用
enum class mark_type: uint8
{ unknown, x, y };

// 基底型
UCLASS() class UX: public UObject
{ GENERATED_BODY()
protected:
  mark_type mark = mark_type::x;
public:
  auto is_mark( const mark_type m ) { return mark == m; }
};

// 派生型
UCLASS() class UY: public UX
{ GENERATED_BODY()
public:
  UY() { mark = mark_type::y; }
  uint64 counter = 0;
};
// .cpp; 実際には項目ごと個別にコードするが便宜上続けて並べて記述する。

// UE4 Cast<T>; 成否判定しなければ実用性はないが参考値として。
++(Cast< UY >( its_y )->counter);

// UE4 Cast<T> + if
if ( auto y = Cast< UY >( its_y ) )
  ++y->counter;

// UE4 CastChecked<T>
++(CastChecked< UY >( its_y )->counter);

// C++ RTTI dynamic_cast<T>; 成否判定しなければ実用性はないが参考値として。
++(dynamic_cast< UY* >( its_y )->counter);

// C++ RTTI dynamic_cast<T> + if
if ( auto y = dynamic_cast< UY* >( its_y ) )
  ++y->counter;

// C (T); 成否判定しなければ実用性はないが参考値として。
++(((UY*)its_y)->counter);

// C (T) + mark + if
auto y = (UY*)its_y;
if ( y->is_mark( mark_type::y ) )
  ++y->counter;

なお、 C++er が追加的に知っておく知識がある。以下のコードは一見すると UE4 でも C++ の RTTI が「なぜか」有効で dynamic_cast が機能するかのように振る舞う。

UObject* x = NewObject< USceneComponent >( this );
USceneComponent* y = dynamic_cast< USceneComponent* >( x );

しかし、これは C++ の RTTI による dynamic_cast ではない。 "Runtime/CoreUObject/Public/Templates/Casts.h" の末尾に以下の定義がある。

#define dynamic_cast UE4Casts_Private::DynamicCast

一般的な C++er ならば察するように、つまり Cast<T> と同等の機能に置き換えられるよう仕込まれている。もし、本当に RTTI による dynamic_cast<T> を使いたければ、次のようにコードする必要がある。(もっとも、それが必要とは思わないが本記事の趣旨として "いちおう" 紹介する。)

// C++98 言語規格的に #ifdef __cpp_rtti だけで済ませたいが…
//   https://connect.microsoft.com/VisualStudio/feedback/details/3144244
#if defined( __cpp_rtti ) || defined( _CPPRTTI )
  #ifdef dynamic_cast
    #undef dynamic_cast
  #endif
  auto delivered_object_ptr = dynamic_cast< delivered_type* >( base_object_ptr );
#endif

C++ ソースコードとしては上記の様に #undef dynamic_cast すれば RTTI による C++ 本来の dynami_cast<T>UE4/C++ でも使用可能となる。加えて、 UE4 プロジェクトの "{your-project-name}.Build.cs" へ次のように明示的に RTTI を使用するコンパイルモードの設定を行えば実際に RTTI が有効な UE4/C++ コードのコンパイルが可能となる。

// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する
bUseRTTI = true;

このフラグの定義については ModuleRules.cs bUseRTTI を参照すると良い。例によってしばしばシンボル名が変更されているためソースで確認した方が速い。

UE4/C++ で RTTI を有効化したい状況には次のような事情が考えられる。

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

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

1.7. UE4/C++ における「アサーション

本節ではアサーションと例外によるエラーハンドリングについて std と UE4 の共通点、相違点を明らかとする。

1.7.1. UE4 アサーション各種と static_assert / assert (<cassert>) の挙動

UE4 ではライブラリーレベルのアサーションが比較的充実している。 std との対応も含めて紹介する。

所属 macro/keyword header
C++ static_assert (keyword)
C++ assert <cassert>
UE4 check
checkf
checkCode
checkNoReentry
checkNoRecursion
unimplemented
verify
verifyf
checkSlow
checkSlowf
verifySlow
verifySlowf
ensure
ensureMgsf
Runtime/Core/Public/Misc/AssertionMacros.h

C++er は例外に頼らないエラーハンドリングとしてしばしば簡単には int 値や enum class によるエラーコードを用いる方法や、 <cassert> によるアサーションC++11 から導入された static_assert を開発中に仕込む事でリリース前に設計や実装のミスによる不慮の動作を潰す手段を十分に知っている。

UE4 では C++ で一般的な手法に加えて、 UE4 独自の比較的豊富なアサーション機能が使用可能となる。以下に UE4 で使用可能なアサーションC++ 標準機能を含めて整理する。

所属 macro/keyword 評価 有効化条件 無効時の挙動 特徴・停止条件など
C++ static_assert 翻訳時 常に有効 条件式が false で停止する
std assert 実行時 #ifndef NDEBUG 何もしない 条件式が false で停止する
UE4 check 実行時 #ifdef DO_CHECK 何もしない 条件式が false で停止する
UE4 checkf 実行時 #ifdef DO_CHECK 何もしない 条件式が false で停止し、
条件式に続く引数をprintf的に表示する
UE4 checkCode 実行時 #ifdef DO_CHECK 何もしない do{...;}while(false); に展開される
UE4 checkNoReentry 実行時 #ifdef DO_CHECK 何もしない 複数回の実行で停止する
UE4 checkNoRecursion 実行時 #ifdef DO_CHECK 何もしない 複数回の実行で停止する
UE4 unimplemented 実行時 #ifdef DO_CHECK 何もしない 到達で停止する
UE4 verify 実行時 #ifdef DO_CHECK 条件式を単純に実行 条件式が false で停止する
UE4 verifyf 実行時 #ifdef DO_CHECK 条件式を単純に実行 条件式が false で停止し、
条件式に続く引数をprintf的に表示する
UE4 checkSlow 実行時 #ifdef DO_GUARD_SLOW 何もしない 条件式が false で停止する
UE4 checkSlowf 実行時 #ifdef DO_GUARD_SLOW 何もしない 条件式が false で停止し、
条件式に続く引数をprintf的に表示する
UE4 verifySlow 実行時 #ifdef DO_GUARD_SLOW 条件式を単純に実行 条件式が false で停止する
UE4 verifySlowf 実行時 #ifdef DO_GUARD_SLOW 条件式を単純に実行 条件式が false で停止し、
条件式に続く引数をprintf的に表示する
UE4 ensure 実行時 #ifdef DO_CHECK 何もしない 条件式が false でコールスタックを生成
UE4 ensureMsgf 実行時 #ifdef DO_CHECK 何もしない 条件式が false でコールスタックを生成し、
条件式に続く引数をprintf的に表示する

列挙すると多いが、基本的には static_assert, check, verify の 3 パターン、準じて unimplemented, checkNoReentry, checkNoRecursion を使用する事になる。

具体的な実装イメージは以下の仮想コードの様になる。

UCLASS( Blueprintable, BlueprintType )
class UMyBaseComponent : USceneComponent
{
  GENERATED_BODY()
  
public:
  
  virtual void InitializeHoge()
  {
    checkNoReentry();
    verify( xyz = NewObject< UHogeComponent >( this ) )
  }
  
  int8 CalcSquarePlusX( const int8 value ) const
  {
    constexpr auto int8_max = std::numeric_limits< int8 >::max();
    checkSlow( value < std::sqrt( int8_max ) )
    auto square = value * value;
    checkSlow( (int32)square + (int32)x < int8_max )
    return verifySlow( value * value + x );
  }
  
  virtual void Something() { unimplemented(); }
  
  UPROPERTY( EditAnywhere )
  UHogeComponent* hoge = nullptr;
  int8 x = 1;
};

1.8. UE4/C++ における「例外処理」の注意点

1.8.1. ビルドプロファイルとプラットフォームに与える制約

C++er は一般的にツールチェイン、対象プラットフォームにおける例外(SJLJ、DW4、SEHなど)について安全性やコストを吟味し、開発するアプリケーションに適した例外の使用方法を決め、アプリケーションの実装へ適用する。

UE4 を扱おうとする C++er 各位には残念な事実を受け入れて頂く必要がある。 UE4 では原則的には例外処理を使わない。なお「使えない」わけでは「ない」。

UE4 ではパフォーマンスとクロスプラットフォーム対応のため原則的には例外を用いないようにコードを設計する。特に正常系や致命的でないエラーの対応でも通過しうるような例外や制御構造の代用となるような例外の使い方は避ける。(もっとも、まともな C++er はそのような使い方はジョーク以外ではしないだろうが。)

例えば throw を使用したソースコードWin64 向けにはビルドできても、 Android 向けにビルドしようとすると、以下のエラーで失敗し、 UE4 がデフォルトでは Android 向けの例外処理を無効化している事がわかる。

error: cannot use 'throw' with exceptions disabled

なお、諸事情により UE4 がデフォルトで例外処理を無効化している処理系に対して例外処理を有効化したビルドを行いたい場合、 §6.1. と同様の手法でビルド時の例外処理を有効化できる。 "{your-project-name.Build.cs}" に次のように記述する:

// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する
bEnableExceptions = true;

この定義については ModuleRules.cs bEnableExceptions を参照すると良い。これもしばしばシンボル名が変更されているためソースで確認すると良い。

これで "デプロイ対象の処理系が対応しているならば" 例外処理を有効にしたビルドを生成できるように "いちおう" なる。プラットフォームによっては実行上のデメリットがあったり、ビルドがプロジェクトの例外有効化設定だけではビルド完了できないなど悩まされる事になろうが "いちおう" プロジェクト単位での例外処理はこれで有効化できる。

少なくともデスクトップ向け専用でクロスプラットフォーム展開を絶対にしないという強い確信、信念を持てないプロジェクトでは C++ の例外処理を用いないエラーハンドリング、例外処理を正常系でも通過し得るライブラリーの使用は回避するつもりで UE4/C++er へクラスチェンジする必要がある。

はてなブログの記事あたりの容量制限のため続き §1.9. 以降は次の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-2/5) - C++ ときどき ごはん、わりとてぃーぶれいく☆