Bankers rounding

先ほど、Bankers rounding が終了した。この問題では、C で新たな発見(私にとっての)がいくつかあり、面白かった。

その1

printf() の "%.f" の丸めモードは、「最近接偶数へのまるめ」(と呼ぶのか)のようだ。本問のタイトル "Bankers' rounding" だ。端数が 0.5 の時は、切り上げるのではなく、最も近い偶数に丸めるという方法。つまり、

1.5 => 2
2.5 => 2
3.5 => 4

となる。てっきり「四捨五入」だと思っていたので驚き。C99/JISX3010を見てみたが、特に規定はされていないようだった。gcc の実装ということか。因みに、Java の printf() は、「四捨五入」のようだ。

その2

scanf() の "%f" は、float 型を扱うが、printf() の "%f" は double 型を扱う。
現在主流の実装は、float は 32bit、double は 64bit で、あなごるサーバーもそうである。double 型は、int 型(32bit) 2つ分のサイズだ。
この非対称性だが、scanf() では、double 型を扱うようにする方法が用意されている。"%lf" だ。こうすると、対応する引数は、double 型のアドレスが期待される。int 二つ分の入れ物のアドレスだ。なお、printf() には、逆に、float 型を扱うための指定は用意されていない。それは、float を渡すことができないからだ?!理由は、次の「その3」のため。

その3

関数の引数に float 型を渡すと実際には、double 型に変換されて関数へ渡される。より正確には、JISX3010によると、

6.5.2.2 関数呼び出し
...
意味規則
...
呼び出される関数を表す式が、関数原型を含まない型をもつ場合、各実引数に対して整数拡張を行い、型 float をもつ実引数は型 double に拡張する。

となっている。「関数原型」とは、「プロトタイプ」の訳だろう。要するに、関数が明示的に float を引数として受け取ると宣言されていない限り、double に拡張されるということで、特に、printf() のような可変個引数の場合は、常に、double になるということだ。

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

さて、解答に関してだが、まず、

main(v){for(;~scanf("%f",&v);)printf("%.f\n",v);}        // NG
main(float v){for(;~scanf("%f",&v);)printf("%.f\n",v);}  // OK

で、int v も float v もどちらも 32bit で同じサイズなのに、なんで、上行はNG なのか?理由は、「その3」で述べたように、コンパイラーが printf() に float v を渡す時に、double に拡張変換しているためだ。ならば、以下は?

main(long long v){for(;~scanf("%f",&v);)printf("%.f\n",v);} // NG

これも NG 。確かに printf() には、long long により double と同じサイズが渡るが、値は、scanf() により、float のままなのでだめということ。なので、次のように、scanf() に double として扱わさせればよい。

main(long long v){for(;~scanf("%lf",&v);)printf("%.f\n",v);} // OK

ただ、これだと long long と宣言しなければならず、ゴルフ的には B の無駄遣いだ。ここで、次のような技が使える。int 2つ分なので、int の変数を2つ用意すればよい。

x,y;main(){for(;~scanf("%lf",&x);)printf("%.f\n",x,y);}  // OK

x と y が連続してその順番でメモリー上に配置されるため、scanf() は double 値を x と y 上に展開する。printf() には、その double 値を x と y という2引数で渡してやると printf() は素直に double と認識する。これで、55B だ。
ここで、フッと頭をよぎったのが、vprintf() なるもの。もしかしたら、scanf() と同じ引数構造なのでは?と思い、やってみたら、まさにそうだった。

main(v){for(;~scanf("%lf",&v);)vprintf("%.f\n",&v);}  // OK

v は int なので、32bit だが、その隣のワードまで使って double 値を渡している。
ここまで来ると、scanf() と vprintf() の第二引数がまったく同じなので、例の引数暗黙渡しができるのではないかと思い、やってみたらできた。それが、49B で以下の通り。

main(v){for(;~scanf("%lf",&v);)vprintf("%.f\n");}