Verwenden von Ganzzahlen ohne Vorzeichen in C und C ++

23

Ich habe eine sehr einfache Frage, die mich lange Zeit verblüfft. Ich habe es mit Netzwerken und Datenbanken zu tun, daher handelt es sich bei vielen Daten, mit denen ich zu tun habe, um 32-Bit- und 64-Bit-Zähler (ohne Vorzeichen), 32-Bit- und 64-Bit-Identifikations-IDs (auch ohne aussagekräftige Zuordnung für Vorzeichen). Ich beschäftige mich praktisch nie mit realen Wörtern, die als negative Zahl ausgedrückt werden könnten.

Ich und meine Mitarbeiter verwenden routinemäßig vorzeichenlose Typen wie uint32_tund uint64_tfür diese Zwecke und da dies so häufig vorkommt, verwenden wir sie auch für Array-Indizes und andere allgemeine Ganzzahlverwendungen.

Gleichzeitig lese ich verschiedene Codierungsleitfäden (z. B. Google), die von der Verwendung von Ganzzahltypen ohne Vorzeichen abraten, und meines Wissens haben weder Java noch Scala Ganzzahltypen ohne Vorzeichen.

Daher konnte ich nicht herausfinden, was das Richtige ist: Es wäre sehr unpraktisch, signierte Werte in unserer Umgebung zu verwenden. Gleichzeitig sollten Sie in Codierungsanleitungen darauf bestehen, genau dies zu tun.

zzz777
quelle

Antworten:

31

Es gibt zwei Denkschulen, und keine wird jemals zustimmen.

Das erste Argument besagt, dass es einige Konzepte gibt, die von Natur aus nicht mit Vorzeichen versehen sind, wie z. B. Array-Indizes. Es ist nicht sinnvoll, vorzeichenbehaftete Zahlen zu verwenden, da dies zu Fehlern führen kann. Es kann auch unnötige Beschränkungen für Dinge festlegen - ein Array, das vorzeichenbehaftete 32-Bit-Indizes verwendet, kann nur auf 2 Milliarden Einträge zugreifen, während das Umschalten auf vorzeichenlose 32-Bit-Nummern 4 Milliarden Einträge zulässt.

Das zweite Argument besagt, dass in jedem Programm, das vorzeichenlose Zahlen verwendet, früher oder später gemischte vorzeichenlose Arithmetik ausgeführt wird. Dies kann zu seltsamen und unerwarteten Ergebnissen führen: Wenn Sie einen großen Wert ohne Vorzeichen in "Vorzeichen" umwandeln, erhalten Sie eine negative Zahl, und wenn Sie einen negativen Wert in "Vorzeichen" umwandeln, erhalten Sie eine große positive Zahl. Dies kann eine große Fehlerquelle sein.

Simon B
quelle
8
Der Compiler erkennt gemischte Arithmetikprobleme mit und ohne Vorzeichen. Halten Sie Ihren Build einfach warnfrei (mit einer ausreichend hohen Warnstufe). Außerdem intist kürzer zu tippen :)
Rucamzu
7
Geständnis: Ich bin mit der zweiten Denkrichtung befasst, und obwohl ich die Überlegungen für nicht signierte Typen verstehe, intist dies in 99,99% der Fälle mehr als genug für Array-Indizes. Die vorzeichenbehafteten - vorzeichenlosen arithmetischen Probleme sind weitaus häufiger und haben daher Vorrang in Bezug auf das, was zu vermeiden ist. Ja, Compiler warnen Sie davor, aber wie viele Warnungen erhalten Sie, wenn Sie ein umfangreiches Projekt kompilieren? Das Ignorieren von Warnungen ist gefährlich und eine schlechte Praxis, aber in der realen Welt ...
Elias Van Ootegem
11
+1 auf die Antwort. Achtung : Blunt Opinions Ahead : 1: Meine Antwort auf die zweite Denkrichtung lautet: Ich wette, dass jeder, der unerwartete Ergebnisse aus nicht signierten Integraltypen in C erhält, undefiniertes Verhalten (und nicht das rein akademische) in C hat ihre nicht-trivialen C-Programme, die vorzeichenbehaftete Integraltypen verwenden. Wenn Sie nicht gut genug kennen C zu denken , dass unsigned Typen die sind besser diejenigen zu verwenden, rate ich C. Vermeidung 2: Es gibt genau einen richtigen Typ für Array - Indizes und Größen in C, und das ist size_t, es sei denn , ein Spezialfall ist aus gutem grund anders.
mtraceur
5
Sie geraten ohne gemischte Unterschrift in Schwierigkeiten. Berechnen Sie einfach unsigned int minus unsigned int.
gnasher729
4
Sie haben kein Problem mit Ihnen, Simon, nur mit der ersten Denkschule , die argumentiert, dass "es einige Konzepte gibt, die von Natur aus nicht signiert sind - wie Array-Indizes". speziell: "Es gibt genau einen richtigen Typ für Array-Indizes ... in C" Bullshit! . Wir DSPs verwenden die ganze Zeit negative Indizes. insbesondere bei geraden oder ungeraden Symmetrieimpulsantworten, die nicht kausal sind. und für LUT Mathe. Ich bin in der zweiten Schule des Denkens, aber ich denke, dass es nützlich ist, sowohl vorzeichenbehaftete als auch vorzeichenlose ganze Zahlen in C und C ++ zu haben.
Robert Bristow-Johnson
21

Zuallererst ist die Google C ++ - Codierungsrichtlinie nicht sehr gut zu befolgen: Sie meidet Dinge wie Ausnahmen, Boost usw., die Grundvoraussetzungen für modernes C ++ sind. Zweitens bedeutet nur, dass eine bestimmte Richtlinie für Unternehmen X funktioniert, nicht, dass sie für Sie geeignet ist. Ich würde weiterhin unsignierte Typen verwenden, da Sie ein gutes Bedürfnis nach ihnen haben.

Eine intgute Faustregel für C ++ lautet: Bevorzugen Sie, es sei denn, Sie haben einen guten Grund, etwas anderes zu verwenden.

bstamour
quelle
8
Das meine ich überhaupt nicht. Konstruktoren dienen zum Einrichten von Invarianten, und da sie keine Funktionen sind, können sie nicht einfach, return falsewenn diese Invariante nicht eingerichtet ist. Sie können also entweder Dinge trennen und Init-Funktionen für Ihre Objekte verwenden, oder Sie können ein werfen std::runtime_error, das Abwickeln des Stapels zulassen und alle RAII-Objekte automatisch bereinigen, und Sie als Entwickler können die Ausnahme behandeln, für die dies praktisch ist Sie dazu.
bstamour
5
Ich sehe keinen Unterschied in der Art der Anwendung. Jedes Mal, wenn Sie einen Konstruktor für ein Objekt aufrufen, erstellen Sie eine Invariante mit den Parametern. Wenn diese Invariante nicht erfüllt werden kann, müssen Sie einen Fehler melden, da sich Ihr Programm sonst nicht in einem guten Zustand befindet. Da Konstruktoren kein Flag zurückgeben können, ist das Auslösen einer Ausnahme eine natürliche Option. Bitte begründen Sie, warum eine Geschäftsanwendung von einem solchen Codierungsstil nicht profitiert.
bstamour
8
Ich bezweifle sehr, dass die Hälfte aller C ++ - Programmierer nicht in der Lage ist, Ausnahmen richtig zu verwenden. Wenn Sie jedoch der Meinung sind, dass Ihre Mitarbeiter nicht in der Lage sind, modernes C ++ zu schreiben, halten Sie sich auf jeden Fall von modernem C ++ fern.
bstamour
6
@ zzz777 Keine Ausnahmen verwenden? Haben Sie private Konstruktoren, die von öffentlichen Factory-Funktionen verpackt sind, die die Ausnahmen abfangen und was tun - a zurückgeben nullptr? Ein "Standard" -Objekt zurückgeben (was auch immer das bedeuten mag)? Sie haben nichts gelöst - Sie haben das Problem nur unter einem Teppich versteckt und hoffen, dass niemand es herausfindet.
Mael
5
@ zzz777 Wenn du die Box trotzdem zum Absturz bringst, warum ist es dir dann egal, wenn es ausnahmsweise passiert oder signal(6)? Wenn Sie eine Ausnahme verwenden, können die 50% der Entwickler, die wissen, wie sie damit umgehen sollen, guten Code schreiben, und der Rest kann von ihren Kollegen getragen werden.
IllusiveBrian
6

In den anderen Antworten fehlen Beispiele aus der Praxis, daher möchte ich eines hinzufügen. Einer der Gründe, warum ich (persönlich) versuche, nicht signierte Typen zu vermeiden.

Verwenden Sie den Standard size_t als Array-Index:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, ganz normal. Bedenken Sie dann, dass wir uns aus irgendeinem Grund entschieden haben, die Richtung der Schleife zu ändern:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

Und jetzt klappt es nicht. Wenn wir es intals Iterator verwenden würden, gäbe es kein Problem. Ich habe solche Fehler in den letzten zwei Jahren zweimal gesehen. Einmal passierte es in der Produktion und war schwer zu debuggen.

Ein weiterer Grund für mich sind nervige Warnungen, bei denen man jedes Mal so etwas schreibt :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

Das sind Kleinigkeiten, aber sie summieren sich. Ich denke, der Code ist sauberer, wenn überall nur Ganzzahlen mit Vorzeichen verwendet werden.

Edit: Sicher, die Beispiele sehen dumm aus, aber ich habe gesehen, wie Leute diesen Fehler gemacht haben. Wenn es so einfach ist, es zu vermeiden, warum nicht?

Wenn ich den folgenden Code mit VS2015 oder GCC kompiliere, werden keine Warnungen mit Standardwarnungseinstellungen angezeigt (auch mit -Wall für GCC). Sie müssen nach -Wextra fragen, um eine Warnung in GCC zu erhalten. Dies ist einer der Gründe, warum Sie immer mit Wall und Wextra kompilieren sollten (und einen statischen Analysator verwenden sollten), aber in vielen realen Projekten tun die Leute das nicht.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
quelle
Sie können es mit signierten Typen noch mehr falsch machen ... Und Ihr Beispielcode ist so hirntot und eklatant falsch, dass jeder anständige Compiler warnt, wenn Sie nach Warnungen fragen.
Deduplizierer
1
Ich habe in der Vergangenheit auf solche Schrecken zurückgegriffen for (size_t i = n - 1; i < n; --i), damit es richtig funktioniert.
Simon B
2
Apropos For-Loops mit size_tfor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
Inverse
2
@rwong Omg, das ist hässlich. Warum nicht einfach benutzen int? :)
Aleksei Petrenko
1
@AlexeyPetrenko - Beachten Sie, dass weder die aktuellen C- noch die C ++ - Standards garantieren, dass diese intgroß genug sind, um alle gültigen Werte von aufzunehmen size_t. Insbesondere intkönnen Nummern nur bis zu 2 ^ 15-1 zugelassen werden, und dies gilt normalerweise für Systeme mit einer Speicherzuweisungsgrenze von 2 ^ 16 (oder in bestimmten Fällen sogar höher). longMöglicherweise ist dies eine sicherere Wette, es ist jedoch immer noch nicht garantiert , dass sie funktioniert. Nur size_tfunktioniert garantiert auf allen Plattformen und in allen Fällen.
Jules
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Das Problem hierbei ist, dass Sie die Schleife auf eine ungewöhnliche Art und Weise geschrieben haben, was zu einem fehlerhaften Verhalten führte. Die Konstruktion der Schleife ist so, als würde sie Anfängern für vorzeichenbehaftete Typen beigebracht (was in Ordnung und korrekt ist), passt aber einfach nicht für vorzeichenlose Werte. Dies kann jedoch nicht als Gegenargument gegen die Verwendung von vorzeichenlosen Typen dienen. Hier geht es einfach darum, die richtige Schleife zu finden. Und dies kann leicht behoben werden, um für nicht signierte Typen zuverlässig zu funktionieren:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Diese Änderung kehrt einfach die Reihenfolge des Vergleichs und der Dekrementierungsoperation um und ist meiner Meinung nach der effektivste, störungsfreieste, sauberste und kürzeste Weg, um vorzeichenlose Zähler in Rückwärtsschleifen zu behandeln. Sie würden genau dasselbe (intuitiv) tun, wenn Sie eine while-Schleife verwenden:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Es kann kein Unterlauf auftreten, der Fall eines leeren Behälters wird implizit abgedeckt, wie bei der bekannten Variante für die vorzeichenbehaftete Zählerschleife, und der Körper der Schleife kann im Vergleich zu einem vorzeichenbehafteten Zähler oder einer Vorwärtsschleife unverändert bleiben. Man muss sich nur an das zunächst etwas seltsam aussehende Loop-Konstrukt gewöhnen. Aber nachdem Sie das ein Dutzend Mal gesehen haben, ist nichts mehr unverständlich.

Ich hätte das Glück, wenn Anfängerkurse nicht nur die richtige Schleife für signierte, sondern auch für nicht signierte Typen zeigen würden. Dies würde ein paar Fehler vermeiden, die IMHO den unwissenden Entwicklern vorgeworfen werden sollten, anstatt dem nicht signierten Typ die Schuld zu geben.

HTH

Don Pedro
quelle
1

Ganzzahlen ohne Vorzeichen gibt es nicht ohne Grund.

Denken Sie beispielsweise daran, Daten als einzelne Bytes zu übergeben, z. B. in einem Netzwerkpaket oder in einem Dateipuffer. Es kann gelegentlich vorkommen, dass Sie auf Bestien wie 24-Bit-Ganzzahlen stoßen. Einfache Bitverschiebung von drei 8-Bit-Ganzzahlen ohne Vorzeichen, bei 8-Bit-Ganzzahlen mit Vorzeichen nicht so einfach.

Oder denken Sie über Algorithmen nach, die Nachschlagetabellen für Zeichen verwenden. Wenn ein Zeichen eine 8-Bit-Ganzzahl ohne Vorzeichen ist, können Sie eine Nachschlagetabelle nach einem Zeichenwert indizieren. Was tun Sie jedoch, wenn die Programmiersprache keine Ganzzahlen ohne Vorzeichen unterstützt? Sie hätten negative Indizes zu einem Array. Nun, ich denke, Sie könnten so etwas gebrauchen, charval + 128aber das ist einfach hässlich.

Tatsächlich verwenden viele Dateiformate Ganzzahlen ohne Vorzeichen. Wenn die Programmiersprache der Anwendung Ganzzahlen ohne Vorzeichen nicht unterstützt, kann dies ein Problem sein.

Dann betrachten Sie die TCP-Sequenznummern. Wenn Sie einen TCP-Verarbeitungscode schreiben, sollten Sie auf jeden Fall Ganzzahlen ohne Vorzeichen verwenden.

Manchmal ist Effizienz so wichtig, dass Sie wirklich ein bisschen vorzeichenlose Ganzzahlen benötigen. Betrachten Sie beispielsweise IoT-Geräte, die in Millionen ausgeliefert werden. Viele Programmierressourcen können dann gerechtfertigt für Mikrooptimierungen ausgegeben werden.

Ich würde argumentieren, dass die Rechtfertigung, vorzeichenlose Integer-Typen (Mischzeichen-Arithmetik, Mischzeichen-Vergleiche) zu vermeiden, von einem Compiler mit richtigen Warnungen überwunden werden kann. Solche Warnungen sind normalerweise nicht standardmäßig aktiviert, sondern werden z. B. -Wextraoder separat -Wsign-compare(automatisch aktiviert in C von -Wextra, obwohl ich glaube, dass es in C ++ nicht automatisch aktiviert ist) und angezeigt -Wsign-conversion.

Verwenden Sie im Zweifelsfall dennoch einen signierten Typ. Oft ist es eine Wahl, die gut funktioniert. Und aktivieren Sie diese Compiler-Warnungen!

juhist
quelle
0

Es gibt viele Fälle, in denen Ganzzahlen eigentlich keine Zahlen darstellen, aber zum Beispiel eine Bitmaske, eine ID usw. Grundsätzlich hat das Hinzufügen von 1 zu einer Ganzzahl kein aussagekräftiges Ergebnis. In diesen Fällen verwenden Sie unsigned.

Es gibt viele Fälle, in denen Sie mit ganzen Zahlen rechnen. Verwenden Sie in diesen Fällen vorzeichenbehaftete Ganzzahlen, um Fehlverhalten um Null herum zu vermeiden. Sehen Sie sich viele Beispiele für Schleifen an, bei denen das Ausführen einer Schleife bis auf Null entweder sehr unintuitiven Code verwendet oder aufgrund der Verwendung von Zahlen ohne Vorzeichen unterbrochen wird. Es gibt das Argument "aber Indizes sind nie negativ" - sicher, aber Unterschiede von Indizes zum Beispiel sind negativ.

In dem sehr seltenen Fall, dass Indizes 2 ^ 31, aber nicht 2 ^ 32 überschreiten, verwenden Sie keine vorzeichenlosen Ganzzahlen, sondern 64-Bit-Ganzzahlen.

Zum Schluss noch eine nette Falle: In einer Schleife "für (i = 0; i <n; ++ i) a [i] ..." kann der Compiler nicht optimieren, wenn i 32 Bit vorzeichenlos ist und der Speicher 32 Bit-Adressen überschreitet den Zugriff auf a [i] durch Inkrementieren eines Zeigers, da bei i = 2 ^ 32 - 1 ein Umlauf erfolgt. Auch wenn n nie so groß wird. Die Verwendung von Ganzzahlen mit Vorzeichen vermeidet dies.

gnasher729
quelle
-5

Endlich habe ich hier eine wirklich gute Antwort gefunden: "Secure Programming Cookbook" von J.Viega und M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Sicherheitsprobleme mit signierten Ganzzahlen:

  1. Wenn für die Funktion ein positiver Parameter erforderlich ist, kann die Überprüfung des unteren Bereichs leicht vergessen werden.
  2. Unintuitives Bitmuster aus Konvertierungen negativer ganzzahliger Größen.
  3. Unintuitives Bitmuster, das durch die Rechtsschiebeoperation einer negativen ganzen Zahl erzeugt wird.

Es gibt Probleme mit signierten <-> nicht signierten Conversions, daher ist es nicht ratsam, mix zu verwenden.

zzz777
quelle
1
Warum ist es eine gute Antwort? Was ist Rezept 3.5? Was sagt es über Integer Overflow usw. aus?
Baldrickk
In meiner praktischen Erfahrung ist es ein sehr gutes Buch mit wertvollen Ratschlägen in allen Aspekten, die ich ausprobiert habe, und es ist ziemlich fest in dieser Empfehlung. Im Vergleich dazu scheinen die Gefahren von Integer-Überläufen auf Arrays, die länger als 4G sind, ziemlich gering zu sein. Wenn ich mit so großen Arrays umgehen muss, wird mein Programm sehr fein abgestimmt, um Leistungseinbußen zu vermeiden.
zzz777
1
Es geht nicht darum, ob das Buch gut ist. Ihre Antwort liefert keine Rechtfertigung für die Verwendung des Rezeptes, und nicht jeder wird eine Kopie des Buches haben, um es nachzuschlagen. Schauen Sie sich die Beispiele für das Schreiben einer guten Antwort an
Baldrickk
Zu Ihrer
Information habe