[C++] 拡張浮動小数点数型の変換ランクに関する規定のある一文について

この記事はC++ Advent Calendar 2024の3日目の記事です。

拡張浮動小数点数型についてはあまり詳しく説明しないので他のページを参照してください。

なんか書いてみたら自明な感じがしてきましたが、一応備忘録です。

拡張浮動小数点数型の変換ランク

C++23において拡張浮動小数点数型という新しい浮動小数点数型のサポートが(必須ではないものの)追加されています。規格では、拡張浮動小数点数型を含む浮動小数点数型の変換やオーバーロード解決について、浮動小数点数型の変換ランクというものを用いて説明されています。

この変換ランクは浮動小数点数型に対応する浮動小数点数表現が表現可能な値の集合の包含関係によって定義されており、より表現可能な値の集合が大きい型(より幅の広い浮動小数点数型)の変換ランクが上位に来るようになっています。とはいえ、float16とbfloat16のように互いに包含関係が成立しない型が存在するので、この順序は半順序になります。

N4950 [conv.rank]/2にそれは規定されており、概ねそのようなことが書かれています。

全ての浮動小数点数型は、次のように定義される浮動小数点数変換ランクを持つ
1. 浮動小数点数型Tのランクは、その値の集合がTの値の集合の真部分集合となる浮動小数点数型のランクよりも大きくなる
2. long doubleのランクはdoubleよりも大きく、doubleのランクはfloatよりも大きい
3. 同じ値の集合を持つ2つの拡張浮動小数点数型のランクは同じ
4. (CV修飾を無視して)標準浮動小数点数型のうちのちょうど1つと同じ値の集合を持つ拡張浮動小数点数型のランクは、その標準浮動小数点数型と同じ
5. (CV修飾を無視して)標準浮動小数点数型のうちの2つ以上と同じ値の集合を持つ拡張浮動小数点数型のランクは、doubleと同じ

翻訳が気になる場合は原文を参照してください。

3に該当する拡張浮動小数点数型のペアはC++23時点では存在していない気がするのですが、ここで気になるのはそこではなく5の規定です。doubleとfloatがIEEE754の倍精度と単精度の表現を持つものとすると、4によってstd::float64_tとstd::float32_tはそれぞれdoubleとfloatと同じ変換ランクになります。じゃあ5は一体何を言っているのでしょうか?あるいは、何を想定しているのか・・・?

long double

標準浮動小数点数型にはもう一つlong doubleという奴がいます。これは少なくともdoubleと同じ精度を持つということくらいしか指定されていない自由人なのですが、この実体が実装によって実にバラエティ豊かになっています。そしてとくに、long doubleがdoubleと同じ表現を持つ場合が普通にあります。

例えばMSVCのWindows環境がそうですが、他にもARMの32ビット環境などもそうなるようです。doubleがIEEE754の倍精度表現になっているとすると、この場合拡張浮動小数点数型std::float64_tに対して同じ表現を持つ標準浮動小数点数型が2つ存在していることになります。

先程の変換ランクの規定5はまさにこのような場合の事を想定し、指定しています。

ある拡張浮動小数点数と他の浮動小数点数型間の変換において、変換ランクの低い型から高い型への変換はロスレス変換として暗黙的に行える一方で、変換ランクを下る方向の変換は縮小変換であり暗黙的には行えません。規定2によってlong double > doubleとなるため、この場合にstd::float64_tはどちらかと同じ変換ランクになる必要があり、それはdoubleと同じになることを規定4は指定しています。

この場合にもしlong doubleと同じ変換ランクになるとすると、long doubleとstd::float64_tは同じ変換ランクなので相互に暗黙変換可能なのに対して、std::float64_tとdoubleでは変換ランクが異なるためdoubleからの変換は暗黙的に行えるものの、doubleへの変換は明示的変換が必要となります。

// long doubleとstd::float64_tが同じ変換ランクだったとすると
const long double ld = 1.0;
const double d = 1.0;
const std::float64_t fp64 = 1.0f64;

// long doubleとstd::float64_tは相互変換可能
long double ld2 = fp64;     // ✅
std::float64_t fp64_2 = ld; // ✅

// doubleとstd::float64_tは一方通行
std::float64_t fp64_3 = d;  // ✅
double d2 = fp64;           // ❌

この挙動はおそらく便利なものではありません。どう考えてもdoubleの使用機会の方が多いでしょう。従って実際の仕様ではこのような場合の拡張浮動小数点数型の変換ランクはdoubleと同じになり、少なくともdoubleとのやり取りをスムーズにしています。

// 実際のC++23では
const long double ld = 1.0;
const double d = 1.0;
const std::float64_t fp64 = 1.0f64;

// long doubleとstd::float64_tは一方通行
long double ld2 = fp64;     // ✅
std::float64_t fp64_2 = ld; // ❌

// doubleとstd::float64_tは相互変換可能
std::float64_t fp64_3 = d;  // ✅
double d2 = fp64;           // ✅

非Windowsのx86-64環境ではlong doubleの表現は80ビットの拡張倍精度になっていることが多いですが、この動作はオプション(-mlong-double-64)で変更することができるのでこの挙動を実際に確かめることができます。

余談ですが、AVRマイコンの環境でGCC9まではdoubleもlong doubleもfloatと同じ32ビット幅の表現になっていたようで、gcc10以降もオプションで変更可能とのことです。このような環境では、(もし実装されれば)std::float32_tに対して同じ表現を持つ標準浮動小数点数型が3つ存在することになります・・・

参考文献

この記事のMarkdownソース