2016年10月21日金曜日

変数の値が非数 NaNかどうか調べるために、定数 NANと比較してはならない。

次に示すのは CUnitを用いたテストケースである。
/**
 * 定数 NANと比較すると偽を返すこと。
 *
 * NaNかどうか判定するために、定数 NANと比較してはならない。
 */
public void testCompareNanAndNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = (value == NAN);

        /* 事後条件の検証 */
        Assert_false("結果は偽であること。", result);
}
これを実行すると、テストは成功する。
変数 valueが定数 NANで初期化されているが、その変数を NANと比較しても真にならない。
比較演算子で NANと比較しても NaNかどうか判断できないということで、これは正しい挙動である。
なお、このテストケースは gccを使用してコンパイルしたが、定数 NANを使用するためには math.hをインクルードする必要がある。
ISOの C99よりも前のバージョンでは、処理系によっては NANが使えない。
NaNであるかどうかの正しい判定方法を次のテストケースに示す。
/**
 * NaNであることを判定できること。
 *
 * NaNかどうかの判定には isnan()を使用すること。
 */
public void testIsNan() {
        /* 事前条件の準備 */
        double value = NAN;

        /* 実行 */
        boolean result = isnan(value);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
isnan()も math.hが必要。
==比較演算子を使用して「強引に」判定する例を次に示す。
/**
 * ポインタを使用して NANと比較することで、NaNかどうかを判定できること。
 *
 * この使い方を推奨しているわけではないが、定数を代入しただけのものは
 * ビットパターンの比較により判断ができる。
 * この例ではポインタを使用したが、unionを用いても同様のことができる。
 *
 * NaNの値は1通りではないため、計算結果や外部から読み込んだ値を正しく
 * 判断するには == は使えない。
 */
public void testCompareNanAsPointer() {
        /* 事前条件の準備 */
        double value = NAN;
        double nan = NAN;

        /* 実行 */
        boolean result = (*(long long*)&value == *(long long*)&nan);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
ドキュメントコメントにも書いてあるよう、この方法では特定の条件でした判断できない。
非数 NaNを理解するための例をもう1つ示しておこう。
/**
 * NaNは 指数部の全ビットが 1で、仮数部が 0でないこと。
 *
 * これは例を示しただけで、実際にやるのは無駄。 isnan()を使うべき。
 */
public void testNanAsPattern() {
        /* 事前条件の準備 */
        double value = NAN;
        long long exponentMask = 0x7ff0000000000000;
        long long mantissaMask = 0x000fffffffffffff;

        /* 実行 */
        long long longValue = *(long long*)&value;
        boolean result = ((longValue & exponentMask) == exponentMask)
                && ((longValue & mantissaMask) != 0);

        /* 事後条件の検証 */
        Assert_true("結果は真であること。", result);
}
valueを定数 NANで初期化したものだけでなく、0.0 / 0.0した結果も正しく判断できる。
printfを使って調べると、gccの場合、NANのパターンは 0x7ff8000000000000、0.0 / 0.0の結果は 0xfff8000000000000で 1ビット異なるが、いづれも NaNと判定できた。
注:value = 0.0 / 0.0; というコードはコンパイル時点でエラーになる。 分母を変数にしてその変数を 0.0で初期化するようにすれば、コンパイルが通るようになる。

普段の判定は isnan()を使用すればよいが、isnan()が使えない場合や NaNを使ったテストケースを記述するとき、この知識は役にたつだろう。
なお、上記の例で使った CUnitテストフレームワークについてはこのブログで紹介してきたが、はじめてみる人のために簡単に説明しておく。
これらのテスト関数はつぎのようなコードを含むソースコードとして記述され、softiesの CUnitライブラリとリンクされる。
#define import_lwd_unit_Assert
#define import_lwd_unit_CUnit

#include <lwd/lwd.h>
#include <lwd/unit/Assert.h>
#include <lwd/unit/CUnit.h>
#include <math.h>

/**
 * @file
 *
 * NaNの振る舞いのテスト。
 */

/****************************************************************
        test cases
****************************************************************/

// ここに、テスト関数を記述する。
// テスト関数名が異なれば複数個記述可能。

/****************************************************************
        main
****************************************************************/

public int main(int argc, char** argv) {
        return CUnit_test(argc, argv);
}
main関数のなかで CUnit_test()を呼び出すことにより、自動的にテスト関数を認識して実行する。
なおテスト関数の名前は textXXXのように先頭4文字は testでなければならない。

0 件のコメント:

コメントを投稿