一見したところ、この質問は 整数オーバーフローの検出方法 の複製のように見えるかもしれませんが、実際には大きく異なります。
符号なし整数のオーバーフローを検出することは非常に簡単ですが、C/C++でsignedオーバーフローを検出することは、実際にはほとんどの人が考えるよりも難しいことがわかりました。
最も明白でありながら単純な方法は、次のようなものです。
int add(int lhs, int rhs)
{
int sum = lhs + rhs;
if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
/* an overflow has occurred */
abort();
}
return sum;
}
これに伴う問題は、C標準によると、符号付き整数オーバーフローが未定義の動作であるということです。つまり、標準に従って、符号付きオーバーフローを引き起こしても、プログラムはnullポインターを逆参照した場合と同様に無効です。したがって、上記の事後条件チェックの例のように、未定義の動作を引き起こして、事後のオーバーフローの検出を試みることはできません。
上記のチェックは多くのコンパイラで機能する可能性が高いとはいえ、それを期待することはできません。実際、C標準では符号付き整数オーバーフローは未定義であると規定されているため、一部のコンパイラ(GCCなど)は、最適化フラグが設定されていると 上記のチェックを最適化 します。これにより、オーバーフローをチェックする試みが完全に中断されます。
したがって、オーバーフローをチェックする別の可能な方法は次のとおりです。
int add(int lhs, int rhs)
{
if (lhs >= 0 && rhs >= 0) {
if (INT_MAX - lhs <= rhs) {
/* overflow has occurred */
abort();
}
}
else if (lhs < 0 && rhs < 0) {
if (lhs <= INT_MIN - rhs) {
/* overflow has occurred */
abort();
}
}
return lhs + rhs;
}
このような加算を実行してもオーバーフローが発生しないことを事前に確認するまで、実際に2つの整数を加算しないため、これはより有望なようです。したがって、未定義の動作は発生しません。
ただし、残念ながら、このソリューションは、加算操作が機能するかどうかをテストするために減算操作を実行する必要があるため、初期ソリューションよりもはるかに効率が低くなります。そして、この(小さな)パフォーマンスヒットを気にしなくても、このソリューションが適切であると完全に確信しているわけではありません。表現 lhs <= INT_MIN - rhs
は、符号付きオーバーフローは不可能であると考えて、コンパイラが最適化する可能性のある式とまったく同じように見えます。
ここにもっと良い解決策がありますか? 1)未定義の動作を引き起こさないこと、2)コンパイラーにオーバーフローチェックを最適化する機会を与えないことが保証されているもの私は両方のオペランドを符号なしにキャストし、独自の2の補数演算をロールしてチェックを実行することで何らかの方法があると考えていましたが、それを行う方法は本当にわかりません。
減算を使用したアプローチは正しく、明確に定義されています。コンパイラはそれを最適化することはできません。
より大きな整数型が利用可能な場合、別の正しいアプローチは、より大きな型で算術演算を実行し、それを元に戻すときに結果がより小さな型に収まることを確認することです
int sum(int a, int b)
{
long long c;
assert(LLONG_MAX>INT_MAX);
c = (long long)a + b;
if (c < INT_MIN || c > INT_MAX) abort();
return c;
}
優れたコンパイラーは、加算とif
ステートメント全体をint
サイズの加算と単一の条件付きジャンプオンオーバーフローに変換し、実際に大きな加算を実行することはありません。
編集: Stephenが指摘したように、私は(あまり良くない)コンパイラーgccを取得して正常なasmを生成するのに問題があります。それが生成するコードはひどく遅くはありませんが、確かに最適ではありません。 gccに正しいことをさせるこのコードのバリアントを知っている人がいれば、ぜひ見たいと思います。
いいえ、2番目のコードは正しくありませんが、近くにいます:設定した場合
int half = INT_MAX/2;
int half1 = half + 1;
加算の結果はINT_MAX
。 (INT_MAX
は常に奇数です)。したがって、これは有効な入力です。しかし、あなたのルーチンでは、INT_MAX - half == half1
そして中止します。誤検知。
このエラーは、<
の代わりに <=
両方のチェックで。
しかし、その後、あなたのコードは最適ではありません。次のようにします:
int add(int lhs, int rhs)
{
if (lhs >= 0) {
if (INT_MAX - lhs < rhs) {
/* would overflow */
abort();
}
}
else {
if (rhs < INT_MIN - lhs) {
/* would overflow */
abort();
}
}
return lhs + rhs;
}
これが有効であることを確認するには、不等式の両側にlhs
を記号で追加する必要があります。これにより、結果が範囲外であるという算術条件が正確に得られます。
私見、センシティブC++コードのオーバーフローを処理する最も簡単な方法は、SafeInt<T>
。これは、コードプレックスでホストされるクロスプラットフォームC++テンプレートであり、ここで希望する安全性の保証を提供します。
通常の数値演算と同じ使用パターンの多くを提供し、例外を介してフローの過不足を表現するため、非常に直感的に使用できます。
Gccの場合、 gcc 5.0リリースノート から、__builtin_add_overflow
さらにオーバーフローをチェックするため:
オーバーフローチェック付きの算術演算用の新しい組み込み関数セットが追加されました。これらの組み込み関数には2つの整数の引数があり(同じ型である必要はありません)、引数は無限精度の符号付き型に拡張され、+、-、または*がそれらに対して実行され、結果が指す整数変数に格納されます最後の引数によって。格納された値が無限精度の結果と等しい場合、組み込み関数はfalseを返します。それ以外の場合はtrueを返します。結果を保持する整数変数の型は、最初の2つの引数の型と異なる場合があります。
例えば:
__builtin_add_overflow( rhs, lhs, &result )
Gccドキュメントから見ることができます オーバーフローチェックで算術を実行する組み込み関数
[...]これらの組み込み関数には、すべての引数値に対して完全に定義された動作があります。
clangはまた、一連の チェック済み算術ビルトイン を提供します。
Clangは、Cで高速かつ簡単に表現できる方法で、セキュリティが重要なアプリケーションのチェック演算を実装する組み込みのセットを提供します。
この場合、組み込みは次のようになります。
__builtin_sadd_overflow( rhs, lhs, &result )
インラインアセンブラを使用する場合、 オーバーフローフラグ を確認できます。別の可能性は、 safe int data type を使用できることです。 整数セキュリティ に関するこのペーパーを読むことをお勧めします。
最速の方法は、GCCビルトインを使用することです:
_int add(int lhs, int rhs) {
int sum;
if (__builtin_add_overflow(lhs, rhs, &sum))
abort();
return sum;
}
_
X86では、GCCはこれを次のようにコンパイルします。
_ mov %edi, %eax
add %esi, %eax
jo call_abort
ret
call_abort:
call abort
_
プロセッサの組み込みオーバーフロー検出を使用します。
GCCビルトインを使用しても問題ない場合、次に速い方法は、符号ビットにビット演算を使用することです。さらに、次の場合に符号付きオーバーフローが発生します。
~(lhs ^ rhs)
の符号ビットは、オペランドの符号が同じである場合にオンになり、_lhs ^ sum
_の符号ビットは、結果がオペランドと異なる符号を持つ場合にオンになります。したがって、未定義の動作を回避するために符号なしの形式で追加を行い、~(lhs ^ rhs) & (lhs ^ sum)
の符号ビットを使用できます。
_int add(int lhs, int rhs) {
unsigned sum = (unsigned) lhs + (unsigned) rhs;
if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
abort();
return (int) sum;
}
_
これは以下にコンパイルされます。
_ lea (%rsi,%rdi), %eax
xor %edi, %esi
not %esi
xor %eax, %edi
test %edi, %esi
js call_abort
ret
call_abort:
call abort
_
これは、32ビットマシン(gccを使用)で64ビットタイプにキャストするよりもかなり高速です。
_ Push %ebx
mov 12(%esp), %ecx
mov 8(%esp), %eax
mov %ecx, %ebx
sar $31, %ebx
clt
add %ecx, %eax
adc %ebx, %edx
mov %eax, %ecx
add $-2147483648, %ecx
mov %edx, %ebx
adc $0, %ebx
cmp $0, %ebx
ja call_abort
pop %ebx
ret
call_abort:
call abort
_
どうですか:
int sum(int n1, int n2)
{
int result;
if (n1 >= 0)
{
result = (n1 - INT_MAX)+n2; /* Can't overflow */
if (result > 0) return INT_MAX; else return (result + INT_MAX);
}
else
{
result = (n1 - INT_MIN)+n2; /* Can't overflow */
if (0 > result) return INT_MIN; else return (result + INT_MIN);
}
}
正当なINT_MIN
およびINT_MAX
(対称かどうか);クリップのように機能しますが、他の動作を取得する方法は明らかです。
64ビット整数に変換して、そのような同様の条件をテストする方が幸運かもしれません。例えば:
#include <stdint.h>
...
int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
// Overflow occurred!
}
else {
return sum;
}
ここでは、符号拡張がどのように機能するかを詳しく調べたいと思うかもしれませんが、それは正しいと思います。
私によると、最も簡単なチェックは、オペランドと結果の符号をチェックすることです。
合計を調べてみましょう。両方のオペランドに同じ符号がある場合にのみ、オーバーフローが+または-の両方向で発生する可能性があります。そして、明らかに、オーバーフローは、結果の符号がオペランドの符号と同じにならない場合に発生します。
したがって、次のようなチェックで十分です。
int a, b, sum;
sum = a + b;
if (((a ^ ~b) & (a ^ sum)) & 0x80000000)
detect_oveflow();
編集:Nilsが示唆したように、これは正しいif
条件です:
((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)
そして、以来、命令
add eax, ebx
未定義の動作につながりますか? Intel x86命令セットの参照にはそのようなものはありません。
明白な解決策は、明確に定義された符号なしオーバーフロー動作を得るために、符号なしに変換することです:
int add(int lhs, int rhs)
{
int sum = (unsigned)lhs + (unsigned)rhs;
if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
/* an overflow has occurred */
abort();
}
return sum;
}
これにより、未定義の符号付きオーバーフロー動作が実装定義の符号付きと符号なしの範囲外の値の変換に置き換えられるため、コンパイラのドキュメントを確認して正確に何が起こるかを知る必要がありますが、少なくとも十分に定義する必要があります。変換時にシグナルを発生させない2の補数のマシンで正しいことを行う必要があります。これは、過去20年間に構築されたほとんどすべてのマシンとCコンパイラです。
2つのlong
値を追加する場合、移植可能なコードはlong
値を低い_と高いint
部分(またはshort
部分の場合はlong
部分に分割できます。 $ var] _はint
と同じサイズです):
static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'
特定のCPUを対象とする場合、インラインアセンブリを使用するのが最も速い方法です。
long a, b;
bool overflow;
#ifdef __AMD64__
asm (
"addq %2, %0; seto %1"
: "+r" (a), "=ro" (overflow)
: "ro" (b)
);
#else
#error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'