🙄

printfの再実装をしたたかにやってみる(4)

に公開

まえおき

これはシリーズ記事です。
https://zenn.dev/monksoffunk/articles/c311aab03d6dfc
https://zenn.dev/monksoffunk/articles/664f5407027e81
https://zenn.dev/monksoffunk/articles/495a5096a5bc42

掲載するコードは説明のために書かれたものであり動作の保証はありません。また、完成形のコードは公開しません。ぜひご自身で書いてください。

浮動小数点数のいろいろな変換指定子

Cを常用していないエンジニアであってもprintfを使ったことぐらいはあるもの。文字列の表示に使う%sやint型の変数の内容表示用の%dといった変換指定子は馴染みがあるんじゃないでしょうか。一方であまりメジャーとは言えない変換指定子もあります。たとえば%g。

浮動小数点の表示には一般的に%fや%eを使うことが多いのではないかと思います。%fは0.125のような十進数と小数点のみで構成される表記。%eは指数を伴う科学表記です。

printf("%f", 0.125);
0.125000

printf("%e", 0.125);
1.250000e-01

%gは場合に応じて%fか%eが自動選択される変換指定子です。変換後の指数が-4より小さいか精度以上である場合に%eが使用されることになっています。

printf("%g", 0.125);
0.125

printf("%g", 0.000125);
0.000125

printf("%g", 0.0000125);
1.25e-05

こうした条件判定を正しく行って切り替えてあげるだけで実装可能です。

%a変換指定子

さらにマイナーなのがこの%aです。
manを見ると (C99; not in SUSv2, but added in SUSv3) となっていますので、比較的(比較的ですよ!)新しい変換指定子です。man(glibc版)の続きを見ましょう。

the double argument is converted to hexadecimal notation
 (using the letters abcdef) in the style [-]0xh.hhhhp±d;

16進数表記で表すことがわかります。
実際に0.125を出力してみましょう。

printf("%a", 0.125);
0x1p-3

上記のように10進数の0.125を%aで表示させると0x1p-3になります。

符号部 | 指数部     | 仮数部
0     011 1111 1100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

指数部は0x3fc = 1020ですので、バイアスである1023を差し引いた-3が指数になります。
仮数部は0なので、暗黙の1(ケチ表現)が整数部にセットされて1。結果、0x1p-3となります。

同様にしてDBL_MINの出力は以下のようになります。

printf("%a", DBL_MIN);
0x1p-1022

%aの形式が活きるのは、たとえば次のようなビット列を持つ値の場合です。

符号部 | 指数部     | 仮数部
0        011 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
0x1.fffffffffffffp+0

整数部の1(暗黙の1)に続き、仮数部の52ビットがそのまま16進数で表示されます。これが%.100fでは以下のようになり、元のビット列を想起するのは困難です。

1.9999999999999997779553950749686919152736663818359375000000000000000000000000000000000000000000000000

libcによる挙動の差異

UNIX系環境においてprintfは標準Cライブラリであるlibcに含まれていますが、libcの実装は様々です。基本的にはその挙動に変わりはないものの、細かい部分では異なる部分もあります。%aでもglibcとBSD系のlibcでは挙動に違いがあります。
たとえば次のケースを見てください。

printf("%a", DBL_TRUE_MIN);
0x0.0000000000001p-1022  <= glibc (linux)
0x1p-1074 <= FreeBSD based libc (macos)

glibcではありのままのビット列を16進数で表現します。DBL_TRUE_MINは仮数部52ビットのLSBのみが立っている状態ですから、52/4=13桁目(小数点以下の13桁目)のみ1となるわけです。
一方、macosのlibcでは0以外の値の場合に整数部を0以外にするよう、正規化が実行されます。こちらは一目で指数が把握出来るというメリットがあります。

Discussion