Na pierwszy rzut oka to pytanie może wydawać się duplikatem pytania Jak wykryć przepełnienie liczb całkowitych? jednak w rzeczywistości jest znacząco inny.
Odkryłam, że podczas wykrywania przepełnienia całkowitą bez znaku jest dość trywialne, wykrywanie podpisane przepełnienie w C / C ++ jest rzeczywiście trudniejsze, niż sądzi większość ludzi.
Najbardziej oczywistym, ale naiwnym sposobem byłoby coś takiego:
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;
}
Problem polega na tym, że zgodnie ze standardem C przepełnienie liczb całkowitych ze znakiem jest niezdefiniowanym zachowaniem. Innymi słowy, zgodnie ze standardem, gdy tylko spowodujesz przepełnienie ze znakiem, Twój program będzie tak samo nieprawidłowy, jak gdybyś wyłuskał wskaźnik zerowy. Nie możesz więc spowodować niezdefiniowanego zachowania, a następnie spróbuj wykryć przepełnienie po fakcie, tak jak w powyższym przykładzie sprawdzania warunku końcowego.
Mimo że powyższe sprawdzenie prawdopodobnie zadziała na wielu kompilatorach, nie możesz na to liczyć. W rzeczywistości, ponieważ standard C mówi, że przepełnienie ze znakiem całkowitym jest niezdefiniowane, niektóre kompilatory (takie jak GCC) zoptymalizują powyższe sprawdzenie, gdy ustawione są flagi optymalizacji, ponieważ kompilator zakłada, że przepełnienie ze znakiem jest niemożliwe. To całkowicie przerywa próbę sprawdzenia przepełnienia.
Tak więc inny możliwy sposób sprawdzenia przepełnienia to:
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;
}
Wydaje się to bardziej obiecujące, ponieważ tak naprawdę nie dodajemy do siebie dwóch liczb całkowitych, dopóki nie upewnimy się z góry, że wykonanie takiego dodania nie spowoduje przepełnienia. W ten sposób nie powodujemy żadnego niezdefiniowanego zachowania.
Jednak to rozwiązanie jest niestety dużo mniej wydajne niż rozwiązanie początkowe, ponieważ musisz wykonać operację odejmowania, aby sprawdzić, czy operacja dodawania zadziała. I nawet jeśli nie przejmujesz się tym (małym) hitem wydajnościowym, nadal nie jestem do końca przekonany, że to rozwiązanie jest odpowiednie. Wyrażenie lhs <= INT_MIN - rhs
wygląda dokładnie tak, jak rodzaj wyrażenia, które kompilator może zoptymalizować, myśląc, że przepełnienie ze znakiem jest niemożliwe.
Czy jest tutaj lepsze rozwiązanie? Coś, co gwarantuje, że 1) nie spowoduje nieokreślonego zachowania i 2) nie zapewni kompilatorowi możliwości optymalizacji kontroli przepełnienia? Pomyślałem, że może być jakiś sposób na zrobienie tego przez rzutowanie obu operandów na niepodpisane i wykonywanie sprawdzeń przez zwijanie własnej arytmetyki uzupełnień do dwóch, ale nie jestem pewien, jak to zrobić.