2進浮動小数点数の10進表記には様々な問題があります。
10進で有限小数として表せる数であっても、2進小数では有限小数で表せない(したがって2進浮動小数点数に変換すると誤差が出る)ことは周知かと思います。例えば 0.1 という実数を2進で表すと
0.1[10進] = 1/16 + 1/32 + 1/256 + ...
= 0.0001 1001 1001 1001 ...[2進]
という循環小数になり、有限桁で正確に表すことができません。
10進小数を正確に表したい場合は10進浮動小数点数(あるいは10進固定小数点数など、10進に基づいた小数型)を使うのが適切です。逆に、2進浮動小数点数を正確に表現したい場合はそれに適した記数法を使うのが適切です。
2進浮動小数点数に適した記数法として、1999年に制定されたC言語の規格(C99)では、16進法を使って浮動小数点数を記述する方法が導入されました。
16進の1桁は2進の4桁に対応し、2進法で有限小数で表せる数は16進でも有限小数で表せます。逆も然りです。16進表記は浮動小数点数の2進表記に比べて桁数を抑えつつ、「正確に表現できる」というメリットをそのまま持っています。
この記事では、
- 浮動小数点数の16進表記について
- 浮動小数点数を表す他の方法(10進表記など)の問題点と、16進表記が優れている理由
- 現代のプログラミング言語における16進表記の対応状況
を扱います。
浮動小数点数の16進表記
浮動小数点数の16進表記は、C言語の規格の1999年の版(C99)が初出です(多分)。浮動小数点数の標準であるIEEE 754でも、2008年の版で2進浮動小数点数の16進表記が取り込まれています。
大雑把な書き方は
0x
<16進表記された仮数部> `p` <10進表記された指数部>
となります。「16進」と言いつつ、指数部は10進表記で、底は2です。
仮数部は小数点を含むことができます。10進小数と同じように、小数点の前または後ろは省略可能です。
例をいくつか見てみましょう。
-
0x1.23ap32
という表記は
$\Bigl(1+\frac{2}{16}+\frac{3}{16^2}+\frac{10}{16^3}\Bigr)\times 2^{32}$
という実数を表します。 - 仮数部が整数なら小数点を省略することもできます。
0x1p10
という表記は $1\times 2^{10}$、すなわち 1024 という実数を表します。 - 指数部はもちろん負の値をとることができます。
0x1p-52
という表記は $2^{-52}$ という実数を表します。これは倍精度におけるマシンイプシロン(1と、1よりも大きい最小の表現可能な数の差)です。 - 仮数部を小数点で始めることもできます。
0x.123p+0
という表記は $\frac{1}{16}+\frac{2}{16^2}+\frac{3}{16^3}$ という実数を表します。 - 仮数部を小数点で終えることもできます。
0xA.p+7
という表記は $10\times 2^7$ すなわち 1280 という実数を表します。 - 仮数部を「小数点のみ」とすることはできません。
0x.p0
は不正な表記です。
浮動小数点数の10進表記では指数部(e
に続く部分)は省略可能でしたが、C言語のリテラルやIEEE 754の規定では16進表記の指数部(p
に続く部分)は省略不可能です。
【余談】C言語の場合、16進表記の指数部を省略してしまうと、末尾が f
だった場合に float
型を表すサフィックスと紛らわしい、という問題があります。それを回避するために指数部を省略できないようにしたのかもしれません。あるいは、 p
という見慣れない文字を必須にすることによってソースコードの読み手に「16進浮動小数点数リテラルが使われている」ことを意識させるようにしたのかもしれません。いずれも筆者の想像です(この件について裏付けまたは反証するようなソースがあったら教えてください)。【/余談】
後述しますが、プログラミング言語によっては16進浮動小数点数リテラルの形式がIEEE 754の定めるものから外れている場合があります。例えば、16進の指数部を省略可能にしていたり、仮数部を小数点で始める・小数点で終わらせることを禁止していたりするものがあります。C言語の場合、ソースコード中のリテラルではなく「文字列のパース」の場合は指数部の省略を認めています。
もう少しいろいろな数を16進で表記してみましょう。
- 1 の次に大きい倍精度浮動小数点数:
0x1.0000000000001p0
(小数点以下0
が12個、その後に1
) - 0.1 という実数に最も近い倍精度浮動小数点数:
0x1.999999999999ap-4
- いかにも「循環小数を打ち切った」という感じが出ています。
- 有限桁への丸めの際に繰り上がりが行われている(真の値よりも少し大きくなっている)ことが見て取れます。
- 円周率に最も近い倍精度浮動小数点数:
0x1.921fb54442d18p1
- これは実際の円周率よりも少し小さな値です。円周率をもう少し先の方まで書くと
0x1.921fb54442d18469898cc517...p1
となります。 - 円周率より大きい最小の倍精度浮動小数点数は
0x1.921fb54442d19p1
となります。 - 流石に「10進小数での表記がよく知られていて、そもそも何進法を使ったとしても有限小数では正確に表現できない数」は10進小数で十分かもしれません。
- これは実際の円周率よりも少し小さな値です。円周率をもう少し先の方まで書くと
- 最小の正の倍精度浮動小数点数(非正規化数):
0x1p-1074
- 最小の正の倍精度正規化数:
0x1p-1022
- 最大の有限の倍精度浮動小数点数:
0x1.fffffffffffffp1023
(小数点以下、f
が13個)
「最小の・最大の」のやつは指数部が1024付近となっていて、倍精度での指数部が11ビットであることと容易に関連づけられるでしょう。あるいは、倍精度の(底2での)指数部の範囲が ±1024 前後であることを覚えておけば倍精度の指数部のビット数をど忘れしても導出できるでしょう。
もちろん、いくら16進を使ったところで、その浮動小数点数形式での精度で表せる桁数よりも多くの桁数を使ってしまうと、文字列表記から浮動小数点数の値への変換の際に誤差が発生してしまいます。ポイントは、16進表記を見れば「その表記を値に変換した際に誤差が発生するかどうか」を正確に見分けられるということです。
具体的には、倍精度(精度53ビット)であれば整数部分を1とした時に小数点以下13桁(小数点以下52ビット)を正確に表すことができます。16進表記と2進表記の対応は次のようになります(H
は16進1桁、B
は2進1桁の意)。
16進: 0x1. HHHH HHHH HHHH H p<指数>
2進: 0b1. BBBB...BBBB BBBB...BBBB BBBB...BBBB BBBB p<指数>
(1ビット).(16ビット) (16ビット) (16ビット)(4ビット)
例えば、 0x1.aaaabbbbccccdp0
という数は整数部分が1で、小数点以下ちょうど13桁なので、倍精度で正確に表現できます。一方、 0x1.aaaabbbbccccddp0
という数は、整数部分が1で、小数点以下14桁続いているので、正確に表現できないことがわかります。
単精度(精度24ビット)の場合、整数部分を一とすると小数点以下およそ6桁を正確に表すことができます。「およそ」と書いたのは、「小数点以下6桁目は偶数である」という条件がつくからです。16進表記と2進表記の対応は次のようになります(H
は16進1桁、B
は2進1桁の意で、H'
は16進1桁の偶数)。最後の桁の最後のビットは2進表記では表せないので常に0、つまり偶数です。
16進: 0x1. HHHH H H' p<指数>
2進: 0b1. BBBB...BBBB BBBB BBB0 p<指数>
(1ビット).(16ビット)(4ビット)(3ビット)
例えば、 0x1.aaaabbp0
という数は整数部分が1で小数点以下に6桁続いていますが、16進の b
(=11)は偶数ではないので、単精度では正確に表すことができません。一方、 0x1.coffeep7
という数は整数部分が1、小数点以下6桁で、最後の e
(=14) は偶数なので、単精度で正確に表現できます。
単精度の場合、別の見分け方として、整数部分を0として小数第1位を 8 以上(2進表記した場合の小数第1位が 1)とすると、「小数点以下16進6桁」が正確に表せるための条件となります。例えば、 0x0.aaaabbp3
という数は小数点以下6桁に収まっているので、単精度で正確に表現できます。16進表記と2進表記の対応は次のようになります(H
は16進1桁、B
は2進1桁の意で、H'
は16進1桁の偶数)。最後の桁の最後のビットは2進表記では表せないので常に0、つまり偶数です。
16進: 0x0. HHHH H H p<指数>
2進: 0b0. 1BBB...BBBB BBBB BBBB p<指数>
(16ビット)(4ビット)(4ビット)
一応断っておきますが、16進表記が有用なのは2進浮動小数点数(あるいは、基数が2のべきであるような浮動小数点数)についてです。10進浮動小数点数(Decimal型みたいなやつ)をわざわざ16進表記する理由はありません。
また、この見分け方は正規化数の場合に使える方法で、非正規化数の場合(指数部がとても小さい場合)の精度の減少にも気を付ける必要があります。
2進浮動小数点数の10進表記
「16進表記があるのはわかったけど、別に10進でもよくない?」みたいな人のために、2進浮動小数点数を10進で表記するのが不適切な理由を書いておきます。
正確な表記
まず、「2進浮動小数点数は10進でも正確に表現できるのでは?」という意見に反駁しておきます。
2は10の約数なので、2進での有限小数は10進でも有限小数として表すことができます。有限桁の10進小数が一般には2進では循環無限小数になるのとは対照的です。
例えば、2進小数を 0b
で表すことにすれば、 0b0.1
は 0.5
, 0b0.1011
は 0.6875
という具合です。
ですが、この「正確な表記」は(よっぽど桁数が短く済むのでない限り)ほとんど使われません。というのは、一般には必要な桁数がとても多くなるからです。
例えば、倍精度で「1の次に大きい浮動小数点数」と「0.1という実数に最も近い浮動小数点数」をそれぞれ正確に10進表記してみましょう:
1の次に大きい浮動小数点数 $1+2^{-52}$:
1.0000000000000002220446049250313080847263336181640625
0.1という実数に最も近い浮動小数点数:
0.1000000000000000055511151231257827021181583404541015625
いずれも50桁以上ありますね。他の数も見てみましょう。
円周率に最も近い倍精度浮動小数点数:
3.141592653589793115997963468544185161590576171875
最小の正の倍精度浮動小数点数(非正規化数)$2^{-1074}$:
4.940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625e-324
最小の倍精度正規化数 $2^{-1022}$:
2.225073858507201383090232717332404064219215980462331830553327416887204434813918195854283159012511020564067339731035811005152434161553460108856012385377718821130777993532002330479610147442583636071921565046942503734208375250806650616658158948720491179968591639648500635908770118304874799780887753749949451580451605050915399856582470818645113537935804992115981085766051992433352114352390148795699609591288891602992641511063466313393663477586513029371762047325631781485664350872122828637642044846811407613911477062801689853244110024161447421618567166150540154285084716752901903161322778896729707373123334086988983175067838846926092773977972858659654941091369095406136467568702398678315290680984617210924625396728515625e-308
最大の有限の倍精度浮動小数点数 $2^{1024}-2^{971}$:
1.79769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368e308
めっちゃ長いですね。$2^{-1074}$ は750桁くらいあります。こんなものをソースコードやファイルに書きたくはないです。読むのも書くのも大変です。一方で、16進表記なら桁数が固定(倍精度ならせいぜい13桁)なのでとても経済的です。
「2進浮動小数点数は有限桁の10進小数で正確に記述できるから10進でいいでしょ」というのが机上の空論に過ぎないということを体感していただけたでしょうか?
10進との変換はどの程度正確か
10進表記された小数をコンパイラーやライブラリー関数が浮動小数点数に変換する際、10進表記の表す実数が対象の浮動小数点数形式で正確に表現できる場合はその数に、正確に表せない場合は次善の策として10進表記の表す実数に最も近い2つの数のいずれかに変換される(どちらになるかは丸めモードに従う)という期待は自然なものでしょう。
ですが、C言語やIEEE 754では、元の10進表記があまりにも長い場合は適当な上限を設けて多少不正確な(2回丸めた)結果を返すことを認めています(C11の7.22.1.9, IEEE754-2019の5.12.2)。
ですので、先の「正確な10進表記」をC言語やIEEE 754準拠の関数で2進浮動小数点数へ変換する場合は、本来の(正しい)値と異なる値が得られる可能性を考慮する必要があります。
「10進表記された数を正確に2進浮動小数点数に丸めるのってそんなに難しいのか?」と思うかもしれませんが、………難しいらしいです。動的なメモリ確保をして多倍長整数とかを使えばもちろん可能ですが、動的なメモリ確保なしでは不可能らしいです。
この辺については、論文があるようなので気になる方は読んでみて下さい(私はまだ読んでないです):
- William D. Clinger. 1990. How to read floating point numbers accurately. SIGPLAN Not. 25, 6 (Jun. 1990), 92–101. DOI:https://doi.org/10.1145/93548.93557
ちなみに、「適当な上限」は、普通にやっていく分には引っかからない程度に大きい(倍精度の場合は17桁以上)ことが、IEEE 754でもC言語でも「推奨 (should)」されています。
逆方向の変換(2進浮動小数点数→10進表記)も、多倍長計算を使えば普通にできますがそうでない場合は大変なようです。この話題についても何本か論文が出ているようです(私はまだry):
- Guy L. Steele and Jon L. White. 1990. How to print floating-point numbers accurately. SIGPLAN Not. 25, 6 (Jun. 1990), 112–126. DOI:https://doi.org/10.1145/93548.93559
この記事の本題は16進表記についてなので、10進表記についてはこの辺にしておきます。ちなみに、IEEE 754では「10進浮動小数点数と10進表記」および「2進浮動小数点数と16進表記」については常に正確な丸めが行われることを要請しています。
ビット列(32ビットあるいは64ビット)との変換ではダメなのか
言語によっては、浮動小数点数の16進表記には対応しないが、ビット列としての表現(単精度なら32ビット整数、倍精度なら64ビット整数)との変換には対応している場合があります。
ビット単位で浮動小数点数を正確に記述したい場合はこれも一つの方法ですが、あまり人間に優しい方法ではありません。実際どんな感じになるのか、いくつか例を見てみましょう。
まず、倍精度での +0
は 0x0000_0000_0000_0000
となります。これはまだわかりやすいです。ちなみに -0
は符号ビットが立って 0x8000_0000_0000_0000
となります。
一方、 1.0 は 0x3FF0_0000_0000_0000
となり、「1.0の次に大きい数」は 0x3FF0_0000_0000_0001
となります。3FF
というマジックナンバーが登場しましたが、これは「バイアスされた」指数部です。
また、倍精度で表現できる最大の有限の数 $2^{1024}-2^{971}$ は 0x7FEF_FFFF_FFFF_FFFF
となります。
数学的には同じ数であっても、精度が変わるとビット列による表現は変わります。単精度の 1.0 は 0x3f800000
となります。倍精度と比べると、全体の幅だけでなくバイアスの部分も変わっています。
ビット列による表現はとにかく具体的すぎるのです。面倒なポイントを具体的に挙げると
- 指数部と仮数部が分かれていない
- 指数部のバイアスを考慮する必要がある
- 指数部まで16進で考える必要がある(16進にするメリットがあるのは仮数部であって、指数部は10進で良いのに…)
という感じでしょうか。もう一つ「ビット列による表現」が使えない例を挙げると、
- ビット列で表す標準的な方法がない浮動小数点数型(任意精度など)には使えない
があります。
結局、我々が扱いたいのは、浮動小数点数の数学的な定義
± <仮数部> * 底 ^ <指数部>
に即した表現であって、64ビットだか32ビットだかに押し込めたビット列ではないのです。仮に整数リテラルを -42
じゃなくて2の補数で 0xFFFF_FFD6
と書かされたら嫌でしょう?
とはいえ、ビット列との変換が役にたつ状況もあります。それは、実数ではない浮動小数点数をビット単位で正確に記述したい場合、つまり NaN の記述です。特定のビットパターンを持つNaNを記述するのにはビット列による表現は有益です。特に、現行のC標準では signaling NaN を作り出す標準的な方法はないので、 signaling NaN を試したい場合は浮動小数点数をビット列とみなして構築する必要があります。(signaling NaNのビット列による表現方法は実装依存だったりしますけどね!)
浮動小数点数の16進表記が役立つ場面をもっと具体的に
筆者が最近書いたコードで、16進表記が特に役立ったな、という場面を2つほど挙げます。
数学の定数を区間演算で使う場合
まず、「各種定数($\pi$ や $e$, $\log 2$ など)を含むような最小の区間」を記述したい場合です。この場面では書いた値が最後の一桁まできっかり望み通りの浮動小数点数となる必要があります。
普通に「最も近い数」を表すだけなら10進で気持ち多めに桁を並べていけば良いのですが、「本来の値よりも大きくなっては困る」「本来の値より小さくなっても困る」という場面で不正確な10進表記をするのは不安です。かと言って、10進小数で正確に記述しようとすると必要な桁数が増えてしまいます。
そこで16進表記の出番です。16進表記を使うと単に正確な表記を経済的に行えるだけではなく、最後の1桁に注目することによって、表している区間が最小であるかを目視で確認できます。例えば
sqrt2_down = 0x1.6a09e667f3bccp+0
sqrt2_up = 0x1.6a09e667f3bcdp+0
とコード中に書かれていれば、 sqrt2_down
と sqrt2_up
の違いは最下位の桁が1違うだけ(0xc = 12
と 0xd = 13
)ですから、その途中に別の倍精度浮動小数点数が挟まっている可能性は無いわけです。
筆者が書いた実際のコードは ここ にあります。
数学関数の実装の正しさのチェック
別の例は、「数学関数が正しく実装されていることのテストケース」です。C言語等の数学関数の中にはそこまで正確さが要求されていないものもありますが、いくつかの関数については「最後の一桁まで正確である(正確に丸められている)こと」を要求しています。こういった関数を活用すれば、環境が変わっても計算結果が一致するようなコードを書くことも夢ではありません。
そういう「正確さが要求されている」関数の1つが fma
(fused multiply-add) です。最近のCPU(や、GPU)は命令セットにFMA命令を持っていることも多く、それらはまず間違いなく「正しく」実装されています。なので、C言語の処理系的にはFMA命令を使えば fma
関数の正しい実装が手軽に得られます。……CPUが対応していれば。
実際のところ、少し前のx86系プロセッサーにはFMA命令がありませんでした。FMA命令が実装されたのは、Intelの場合はHaswell世代、2010年代の途中です。それより古いx86プロセッサーにはFMA命令はないので、そういう環境をターゲットにする場合は処理系はCPUにFMA命令に頼らずに fma
関数を実装しなくてはなりません。
ただ、 fma
は自前で実装しようとすると結構厄介です。コーナーケースもそれなりにあります。なので、処理系によっては fma
関数の実装が間違っていることがあります。(具体的には、Windowsがターゲットのあの処理系とその処理系です。)
そこで、 fma
のコーナーケースを試して fma
の実装が「正しそう」か「実際に間違っている」のか判断に役立つプログラムを、先日書いてみました。
具体的な浮動小数点数を与えて、それが最後の1桁まで正しいことを確認するのですから、コード中には何らかの方法で「正しい」浮動小数点数を記述する方法が必要です。2進浮動小数点数の場合、16進表記はこの場合にうってつけです。
ちなみに、 fma
の場合は「積が特定のビットパターンとなるような数」がコーナーケースとなりがちです。そのような数を記述する際に、16進の「2進のビットパターンと容易に変換できる」という性質が役に立ちます。例えば、上記プログラムにも登場する 0x0.ffffffep513 * 0x1.0000002p511
という組み合わせは、「1が連続で27ビット立った数」と「2つの1の間に0が26ビット挟まった数」との積です。この積の(丸めを行わない)正確な値は「1が連続で54ビット立った数」となります。
ちなみに(2回目)、C言語以外ののプログラミング言語も fma
に相当する関数を持っている場合があります。このテストプログラムをそう言う言語に移植することも当然考えられますが、その際に移植先の言語が浮動小数点数の16進表記に対応しているかどうかはわりと重要な問題です。
各言語での16進小数表記の対応状況
最後に、各種プログラミング言語における浮動小数点数の16進表記の、
- リテラル
- 文字列化
- 文字列からの変換
に関する対応状況を調べてみました。文字列化と文字列からの変換については、代表的な物を挙げてみました。筆者の理解が浅い言語もあるので、おかしなところがあればやんわりと指摘していただけると幸いです。
C
C99で浮動小数点数の16進表記が導入されました。
- リテラル:対応
- 文字列化:
printf
系の%a
指定子 - 文字列からの変換:
strtod
およびその仲間たち(strtod
,strtof
,strtold
,atof
の各関数、およびscanf
系の%a
,%e
,%f
,%g
指定子)
文字列からの変換の際、指数部は省略可能となっています。
文字列化の際、細かい部分は任意性があります。実際、Cの標準ライブラリーの実装によって細かい出力方法が異なります。
macOS の libSystem や Linux の musl は、常に先頭の桁を 1 として正規化された出力を行うようです:
0x1.c0ffee0122345p+0
0x1.c0ffee0122345p-1022
0x1.c0ffee0122344p-1023
0x1p-1074
0x1.c0ffee0122345p+3
Linux の glibc や Windows の Universal CRT は、正規化数に関しては先頭を1としますが、非正規化数の場合は指数部を -1022
に固定して非正規な形で出力するようです:
0x1.c0ffee0122345p+0
0x1.c0ffee0122345p-1022
0x0.e07ff700911a2p-1022
0x0.0000000000001p-1022
0x1.c0ffee0122345p+3
Windows の mingw-w64 のランタイムは少し変わり種で、16進の先頭の桁が8以上となるような表記をするようです:
0xe.07ff700911a28p-3
0xe.07ff700911a28p-1025
0xe.07ff700911a2p-1026
0x8p-1077
0xe.07ff700911a28p+0
C++
C++11で文字列との変換に対応し、C++17でリテラルに対応しました。
- リテラル:C++17
- 文字列化:
- iostreamの場合
std::hexfloat
(C++11) - C99と同様の方法も利用可能
- iostreamの場合
- 文字列からの変換:
- C99と同様の方法が利用可能
- istreamについては、筆者の環境ではclang++は対応、g++は非対応という結果でした。標準では細かく規定されてないのでしょうか?>識者
浮動小数点数の16進リテラルがC++11で導入されず、C++17まで待つことになったのは興味深いです。C言語とは18年差です。数値リテラルの拡張としては、整数の2進リテラルや桁区切り文字の方がむしろ先に(C++14で)入ってます。
Java
この記事によると、J2SE 5.0 (2004年) で対応したらしいです。筆者の方でググってもJava 5当時のドキュメントが見つけられず、裏は取れませんでした。
- リテラル:対応
- https://docs.oracle.com/javase/specs/jls/se14/html/jls-3.html#jls-3.10.2
- 区切り文字としてアンダースコアを使えます。
- 文字列化:https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/util/Formatter.html#syntax
%a
,%A
- 文字列からの変換:Float.valueOf, Float.parseFloat, Double.valueOf, Double.parseDouble こちらはアンダースコアによる区切りを許容しないようです。
D
DMD 0.50(2002年リリース)の時点ですでに16進リテラルに対応していたようです。浮動小数点数への意識の高さが垣間見えます。
-
リテラル:対応
- Floating Point Literals - Lexical - D Programming Language
- 区切り文字としてアンダースコアを使えます。
-
文字列化:
-
文字列からの変換:
- std.conv - D Programming Languageが使えるようです。指数部は必須です。
Go
Go 1.13(2019年リリース)で導入されたようです。
- リテラル:対応 https://golang.org/ref/spec#Floating-point_literals
- 区切り文字としてアンダースコアを利用できます。
- 文字列化: https://golang.org/pkg/strconv/ AppendFloat, FormatFloat https://golang.org/pkg/fmt/
%x
,%X
- 文字列からの変換: https://golang.org/pkg/strconv/#ParseFloat ParseFloat fmtパッケージ
導入に関するIssue:proposal: Go 2: hexadecimal floats · Issue #29008 · golang/go
Rust
Rust標準では、リテラル、文字列との変換、いずれも用意されてなさそうです。塩対応です。
- リテラル:非対応
- 文字列化:非対応
- std::fmt は対応していなさそうです(f32, f64がLowerHex/UpperHex traitを実装していない)
- 文字列からの変換:非対応
- impl FromStr for f32, impl FromStr for f64 は対応していないようです。
Rustのような、モダンでC/C++の置き換えを期待されている言語が浮動小数点数の16進表記に対応していないのは意外です。今後に期待したいところです(8年前に開かれてその後Closedになったissueを眺めながら)。
ちなみに、10進表記で2進小数を正確に表そうとして長々とリテラルを書くと、 "could not evaluate float literal (see issue #31407)" というエラーが出ます(「最小の正の数」「最小の正の正規化数」の10進表記の例で確認)。
一応有志によるライブラリーを紹介しておくと、hexfというcrateで(マクロによる)リテラルおよび文字列からの変換ができるようです。
C#
- リテラル:非対応
- 文字列化:非対応
- 文字列からの変換:非対応
C#は16進浮動小数点数リテラルに対応していません。dotnet/csharplangをみた感じでは、そういうproposalも出ていないようです。文字列との変換も標準では用意されていません。
ちなみに、ドットネットには32ビットまたは64ビットの整数とのビット列としての相互変換はあります(BitConverter
クラス)が、なぜか符号付きの整数型との変換となっています。-0
が 0x8000...0000
なのを見ればわかるように、符号付き整数型と浮動小数点数とでは負数の表し方が違うので、符号なし整数型を持っている言語においてあえて符号付き整数と変換するメリットは少ないはずです。
Scala
リテラルには非対応のようですが、JVMターゲットであれば文字列との変換は可能です。JVM以外がターゲットの場合は未調査です。
- リテラル:非対応
- 文字列化:JVMターゲットならJava同様に
String.format
を使えば可能なようです。 - 文字列からの変換:JVMターゲットなら
StringOps.toDouble
,StringOps.toFloat
が利用できるようです。これらは内部的にjava.lang.Double.parseDouble
/java.lang.Float.parseFloat
を呼んでいるようです
Kotlin
リテラルには非対応のようですが、JVMターゲットであれば文字列との変換は可能です。JVM以外がターゲットの場合は未調査です。
- リテラル:非対応
- 文字列化:JVMターゲットなら
String.format
が利用できるようです。 - 文字列からの変換:JVMターゲットなら
String.toDouble
やString.toFloat
が利用できるようです。
Haskell
GHC 8.6(2018年リリース)で実装された言語拡張により、浮動小数点数の16進リテラル表記に対応しました。
- リテラル:GHC 8.6で実装された
HexFloatLiterals
拡張で対応しました。- 指数部は省略可能です。
- 同じくGHC 8.6で実装された
NumericUnderscores
拡張を使うと、区切り文字としてアンダースコアを利用できるようになります。 -
fromRational
関数によって脱糖されるので、浮動小数点数型だけではなく、有理数型でも利用できます。
- 文字列化:
Numeric
モジュールのshowHFloat
関数が使えます。Text.Printf
は非対応です。 - 文字列からの変換:標準 (baseパッケージ) にはなさそうです。
ちなみに、 HexFloatLiterals
がない場合、 0x1.234p5
は 0x1
.
234
p5
, つまり整数リテラル、ドット演算子、整数リテラル、変数名、とパースされます。他のML系言語にも同じような字句解析規則を持っているものがあったはずです(なので、そういう言語に後付けで16進浮動小数点数リテラルを導入すると破壊的変更となります)。
Fortran
Fortran 2018で16進浮動小数点数の入出力に対応したっぽいです。
リテラルは、そもそも整数の16進リテラル表記がC系の言語とは大きく異なるようなので、アレです。
Perl
Perl 5.22(2015年リリース)で対応したらしいです。
-
Floating point parsing has been improved - perl5220delta - perldoc.perl.org
-
リテラル:Scalar value constructors - perldata - perldoc.perl.org 区切り文字としてアンダースコアを利用可能
-
文字列化:
printf
系関数の%a
,%A
が利用できます。 https://perldoc.perl.org/functions/sprintf.html -
文字列からの変換:
- 筆者はPerlに詳しくないので、文字列を浮動小数点数に変換する標準的な方法が何なのかよくわかりませんでした。少なくとも、暗黙の変換
1 * "0x1p0"
や0 + "0x1p0"
では16進表記は変換されませんでした。 - もちろん
eval
を使えば文字列から変換できますが…。
- 筆者はPerlに詳しくないので、文字列を浮動小数点数に変換する標準的な方法が何なのかよくわかりませんでした。少なくとも、暗黙の変換
ちなみに、Perlで16進リテラルの指数部をうっかり書き忘れると、全く違う意味になります。
$ perl -l -e 'print(0x1.23); print(0x1.23p0)'
123
1.13671875
何が起きたかわかりますか?(Perlの文法の闇を感じます)
Python
リテラルには対応していませんが、 float.hex()
と float.fromhex()
で文字列化・文字列からの変換ができます。これらはPython 2.6(2008年リリース)で実装されたようです。Python 3系列には3.0から入っています。
- リテラル:非対応
- 文字列化:
float.hex()
(インスタンスメソッド) https://docs.python.org/3/library/stdtypes.html#float.hex - 文字列からの変換:
float.fromhex()
(クラスメソッド) https://docs.python.org/3/library/stdtypes.html#float.fromhex
float.hex()
と float.fromhex()
が含まれるリリース:
float.hex()
/ float.fromhex()
だけではなくリテラルや float()
でも対応しようぜ、という議論が2017年ごろにあったようです。
現行のPythonでは、 0x1.cafep0
は 0x1
に対する cafep0
属性の参照、とパースされるようです。なので仮に後付けで浮動小数点数の16進リテラルを導入すると破壊的変更となります。
Ruby
Ruby 1.9.2(2010年リリース)で文字列との変換がサポートされました。リテラルには対応していません。
- リテラル:非対応
- 文字列化:
Kernel.#sprintf
等の%A
および%a
指示子 https://docs.ruby-lang.org/ja/latest/method/Kernel/m/sprintf.html - 文字列からの変換:
Kernel.#Float
は対応、String#to_f
は非対応-
Kernel.#Float
やString#to_f
のドキュメントにこれに関する記述は見つからなかったが、導入時の議論 https://redmine.ruby-lang.org/issues/2969 では言及がある
-
Rubyは整数リテラル直後のドットがメソッド呼び出しと解釈されうる言語なので、 0x1.cafep0
は 0x1
という値に対する cafep0
メソッドの呼び出しと解釈されます。仮にRubyにC言語と同様の16進浮動小数点数リテラルを導入すると破壊的変更となってしまうので、今後も導入されることはないでしょう。
Lua
Lua 5.2(2011年リリース)で対応しました。指数部は省略可能です。
- リテラル:対応。指数部は省略可能。
- 文字列化:
string.format
の%A
,%a
フォーマット指定子 - 文字列からの変換:
tonumber
ちなみに、Luaはリテラルに対して(括弧等を使わずに)直接フィールド参照をすることはできない文法となっているため、16進浮動小数点数リテラルの導入の際にそれが原因で既存のコードが壊れることはなかったはずです(もちろん、Lua 5.2ではキーワードの追加を含む変更が行われているため、リテラル以外の理由で既存のコードが動かなくなることはあり得ます)。
Lua 5.1:
$ lua5.1
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> print(1.234 . foo) -- 数値リテラルの直後にフィールド参照はできない
stdin:1: ')' expected near '.'
> print(37 ["key"]) -- 括弧でもダメ
stdin:1: ')' expected near '['
> print(0x1.234p7) -- '0x1' と '.234p7' の2つのトークンとして解釈されて、後者がパースエラーとなったっぽい
stdin:1: malformed number near '.234p7'
> print(0x3.cafep10) -- 数値リテラルの直後にフィールド参照はできない
stdin:1: ')' expected near '.'
Lua 5.2以降:
$ lua5.2
Lua 5.2.4 Copyright (C) 1994-2015 Lua.org, PUC-Rio
> print(1.234 . foo) -- 数値リテラルの直後にフィールド参照はできないのは同じ
stdin:1: ')' expected near '.'
> print(37 ["key"]) -- 同じく
stdin:1: ')' expected near '['
> print(0x1.234p7) -- 16進リテラルとして解釈されるようになった
145.625
> print(0x3.cafep10) -- 16進リテラルとして解釈されるようになった
3883.96875
Julia
- リテラル:対応、ただし
Float64
のみ- Integers and Floating-Point Numbers · The Julia Language
- アンダースコアを区切り文字として利用可能。
- 文字列化:
-
Printfで
%a
,%A
が利用できます - 文字列からの変換:
- parse関数が利用できます。ドキュメントには For floating-point types, the string is parsed as a decimal floating-point number. と書かれているのですが…。
JavaScript (ECMAScript)
非対応です。軽くググった感じではproposalもなさそうです。
ちなみに、JavaScriptでは10進整数リテラルの直後にドットを置いてプロパティーを参照することはできませんが、16進リテラルのあとは直後のドットがプロパティーアクセスとみなされるようです。前者は「小数部分が空の小数リテラル」と解釈されたのでしょう。
> 42.toFixed(3) // 整数リテラルの直後に空白等を入れずにメソッド呼び出しはできない
42.toFixed(3)
^^^
Uncaught SyntaxError: Invalid or unexpected token
> 42 .toFixed(3) // 整数リテラルの直後に空白があるのでOK
'42.000'
> 42..toFixed(3) // 小数リテラル 42. の後ならOK
'42.000'
> 0x2a.toFixed(3) // 16進整数リテラルの後はOK→16進浮動小数点リテラルを導入すると破壊的変更となる
'42.000'
新しくプログラミング言語を作る人へ
何らかの形で16進浮動小数点数に対応してください。お願いします。リテラルに対応しろとは無理には言いませんが、文字列との変換は必須です(IEEEでも規定されてますし)。
それから、整数リテラル直後のドットでメソッド呼び出し等をできるようにしてしまうと、「後付けで16進浮動小数点数リテラルに対応する」という芸当が不可能になります。注意しましょう。