size_t oder int für Dimensionen, Index usw

15

In C ++ wird size_t(oder genauer gesagt, T::size_typewas "normalerweise" ist size_t, dh ein unsignedTyp) als Rückgabewert für size(), das Argument für operator[]usw. verwendet (siehe std::vectoret al.).

Andererseits verwenden .NET-Sprachen int(und optional long) für denselben Zweck. Tatsächlich sind CLS-kompatible Sprachen nicht erforderlich, um nicht signierte Typen zu unterstützen .

Angesichts der Tatsache, dass .NET neuer als C ++ ist, gibt es Hinweise darauf, dass es möglicherweise Probleme bei der Verwendung gibt, unsigned intauch für Dinge, die "unmöglich" negativ sein können, wie z. B. ein Array-Index oder eine Array-Länge. Ist der C ++ - Ansatz "historisches Artefakt" für die Abwärtskompatibilität? Oder gibt es echte und signifikante Designkompromisse zwischen den beiden Ansätzen?

Warum ist das wichtig? Nun ... was soll ich für eine neue mehrdimensionale Klasse in C ++ verwenden? size_toder int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
Ðаn
quelle
6
Bemerkenswert: In .NET Framework -1wird an mehreren Stellen von Funktionen zurückgegeben, die einen Index zurückgeben, um "nicht gefunden" oder "außerhalb des gültigen Bereichs" anzuzeigen. Es wird auch von Compare()Funktionen (Implementierung IComparable) zurückgegeben. Ein 32-Bit-Int wird als Go-to-Type für eine allgemeine Zahl betrachtet. Ich hoffe, dass dies offensichtliche Gründe sind.
Robert Harvey

Antworten:

9

Angesichts der Tatsache, dass .NET neuer als C ++ ist, kann die Verwendung von Int ohne Vorzeichen auch für Dinge problematisch sein, die "unmöglich" negativ sein können, wie z. B. ein Array-Index oder eine Array-Länge.

Ja. Für bestimmte Arten von Anwendungen, z. B. Bildverarbeitung oder Array-Verarbeitung, ist es häufig erforderlich, auf Elemente zuzugreifen, die sich auf die aktuelle Position beziehen:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

In diesen Arten von Anwendungen können Sie keine Bereichsprüfung mit Ganzzahlen ohne Vorzeichen durchführen, ohne sorgfältig zu überlegen:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Stattdessen müssen Sie Ihren Range Check-Ausdruck neu anordnen . Das ist der Hauptunterschied. Programmierer müssen sich auch an die ganzzahligen Konvertierungsregeln erinnern. Lesen Sie im Zweifelsfall erneut http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

Viele Anwendungen müssen keine sehr großen Array-Indizes verwenden, aber sie müssen Bereichsprüfungen durchführen. Darüber hinaus sind viele Programmierer nicht dazu ausgebildet, diese Ausdrucksumordnungsturnen zu machen. Eine einzelne verpasste Gelegenheit öffnet die Tür zu einem Exploit.

C # wurde in der Tat für Anwendungen entwickelt, die nicht mehr als 2 ^ 31 Elemente pro Array benötigen. Beispielsweise muss eine Tabellenkalkulationsanwendung nicht mit so vielen Zeilen, Spalten oder Zellen umgehen. C # behandelt die Obergrenze, indem optional eine aktivierte Arithmetik für einen Codeblock mit einem Schlüsselwort aktiviert wird, ohne die Compileroptionen zu beeinträchtigen. Aus diesem Grund bevorzugt C # die Verwendung von Ganzzahlen mit Vorzeichen. Wenn diese Entscheidungen insgesamt betrachtet werden, ist dies sinnvoll.

C ++ ist einfach anders und es ist schwieriger, richtigen Code zu erhalten.

In Bezug auf die praktische Bedeutung, die vorzeichenbehaftete Arithmetik zuzulassen, um eine potenzielle Verletzung des "Grundsatzes des geringsten Erstaunens" zu beseitigen, ist OpenCV ein typisches Beispiel, das eine vorzeichenbehaftete 32-Bit-Ganzzahl für den Matrixelementindex, die Arraygröße, die Pixelkanalanzahl usw. verwendet Die Verarbeitung ist ein Beispiel für eine Programmierdomäne, die den relativen Array-Index stark verwendet. Ein vorzeichenloser ganzzahliger Unterlauf (negatives Ergebnis umbrochen) erschwert die Implementierung des Algorithmus erheblich.

rwong
quelle
Das ist genau meine Situation; danke für die konkreten beispiele. (Ja, ich weiß das, aber es kann nützlich sein, "höhere Autoritäten" zu zitieren.)
14.
1
@Dan: Wenn du etwas zitieren musst, wäre dieser Beitrag besser.
Rwong
1
@Dan: John Regehr untersucht dieses Problem aktiv in Programmiersprachen. Siehe blog.regehr.org/archives/1401
rwong
Es gibt gegensätzliche Meinungen: gustedt.wordpress.com/2013/07/15/…
rwong
14

Diese Antwort hängt wirklich davon ab, wer Ihren Code verwenden wird und welche Standards er sehen möchte.

size_t ist eine Ganzzahl mit einem Zweck:

Der Typ size_tist ein implementierungsdefinierter Integer-Typ ohne Vorzeichen, der groß genug ist, um die Größe eines Objekts in Byte zu enthalten. (C ++ 11 Spezifikation 18.2.6)

Wenn Sie also mit der Größe von Objekten in Bytes arbeiten möchten, sollten Sie verwenden size_t. In vielen Fällen verwenden Sie diese Dimensionen / Indizes nicht zum Zählen von Bytes, aber die meisten Entwickler verwenden sie size_taus Konsistenzgründen.

Beachten Sie, dass Sie immer verwenden sollten , size_twenn Ihre Klasse das Erscheinungsbild einer STL-Klasse haben soll. Alle STL-Klassen in der Spezifikation verwenden size_t. Es ist gültig, dass der Compiler typedef size_tist unsigned int, und es ist auch gültig, dass er typedef ist unsigned long. Wenn Sie intoder longdirekt verwenden, werden Sie irgendwann auf Compiler stoßen, bei denen eine Person, die glaubt, Ihre Klasse habe den STL-Stil befolgt, gefangen wird, weil Sie den Standard nicht befolgt haben.

Für die Verwendung signierter Typen gibt es einige Vorteile:

  • Kürzere Namen - es ist wirklich einfach für die Leute zu tippen int, aber viel schwieriger, den Code zu überspielen unsigned int.
  • Eine Ganzzahl für jede Größe - Es gibt nur eine CLS-kompatible Ganzzahl mit 32 Bit, nämlich Int32. In C ++ gibt es zwei ( int32_tund uint32_t). Dies kann die API-Interoperabilität vereinfachen

Der große Nachteil signierter Typen liegt auf der Hand: Sie verlieren die Hälfte Ihrer Domain. Eine vorzeichenbehaftete Nummer kann nicht so hoch sein wie eine vorzeichenlose Nummer. Als C / C ++ auf den Markt kam, war dies sehr wichtig. Man musste in der Lage sein, die volle Leistungsfähigkeit des Prozessors zu erreichen, und um dies zu erreichen, musste man vorzeichenlose Zahlen verwenden.

Für die Arten von Anwendungen, auf die .NET abzielt, bestand nicht so stark Bedarf an einem nicht signierten Volldomänenindex. Viele der Zwecke für solche Nummern sind in einer verwalteten Sprache einfach ungültig (man denke an Memory Pooling). Als .NET herauskam, waren 64-Bit-Computer eindeutig die Zukunft. Wir sind noch weit davon entfernt, den vollen Bereich einer 64-Bit-Ganzzahl zu benötigen. Daher ist es nicht mehr so ​​schmerzhaft, ein Bit zu opfern wie zuvor. Wenn Sie wirklich 4 Milliarden Indizes benötigen, wechseln Sie einfach zur Verwendung von 64-Bit-Ganzzahlen. Im schlimmsten Fall läuft es auf einer 32-Bit-Maschine und ist etwas langsam.

Ich sehe den Handel als einen Zweck an. Wenn Sie über genügend Rechenleistung verfügen, um einen Teil Ihres Indextyps zu verschwenden, den Sie niemals verwenden werden, ist es praktisch, ihn nur zu tippen intoder zu verlassen long. Wenn Sie feststellen, dass Sie das letzte Stück wirklich wollten, sollten Sie wahrscheinlich auf die Signatur Ihrer Nummern achten.

Cort Ammon - Setzen Sie Monica wieder ein
quelle
Nehmen wir an, die Implementierung von size()war return bar_ * baz_;; schafft das nicht jetzt ein potenzielles Problem mit Integer-Überlauf (Wrap-Around), das ich nicht hätte, wenn ich es nicht verwendet hätte size_t?
13.
5
@Dan Sie können Fälle wie diesen erstellen, in denen es darauf ankommt, nicht signierte Ints zu haben. In diesen Fällen empfiehlt es sich, die vollständigen Sprachfunktionen zu verwenden, um das Problem zu beheben. Ich muss jedoch sagen, dass es eine interessante Konstruktion wäre, eine Klasse zu haben, in der eine bar_ * baz_Ganzzahl mit Vorzeichen überlaufen kann, aber keine Ganzzahl ohne Vorzeichen. Wenn wir uns auf C ++ beschränken, ist anzumerken, dass ein vorzeichenloser Überlauf in der Spezifikation definiert ist, ein vorzeichenbehafteter Überlauf jedoch ein undefiniertes Verhalten ist. Wenn also die Modulo-Arithmetik vorzeichenloser Ganzzahlen wünschenswert ist, verwenden Sie sie auf jeden Fall, da sie tatsächlich definiert sind!
Cort Ammon - Reinstate Monica
1
@Dan - wenn die vorzeichenbehaftete Multiplikation size()übergelaufen ist, bist du in der Sprache UB land. (und im Modus, siehe weiter :) Wenn dann mit nur einem winzigen bisschen mehr die vorzeichenlose Multiplikation übergelaufen ist, befinden Sie sich im User-Code-Bug-Land - Sie würden eine falsche Größe zurückgeben. Ich glaube nicht, dass unsignierte Kunden hier viel kaufen. fwrapv
Martin Ba
4

Ich denke, die Antwort von rwong oben hebt die Probleme bereits hervorragend hervor.

Ich werde meine 002 hinzufügen:

  • size_t, das heißt, eine Größe, die ...

    kann die maximale Größe eines theoretisch möglichen Objekts eines beliebigen Typs (einschließlich Array) speichern.

    ... wird nur für Bereichsindizes benötigt sizeof(type)==1, wenn es sich um byte ( char) -Typen handelt. (Wir stellen jedoch fest, dass es kleiner sein kann als ein ptr-Typ :

  • Als solche xxx::size_typekonnte in 99,9% der Fälle verwendet werden , auch wenn es ein signiertes Größe Typ waren. (vergleichen ssize_t)
  • Die Tatsache, dass std::vectorund Freunde size_teinen nicht signierten Typ für die Größe und Indexierung gewählt haben, wird von einigen als Designfehler angesehen. Ich stimme zu. (Im Ernst, nehmen Sie sich 5 Minuten Zeit und schauen Sie sich das Blitzgespräch CppCon 2016 an: Jon Kalb "unsigniert: Ein Leitfaden für besseren Code" .)
  • Wenn Sie heute eine C ++ - API entwerfen, müssen Sie Folgendes beachten: Verwenden Sie size_tdiese Option , um mit der Standardbibliothek konsistent zu sein, oder verwenden Sie sie ( signiert ) intptr_toder ssize_tfür einfache und weniger fehleranfällige Indizierungsberechnungen.
  • Verwenden Sie nicht int32 oder int64 - verwenden intptr_tSie, wenn Sie signiert werden möchten und die Größe von Maschinenwörtern möchten oder verwenden Sie ssize_t.

Um die Frage direkt zu beantworten, handelt es sich nicht nur um ein "historisches Artefakt", da die theoretische Frage , ob mehr als die Hälfte des ("Indizierungs-" oder) Adressraums adressiert werden muss , auf eine Art und Weise in einer Sprache auf niedriger Ebene adressiert werden muss C ++.

Im Nachhinein denke ich persönlich , dass es sich um einen Konstruktionsfehler handelt, den die Standardbibliothek size_tüberall ohne Vorzeichen verwendet, auch wenn sie keine unformatierte Speichergröße darstellt, sondern eine Kapazität für eingegebene Daten, wie für die Sammlungen:

  • gegebene C ++ s ganzzahlige Förderungsregeln ->
  • vorzeichenlose Typen sind einfach keine guten Kandidaten für "semantische" Typen für eine Größe, die semantisch nicht vorzeichenbehaftet ist.

Ich werde Jons Rat hier wiederholen :

  • Wählen Sie Typen für die Operationen aus, die sie unterstützen (nicht den Wertebereich). (* 1)
  • Verwenden Sie in Ihrer API keine vorzeichenlosen Typen. Dies verbirgt Fehler ohne Vorteil.
  • Verwenden Sie nicht "unsigned" für Mengen. (* 2)

(* 1) dh vorzeichenlose == Bitmaske, rechnen Sie niemals damit (hier trifft die erste Ausnahme zu - Sie benötigen möglicherweise einen Zähler, der umbrochen wird - dies muss ein vorzeichenloser Typ sein.)

(* 2) Mengen, die etwas bedeuten, worauf Sie zählen und / oder rechnen.

Martin Ba
quelle
Was meinst du mit "full avilable flat memory"? Möchten Sie sicher nicht, dass stattdessen ssize_tder signierte Pendant definiert wird, der einen beliebigen (Nicht-Mitglieder-) Zeiger speichern kann und daher möglicherweise größer ist? size_tintptr_t
Deduplizierer
@ Deduplicator - Nun, ich glaube, ich habe die size_tDefinition ein wenig durcheinander gebracht. Siehe size_t vs. intptr und en.cppreference.com/w/cpp/types/size_t Heute etwas Neues gelernt. :-) Ich denke, der Rest der Argumente steht, ich werde sehen, ob ich die verwendeten Typen reparieren kann.
Martin Ba
0

Ich füge das nur aus Performancegründen hinzu, die ich normalerweise mit size_t benutze sicherzustellen, dass Fehlberechnungen einen Unterlauf verursachen, was bedeutet, dass beide Bereichsprüfungen (unter Null und über Größe ()) auf eins reduziert werden können:

mit signiertem int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

mit unsigned int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
Asger
quelle
1
Sie wollen das wirklich noch gründlicher erklären.
Martin Ba
Um die Antwort nützlicher zu machen, können Sie vielleicht beschreiben, wie die Grenzen des Integer-Arrays oder der Offset-Vergleich (mit und ohne Vorzeichen) im Maschinencode verschiedener Compiler-Hersteller aussehen. Es gibt viele Online-C ++ - Compiler und -Dissassembly-Sites, die den entsprechenden kompilierten Maschinencode für den angegebenen C ++ - Code und die Compiler-Flags anzeigen können.
Rwong
Ich habe versucht, dies noch etwas zu erklären.
Asger