はじめに
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
いい感じのようになりました。
おわりに
おわりです。