なんか書いてみたら自明な感じがしてきましたが、一応備忘録です。
拡張浮動小数点数型についてはあまり詳しく説明しないので他のページを参照してください。
拡張浮動小数点数型の変換ランク
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が同じ変換ランクだったとするとconstlongdouble ld = 1.0; constdouble d = 1.0; conststd::float64_t fp64 = 1.0f64; // long doubleとstd::float64_tは相互変換可能longdouble 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ではconstlongdouble ld = 1.0; constdouble d = 1.0; conststd::float64_t fp64 = 1.0f64; // long doubleとstd::float64_tは一方通行longdouble 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つ存在することになります・・・