CLI や C# における -0 (マイナスゼロ) や符号のコピー

サンプルコード

float pZero = 0.0f;
float nZero = -0.0f;

// True
Debug.WriteLine("pZero == nZero: " + (pZero == nZero));
// False
Debug.WriteLine("+∞ / pZero == +∞ / nZero: " + (float.PositiveInfinity / pZero == float.PositiveInfinity / nZero));

// +∞
Debug.WriteLine("+∞ / pZero: " + float.PositiveInfinity / pZero);
// -∞
Debug.WriteLine("+∞ / nZero: " + float.PositiveInfinity / nZero);

このエントリは、pZero == nZero なのに、+∞ / pZero と +∞ / nZero の結果が != なのはなぜなのか、に関わる話である。

参考

C# や CLS における浮動小数の仕様

C#浮動小数は規格で IEC 60559 な浮動小数であると定められている。
また、CLI*1 の一部である CLS*2 で用いている浮動小数もそうである。

なお、前者は C# 言語仕様書第二版の§11.1.6、後者は CLI 仕様書の§8.2.2 で明示してあようだ。

IEC 60559 や IEEE 754 における +0 と -0

この IEC 60559 (IEEE 754 とも)では、+0 と -0 という二つの概念があり、それらが区別される。なお、符号のない 0 という概念はなく、0 は +0 か -0 のどちらかである。

これについては C# 言語仕様書でも説明されており、例えば乗算については x * y について

x*y +y -y +0 -0 +∞ -∞ NaN
+x +z -z +0 -0 +∞ -∞ NaN
-x -z +z -0 +0 -∞ +∞ NaN
+0 +0 -0 +0 -0 NaN NaN NaN
-0 -0 +0 -0 +0 NaN NaN NaN
+∞ +∞ -∞ NaN NaN +∞ -∞ NaN
-∞ -∞ +∞ NaN NaN -∞ +∞ NaN
NaN NaN NaN NaN NaN NaN NaN NaN

である、といった表が§14.7.1 にある。

ちなみに除算であれば、x / y の表は

x/y +y -y +0 -0 +∞ -∞ NaN
+x +z -z +∞ -∞ +0 -0 NaN
-x -z +z -∞ +∞ -0 +0 NaN
+0 +0 -0 NaN NaN +0 -0 NaN
-0 -0 +0 NaN NaN -0 +0 NaN
+∞ +∞ -∞ +∞ -∞ NaN NaN NaN
-∞ -∞ +∞ -∞ +∞ NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN

となるそうだ。

C# や CLS における +0 と -0

という訳で、C#CLI/CLS においても +0 や -0 の区別が実は存在している。

ただし、C# 仕様書§14.9.2 に

-∞ < -max < ... < -min < -0.0 == +0.0 < +mix < ... < +max < +∞

とある通り、-0 と +0 は基本的に同値扱いであるので、普段はその違いを意識する理由がない。

とはいえ、+0 と -0 は == であるが異なる振る舞いをする事がある。例えば、上に挙げたサンプルコードや x / y の表にあるように、+∞ / +0 は +∞ であるのに対して、+∞ / -0 は -∞ と異なる結果になる。

ちなみに、C#ソースコード上で +0.0f、0.0f、-0.0f を書くと、それぞれ +0、+0、-0 に対応するようにコンパイルしてくれるので、エントリ先頭にあるサンプルコードのように +0 と -0 を意識的に作り出すことも可能である。

+0 と -0 の区別と符号のコピーとゼロ除算

実は IEC 60559 では CopySign という関数の定義が与えられており、これを用いると符号のコピーという操作ができる(例: Java での CopySign)。

この関数がすでにあるならば、CopySign(+0.0, 1) が +1 になることや CopySign(-0.0, 1) が -1 になることを使って正負のゼロを区別できる。

が、見たところ BCL*3 には例えば System.Math.CopySign といったメソッドは無いようだ。また System.Math.Sign(float) といったメソッドはあるものの、これらは与えた値が +0 や -0 である場合、戻り値も +0 か -0 になるので、正負のゼロを区別する用途には使えない。

とはいえ、+0 と -0 は先述の通り振る舞いが違うこともあるので、とりあえずそれを判定なりするコードがほしくもある。そこで、上に挙げた仕様を用いて +0 と -0 を区別するコードを書いてみた。

書いたコード: Source | SVN | Assembla

このコードでは、float と double の拡張メソッドとして

  • IsPositive()
  • IsNegative()
  • CopySign(sign)

といったメソッドを実装してある。

このメソッドたちの実装では、浮動小数(float, double)をゼロ除算した場合に、除数が +0 か -0 かどうかで結果(無限大)の符号が変わることを利用している。普通ならば回避することが多いゼロ除算をあえて使うことになろうとは思っていなかったので新鮮である。

ただ、BCL に CopySign といった関数がなく、Math.Sign もゼロの符号を区別しないあたり、BCL (や .net framework など)の設計者は意図的に +0 と -0 の差異を意識しない方向に仕様としていたのかもしれず、何か深いわけがあるかもしれないので、作って動いてはいるが、一応注意が必要である。

*1:共通言語基盤

*2:共通言語仕様

*3:標準クラスライブラリ