9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

int の値の絶対値を unsigned で返す関数

Last updated at Posted at 2021-12-16

はじめに

C の標準ライブラリにある関数 abs() は、負数の表現に 2の補数を採用している環境では引数に INT_MIN を与えた場合の動作が未定義となるみたいで、「そんなら unsigned で返しゃいんじゃね?」と思ったのでした。

7.20.6.1 abs,labs,及びllabs関数

形式

   #include <stdlib.h>
   int abs(int j);
   long int labs(long int j);
   long long int llabs(long long int j);

機能 abs,labs,及びllabs関数は,整数jの絶対値を計算する。結果が表現できないとき,その動作は未定義とする(255)。


(255) 最小の負数の絶対値は,2の補数では表現不可能である。

大して考えないで実装してみた

とりあえず大して考えないで実装してみたのが以下の関数です。

unsigned myabs(int i)
{
    if (i >= 0) {
        return i;
    } else {
        return -i;
    }
}

値が負のときには単項演算子 - を使って符号を反転させりゃいんじゃね以上のことは考えておりません。

テスト用のコードも用意しました。

#include <stdio.h>
#include <limits.h>

void test(int i)
{
    printf("myabs(%11d) = %10u\n", i, myabs(i));
}

int main(void)
{
    // INT_MAX 付近の値をテスト
    test(INT_MAX);
    test(INT_MAX - 1);
    test(INT_MAX - 2);

    // 0 付近の値をテスト
    for (int i = 3; i >= -3; i--) {
        test(i);
    }

    // INT_MIN 付近の値をテスト
    test(INT_MIN + 2);
    test(INT_MIN + 1);
    test(INT_MIN);
}

↑のふたつをひとつのソースにまとめて、gcc 11.2 を使用して最適化オプション -O2 を指定してコンパイル、実行してみた結果がコチラ

実行結果
myabs( 2147483647) = 2147483647
myabs( 2147483646) = 2147483646
myabs( 2147483645) = 2147483645
myabs(          3) =          3
myabs(          2) =          2
myabs(          1) =          1
myabs(          0) =          0
myabs(         -1) =          1
myabs(         -2) =          2
myabs(         -3) =          3
myabs(-2147483646) = 2147483646
myabs(-2147483647) = 2147483647
myabs(-2147483648) = 2147483648

いい感じのようです。
ついでに clang 13.0.0 でも試してみました。

実行結果
myabs( 2147483647) = 2147483647
myabs( 2147483646) = 2147483646
myabs( 2147483645) = 2147483645
myabs(          3) =          3
myabs(          2) =          2
myabs(          1) =          1
myabs(          0) =          0
myabs(         -1) =          1
myabs(         -2) =          2
myabs(         -3) =          3
myabs(-2147483646) = 2147483646
myabs(-2147483647) = 2147483647
myabs(-2147483648) =          0

なんか、引数に INT_MIN を与えた場合にうまいこといってない感じですね。

どうしてこうなった

myabs() のなかの引数の値が負の値だった場合の処理

        return -i;

で i の値が INT_MIN だった場合、単項演算子 - の計算結果が INT_MIN〜INT_MAX の範囲に収まらないためオーバーフローが発生し、i の型は符号付き整数なのでそれのオーバーフローはC言語の仕様として未定義動作となるため、コンパイラが「ここは好き勝手してよいとこだな」と判断しておかしな結果となったようです。
コンパイル結果のコードを見てみると main() の最後のほう

        mov     edi, offset .L.str
        mov     esi, -2147483646
        mov     edx, 2147483646
        xor     eax, eax
        call    printf
        mov     edi, offset .L.str
        mov     esi, -2147483647
        mov     edx, 2147483647
        xor     eax, eax
        call    printf
        mov     edi, offset .L.str
        mov     esi, -2147483648
        xor     eax, eax
        call    printf
        xor     eax, eax
        pop     rcx
        ret

printf() 呼び出しの際の引数に、INT_MIN-2 と iNT_MIN-1 の場合では myabs() が返したであろう値がedxレジスタにセットされていますが、INT_MIN に対しては myabs() が返したであろう値をedxレジスタにセットしている命令がありません。コンパイラが「ここは好き勝手してよいとこ」と判断した結果、引数の設定を丸ごと削除したようです。
コンパイルオプションに-fsanitize=undefinedを加えてコンパイル、実行してみると標準エラー出力に実行エラーが確認できました。

/app/example.c:6:16: runtime error: negation of -2147483648 cannot be represented in type 'int'; cast to an unsigned type to negate this value to itself
SUMMARY: UndefinedBehaviorSanitizer: undefined-

じゃあどうしよう

符号付き整数のオーバーフローが原因のようなのでそれが起こらぬようコードを以下に改めました。

unsigned myabs(int i)
{
    if (i >= 0) {
        return i;
    } else {
        return -(i + 1) + 1U;
    }
}

変更箇所は i の値が負だった場合の処理

        return -(i + 1) + 1U;

だけです。
i の値 -1~-2147483648 に 1 を足すことで 0~-2147483647 の範囲へ変換し、単項演算子 - を使用して符号を反転して 0~2147483647 の範囲へ変換、符号なしの整数 1 と加算することで符号なしの 1~2147483648 の範囲の値へ変換しています。

clang 13.0.0 を使用してコンパイル、実行し結果を確認してみたところ

実行結果
myabs( 2147483647) = 2147483647
myabs( 2147483646) = 2147483646
myabs( 2147483645) = 2147483645
myabs(          3) =          3
myabs(          2) =          2
myabs(          1) =          1
myabs(          0) =          0
myabs(         -1) =          1
myabs(         -2) =          2
myabs(         -3) =          3
myabs(-2147483646) = 2147483646
myabs(-2147483647) = 2147483647
myabs(-2147483648) = 2147483648

いい感じのようになりました。

おわりに

おわりです。

9
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?