Auf den ersten Blick scheint diese Frage ein Duplikat von Wie erkennt man einen Ganzzahlüberlauf? es ist jedoch tatsächlich deutlich anders.
Ich habe festgestellt, dass das Erkennen eines vorzeichenlosen Ganzzahlüberlaufs zwar ziemlich trivial ist, das Erkennen eines vorzeichenbehafteten Überlaufs in C / C ++ jedoch schwieriger ist, als die meisten Leute denken.
Der naheliegendste und doch naivste Weg, dies zu tun, wäre etwa:
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;
}
Das Problem dabei ist, dass gemäß dem C-Standard ein vorzeichenbehafteter Ganzzahlüberlauf ein undefiniertes Verhalten ist. Mit anderen Worten, gemäß dem Standard ist Ihr Programm genauso ungültig, als ob Sie einen Nullzeiger dereferenziert hätten, sobald Sie überhaupt einen signierten Überlauf verursachen. Sie können also kein undefiniertes Verhalten verursachen und dann versuchen, den Überlauf nachträglich zu erkennen, wie im obigen Beispiel für die Überprüfung nach der Bedingung.
Obwohl die obige Prüfung wahrscheinlich bei vielen Compilern funktioniert, können Sie sich nicht darauf verlassen. Da der C-Standard besagt, dass ein vorzeichenbehafteter Ganzzahlüberlauf undefiniert ist, optimieren einige Compiler (wie GCC) die obige Prüfung, wenn Optimierungsflags gesetzt sind, da der Compiler davon ausgeht, dass ein vorzeichenbehafteter Überlauf unmöglich ist. Dies unterbricht den Versuch, auf Überlauf zu prüfen, vollständig.
Eine andere Möglichkeit, den Überlauf zu überprüfen, wäre:
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;
}
Dies scheint vielversprechender zu sein, da wir die beiden Ganzzahlen erst dann addieren, wenn wir im Voraus sicherstellen, dass das Durchführen einer solchen Addition nicht zu einem Überlauf führt. Daher verursachen wir kein undefiniertes Verhalten.
Diese Lösung ist jedoch leider viel weniger effizient als die ursprüngliche Lösung, da Sie eine Subtraktionsoperation ausführen müssen, um zu testen, ob Ihre Additionsoperation funktioniert. Und selbst wenn Sie sich nicht für diesen (kleinen) Leistungseinbruch interessieren, bin ich immer noch nicht ganz davon überzeugt, dass diese Lösung angemessen ist. Der Ausdruck lhs <= INT_MIN - rhs
scheint genau der Art von Ausdruck zu sein, die der Compiler möglicherweise optimiert, da ein signierter Überlauf unmöglich ist.
Gibt es hier also eine bessere Lösung? Etwas, das garantiert 1) kein undefiniertes Verhalten verursacht und 2) dem Compiler keine Möglichkeit bietet, Überlaufprüfungen zu optimieren? Ich dachte, es könnte eine Möglichkeit geben, dies zu tun, indem beide Operanden in vorzeichenlose Zeichen umgewandelt werden und Überprüfungen durchgeführt werden, indem Ihre eigene Zwei-Komplement-Arithmetik gewürfelt wird, aber ich bin mir nicht sicher, wie ich das tun soll.
quelle
Antworten:
Ihr Ansatz mit Subtraktion ist korrekt und klar definiert. Ein Compiler kann es nicht weg optimieren.
Ein anderer korrekter Ansatz, wenn Sie einen größeren Ganzzahltyp zur Verfügung haben, besteht darin, die Arithmetik im größeren Typ auszuführen und dann zu überprüfen, ob das Ergebnis in den kleineren Typ passt, wenn Sie es zurückkonvertieren
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; }
Ein guter Compiler sollte die gesamte Addition und
if
Anweisung in eineint
Addition in Größe und einen einzelnen bedingten Jump-on-Overflow konvertieren und die größere Addition niemals tatsächlich ausführen.Bearbeiten: Wie Stephen betonte, habe ich Probleme, einen (nicht so guten) Compiler, gcc, zu bekommen, um den vernünftigen Asm zu generieren. Der Code, den es generiert, ist nicht besonders langsam, aber sicherlich nicht optimal. Wenn jemand Varianten dieses Codes kennt, die gcc dazu bringen, das Richtige zu tun, würde ich sie gerne sehen.
quelle
long long
vor dem Hinzufügen dumm weggelassen .sizeof(long long) == sizeof(int)
. C gibt nur das ansizeof(long long) >= sizeof(int)
.Nein, Ihr 2. Code ist nicht korrekt, aber Sie sind nah dran: wenn Sie einstellen
int half = INT_MAX/2; int half1 = half + 1;
das Ergebnis einer Addition ist
INT_MAX
. (INT_MAX
ist immer eine ungerade Zahl). Das ist also eine gültige Eingabe. Aber in Ihrer Routine werden Sie habenINT_MAX - half == half1
und Sie würden abbrechen. Ein falsches Positiv.Dieser Fehler kann , indem sie repariert werden ,
<
anstatt<=
in beiden Kontrollen.Aber dann ist auch Ihr Code nicht optimal. Folgendes würde reichen:
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; }
Um zu sehen, dass dies gültig ist, müssen Sie
lhs
auf beiden Seiten der Ungleichungen symbolisch addieren , und dies gibt Ihnen genau die arithmetischen Bedingungen, unter denen Ihr Ergebnis außerhalb der Grenzen liegt.quelle
/* overflow will occurred */
zu betonen, dass der springende Punkt darin besteht, zu erkennen, dass ein Überlauf aufgetreten wäre, wenn der Code dies getan hätte,lhs + rhs
ohne die Summe tatsächlich zu tun.Meiner Meinung nach ist die Verwendung der östlichsten Methode, um mit überlaufempfindlichem C ++ - Code umzugehen
SafeInt<T>
. Dies ist eine plattformübergreifende C ++ - Vorlage, die auf Code Plex gehostet wird und die Sicherheitsgarantien bietet, die Sie hier wünschen.Ich finde es sehr intuitiv zu bedienen, da es viele der gleichen Verwendungsmuster wie normale numerische Operationen bietet und über und unter Flüssen über Ausnahmen ausdrückt.
quelle
Für den Fall gcc können wir ab den Versionshinweisen zu gcc 5.0 sehen, dass es jetzt zusätzlich
__builtin_add_overflow
zur Überprüfung des Überlaufs Folgendes bietet :Zum Beispiel:
__builtin_add_overflow( rhs, lhs, &result )
Aus dem gcc-Dokument können wir die integrierten Funktionen zur Durchführung von Arithmetik mit Überlauf ersehen. Überprüfen Sie Folgendes :
clang bietet auch eine Reihe von eingebauten arithmetischen Einbauten :
In diesem Fall wäre das eingebaute:
__builtin_sadd_overflow( rhs, lhs, &result )
quelle
int result; __builtin_add_overflow(INT_MAX, 1, &result);
nicht explizit sagen , was in gespeichert wirdresult
bei Überlauf und leider ist ruhig auf die Angabe nicht definiertes Verhalten ist nicht auftreten. Sicher war das die Absicht - keine UB. Besser, wenn es das spezifiziert.(unsigned) long long *result
für__builtin_(s/u)addll_overflow
. Sicher sind diese fehlerhaft. Man wundert sich über die Richtigkeit anderer Aspekte. IAC, schön diese zu sehen__builtin_add/sub/mull_overflow()
. Ich hoffe, sie schaffen es eines Tages zur C-Spezifikation.Wenn Sie einen Inline-Assembler verwenden, können Sie das Überlauf-Flag überprüfen . Eine andere Möglichkeit besteht darin, dass Sie einen sicheren Datentyp verwenden können . Ich empfehle, dieses Dokument über Integer Security zu lesen .
quelle
Der schnellstmögliche Weg ist die Verwendung des integrierten GCC:
int add(int lhs, int rhs) { int sum; if (__builtin_add_overflow(lhs, rhs, &sum)) abort(); return sum; }
Auf x86 kompiliert GCC dies in:
Hier wird die integrierte Überlauferkennung des Prozessors verwendet.
Wenn Sie mit der Verwendung von GCC-integrierten Funktionen nicht einverstanden sind, können Sie am schnellsten Bitoperationen für die Vorzeichenbits verwenden. Ein signierter Überlauf tritt zusätzlich auf, wenn:
Das Vorzeichenbit von
~(lhs ^ rhs)
ist aktiviert, wenn die Operanden das gleiche Vorzeichen haben, und das Vorzeichenbit vonlhs ^ sum
ist aktiviert, wenn das Ergebnis ein anderes Vorzeichen als die Operanden hat. Sie können die Addition also in vorzeichenloser Form durchführen, um undefiniertes Verhalten zu vermeiden, und dann das Vorzeichenbit von~(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; }
Dies setzt sich zusammen in:
lea (%rsi,%rdi), %eax xor %edi, %esi not %esi xor %eax, %edi test %edi, %esi js call_abort ret call_abort: call abort
Das ist viel schneller als das Umwandeln in einen 64-Bit-Typ auf einem 32-Bit-Computer (mit gcc):
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
quelle
Möglicherweise haben Sie besseres Glück, wenn Sie in 64-Bit-Ganzzahlen konvertieren und ähnliche Bedingungen wie diese testen. Zum Beispiel:
#include <stdint.h> ... int64_t sum = (int64_t)lhs + (int64_t)rhs; if (sum < INT_MIN || sum > INT_MAX) { // Overflow occurred! } else { return sum; }
Vielleicht möchten Sie sich genauer ansehen, wie die Zeichenerweiterung hier funktioniert, aber ich denke, das ist richtig.
quelle
return (int32_t)(sum & 0xffffffff);
.sum & 0xffffffff
,sum
wird implizit Typ konvertiertunsigned int
(unter der Annahme , 32-Bitint
) , da0xffffffff
Art hatunsigned int
. Dann ist das Ergebnis von bitweise und einunsigned int
, und wennsum
es negativ war, liegt es außerhalb des von unterstützten Wertenbereichsint32_t
. Die Konvertierung in hatint32_t
dann ein implementierungsdefiniertes Verhalten.int
64-Bit- Umgebungen nicht funktioniert .Wie wäre es mit:
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); } }
Ich denke, das sollte für jeden legitimen
INT_MIN
undINT_MAX
(symmetrischen oder nicht) funktionieren ; die Funktion wie gezeigt Clips, aber es sollte offensichtlich sein, wie man andere Verhaltensweisen bekommt).quelle
result = (n1 - INT_MAX)+n2;
könnte überlaufen, wenn n1 klein wäre (sagen wir 0) und n2 negativ wäre.(n1 ^ n2) < 0
aufzuteilen : Beginnen Sie mit einem für , was auf einer Zwei-Komplement-Maschine bedeuten würde, dass die Werte ein entgegengesetztes Vorzeichen haben und direkt hinzugefügt werden können. Wenn die Werte das gleiche Vorzeichen haben, ist der oben angegebene Ansatz sicher. Auf der anderen Seite bin ich neugierig, ob die Autoren des Standards erwartet haben, dass Implementierungen für Hardware mit zwei Komplementen für den stillen Überlauf im Falle eines Überlaufs auf eine Weise über die Schienen springen würden, die keine sofortige Beendigung eines abnormalen Programms erzwingt, sondern verursacht unvorhersehbare Störung anderer Berechnungen.Die naheliegende Lösung besteht darin, in vorzeichenloses Überlaufverhalten zu konvertieren, um das genau definierte vorzeichenlose Überlaufverhalten zu erhalten:
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; }
Dies ersetzt das undefinierte signierte Überlaufverhalten durch die implementierungsdefinierte Konvertierung von Werten außerhalb des Bereichs zwischen signiert und nicht signiert. Sie müssen daher die Dokumentation Ihres Compilers überprüfen, um genau zu wissen, was passieren wird, aber es sollte zumindest genau definiert sein sollte auf jeder Zwei-Komplement-Maschine, die bei Konvertierungen keine Signale auslöst, das Richtige tun. Dies ist so ziemlich jede Maschine und jeder C-Compiler, die in den letzten 20 Jahren gebaut wurden.
quelle
sum
, was eine istint
. Dies führt dazu, dass entweder ein implementierungsdefiniertes Ergebnis oder ein implementierungsdefiniertes Signal ausgelöst wird, wenn der Wert von(unsigned)lhs + (unsigned)rhs
größer als istINT_MAX
.Wenn zwei
long
Werte hinzugefügt werden, kann portabler Code denlong
Wert in niedrige und hoheint
Teile (oder in) aufteilenshort
Teile, fallslong
diese dieselbe Größe haben wieint
):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'
Die Verwendung der Inline-Assembly ist der schnellste Weg, wenn Sie auf eine bestimmte CPU abzielen:
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'
quelle
Ich denke, dass das funktioniert:
int add(int lhs, int rhs) { volatile int sum = lhs + rhs; if (lhs != (sum - rhs) ) { /* overflow */ //errno = ERANGE; abort(); } return sum; }
Die Verwendung von flüchtig hält den Compiler davon ab, den Test zu optimieren, weil er das glaubt
sum
sich dies zwischen der Addition und der Subtraktion geändert hat.Unter Verwendung von gcc 4.4.3 für x86_64 führt die Assembly für diesen Code die Addition, die Subtraktion und den Test durch, obwohl sie alles auf dem Stapel und nicht benötigte Stapeloperationen speichert. Ich habe es sogar versucht
register volatile int sum =
aber die Montage war dieselbe.Für eine Version mit nur
int sum =
(kein flüchtiges oder Register) hat die Funktion den Test nicht durchgeführt und die Addition mit nur einerlea
Anweisung durchgeführt (lea
ist Load Effective Address und wird häufig verwendet, um eine Addition durchzuführen, ohne das Flags-Register zu berühren).Ihre Version ist größerer Code und hat viel mehr Sprünge, aber ich weiß nicht, welcher besser wäre .
quelle
volatile
, um undefiniertes Verhalten zu maskieren. Wenn es "funktioniert", haben Sie immer noch "Glück".volatile
richtig implementiert . Ich habe nur nach einer einfacheren Lösung für ein sehr häufiges Problem bei einer bereits beantworteten Frage gesucht.Für mich wäre die einfachste Überprüfung die Überprüfung der Vorzeichen der Operanden und der Ergebnisse.
Untersuchen wir die Summe: Der Überlauf kann in beide Richtungen + oder - nur auftreten, wenn beide Operanden das gleiche Vorzeichen haben. Und offensichtlich ist der Überlauf, wenn das Vorzeichen des Ergebnisses nicht mit dem Vorzeichen der Operanden übereinstimmt.
Ein Scheck wie dieser reicht also aus:
int a, b, sum; sum = a + b; if (((a ^ ~b) & (a ^ sum)) & 0x80000000) detect_oveflow();
Bearbeiten: Wie Nils vorgeschlagen hat, ist dies die richtige
if
Bedingung:((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)
Und seit wann die Anweisung
add eax, ebx
führt zu undefiniertem Verhalten? In der Referenz zum Intel x86-Befehlssatz gibt es so etwas nicht.
quelle
sum = a + b
kann zu undefiniertem Verhalten führen.(usngined int)
s es viel unlesbarer machen werden. (Sie wissen, Sie lesen es zuerst und versuchen es nur, wenn es Ihnen gefällt).