プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

constexprでfloatのバイナリ値を取得したい

 constexprで計算したfloatの値を、バイナリをそのままにuint32_tに変換して配列の一部に保存したいという場面に出くわしました。

 templateの構造体で定義すれば良いようにも思えますが、floatが入る位置や数は未定で、やりにくいのです。

 というわけで、float→uint32_tというバイナリ解釈の変更をしたいのですが、なんと、constexprでunion型って、アクティブでない要素を計算に使えないんですね。

union b32_t{
  uint32_t i;
  float f;
  constexpr b32_t(float v):f{v}{}
  constexpr b32_t(uint32_t v):i{v}{}
};

constexpr uint32_t fbToUi32(float f)
{
    b32_t b(f);
    return b.i; // iがアクティブでないのでエラー
}

 んー。まじか。

 リトル/ビッグエンディアンぐらいは考慮しないといけないかなーとか思ってたけど、それ以前の問題だった。たぶん、C++コンパイラのconstexpr処理系では、unionは普通に二つの要素を持つオブジェクトとして処理されてるんだろうね。

 C++20からは制限が解除されるとからしいけど、C++11とかC++14なので関係ない。

 GCCだと、cmathの関数がconstexprになってるから、logbとかsignbitとかを使えば再構築できるけど、残念ながら私が使っている処理系では無理。 中3女子ことBolero MURAKAMIさんのSproutライブラリを使えれば良いのですが、政治的諸事情により使いにくい。困った。

 

 あ、なお、この辺もC++20でconstexprがつく様になるらしいっすよ。今の私にゃ関係ないけどね。

 

 

float→uint32_tを自前で実装

 諦めて自分でfloatの値に対応するバイナリを手動で書いてしまえばいいのだが、残念ながら私はクソニートだ。ここで意志を曲げて部屋から出て行く様では、老後が心配になってしまう。親に立ち向かう勇気を持って、この問題に立ち向かう義務がある。

 さて、こうなってしまっては自分でfloatの値を解析して、uint32_tを再構築するしかないだろう。

 float(IEEE 754準拠)は符号、仮数部、指数部の3つからなる。仮数部は2のn乗の部分で、実際にはn+127の値が入ります。指数部は1から2未満の値です。

 まぁ、そんなに難しかぁないさ。単純にそれぞれについて解析していけば良いでしょう。

 ただし、幾つかfloatには特殊な値が存在があります。+0、-0、∞、-∞、NaN、非正規化数である。これらは別個に処理…

constexpr bool is_pos0(float f){ return f ==  .0f; }
constexpr bool is_neg0(float f){ return f == -.0f; }

int main(int argc, char *argv[])
{
  static_assert(is_pos0(.0f));
  static_assert(!is_pos0(-.0f)); // エラー
  static_assert(!is_neg0(.0f));  // エラー
  static_assert(is_neg0(-.0f));
  static_assert(!is_pos0(1.0f));
  static_assert(!is_neg0(-1.0f));
  return 0;
}

 

 あるぇえ?-0と+0の区別が付かない………。あれ、バイナリ同じ物が生成されるの?いや、b32_tで出力してみたらちゃんと符号ビットが違うし………

 ………

 あははは!そうだよね、0は0だよね!誰だよ、0に正と負があるとか言った奴!ねぇよ、んなもん!

 よし、見なかったことにしよう。

 ただし、幾つかfloatには特殊な値が存在があります。0、∞、-∞、NaN、非正規化数である。これらは別個に処理する必要があります。

 一先ずはC++14で実装。

#include <limits>
#include <cstdint>

using float_limits = std::numeric_limits<float>;

constexpr float FLOAT_POS_INFINITY = float_limits::infinity();
constexpr float FLOAT_NEG_INFINITY = -float_limits::infinity();

constexpr bool is_zero(float f){ return f == .0f; }
constexpr bool is_posinf(float f){ return f == FLOAT_POS_INFINITY;}
constexpr bool is_neginf(float f){ return f == FLOAT_NEG_INFINITY;}
constexpr bool is_nan(float f){ return f != f;}
constexpr bool is_denormal(float f){
  //こんなんでいいのかな?
  return f != 0.f 
      && f < float_limits:: min_exponent 
      && f > -float_limits:: min_exponent;
}

constexpr uint32_t fbToUi32(float f){
  if(is_zero(f)){
    return 0;
  }else if(is_posinf(f)){
    return 2139095040;
  }else if(is_neginf(f)){
    return 4286578688;
  }else if(is_nan(f)){
    return 2143289344;
  }else if(is_denormal(f)){
    //デバイスの制限上0にしています。エラーでも良いかも
    return 0;
  }

  const uint32_t sign = f < 0? 0x80000000:0;
  if(f < 0){ f = -f; }

  uint32_t exp = 127;
  if(f < 1){
    while(f < 1){
      f *=2;
      exp--;
    }
  }else{
    while(f >=2){
      f *=0.5;
      exp++;
    }
  }

  uint32_t frac =0;
  f -=1;
  uint32_t _frac = 1 << 22;
  float sub = .5;
  while(f != 0.0 && _frac != 0){
    if(f >= sub){
      frac |= _frac;
      f -= sub;
    }
    sub *= .5;
    _frac = _frac >> 1;
  }
  
  return sign | (exp << 23) | frac;
}

 どうせコンパイル時にしか使わないんだし、仮数部の探索とかを二分探索とかにする必要はないでしょう。

 一応何個かテストしてみたら、それっぽく正しい値が出てるっぽいよ。とりあえずはこれでいっかな。後は必要ならC++11にすっか。