wake-up-neo.com

Warum gibt es in C++ keinen Endianness-Modifikator wie für signierte?

(Ich denke, diese Frage könnte sich auf viele typisierte Sprachen beziehen, aber ich habe C++ als Beispiel verwendet.)

Warum gibt es keine Möglichkeit, einfach zu schreiben:

struct foo {
    little int x;   // little-endian
    big long int y; // big-endian
    short z;        // native endianness
};

endianness für bestimmte Elemente, Variablen und Parameter festlegen?

Vergleich zur Signatur

Ich verstehe, dass der Typ einer Variablen nicht nur bestimmt, wie viele Bytes zum Speichern eines Werts verwendet werden, sondern auch, wie diese Bytes bei Berechnungen interpretiert werden.

Zum Beispiel weisen diese beiden Deklarationen jeweils ein Byte zu, und für beide Bytes ist jede mögliche 8-Bit-Sequenz ein gültiger Wert:

signed char s;
unsigned char u;

die gleiche binäre Sequenz kann jedoch unterschiedlich interpretiert werden, z. 11111111 würde -1 bedeuten, wenn er s zugewiesen wird, aber 255, wenn er u zugewiesen wird. Wenn vorzeichenbehaftete und vorzeichenlose Variablen in dieselbe Berechnung einbezogen werden, sorgt der Compiler (meistens) für die richtigen Konvertierungen.

Endianness ist meines Erachtens nur eine Variation desselben Prinzips: eine andere Interpretation eines binären Musters basierend auf Kompilierzeitinformationen über den Speicher, in dem es gespeichert wird.

Es liegt nahe, diese Funktion in einer typisierten Sprache zu haben, die eine Programmierung auf niedriger Ebene ermöglicht. Dies ist jedoch kein Teil von C, C++ oder einer anderen Sprache, die ich kenne, und ich habe darüber im Internet keine Diskussion gefunden.

Aktualisieren

Ich werde versuchen, einige Mitbringsel aus den vielen Kommentaren zusammenzufassen, die ich in der ersten Stunde bekam, nachdem ich gefragt hatte:

  1. signiert ist streng binär (entweder signiert oder unsigniert) und wird im Gegensatz zu endianness immer zwei bekannte Varianten (big und little), aber auch weniger bekannte Varianten wie mixed/middle endian haben. In Zukunft könnten neue Varianten erfunden werden.
  2. endianness ist wichtig, wenn auf Byte-Werte auf Werte von mehreren Bytes zugegriffen wird. Es gibt viele Aspekte, die über das Endianness-Prinzip hinausgehen und das Speicherlayout von Multibyte-Strukturen beeinflussen. Daher wird von dieser Art des Zugriffs meist abgeraten.
  3. C++ zielt darauf ab, eine abstract machine anzugreifen und die Anzahl der Annahmen über die Implementierung zu minimieren. Diese abstrakte Maschine hat keine any Endianität.

Jetzt ist mir auch klar, dass Signatur und Endianität keine perfekte Analogie sind, weil:

  • endianness definiert nur wie etwas wird als binäre Sequenz dargestellt, aber jetzt was kann dargestellt. Sowohl big int als auch little int hätten exakt denselben Wertebereich.
  • signiert definiert wie Bits und Istwerte sind einander zugeordnet, beeinflussen jedoch auch was kann, z. -3 kann nicht durch einen unsigned char dargestellt werden und (vorausgesetzt char hat 8 Bits) 130 kann nicht durch einen signed char dargestellt werden.

Eine Änderung des Endwerts einiger Variablen würde also niemals das Verhalten des Programms ändern (außer beim byteweisen Zugriff), wohingegen eine Änderung der Signiertheit dies normalerweise tun würde.

57
Lena Schimmel

Was sagt der Standard?

[intro.abstract]/1 :

Die semantischen Beschreibungen in diesem Dokument definieren eine parametrisierte nichtdeterministische abstrakte Maschine . Dieses Dokument legt keine Anforderungen an die Struktur der konformen Implementierungen fest. Insbesondere brauchen sie die Struktur der abstrakten Maschine nicht zu kopieren oder zu emulieren. Vielmehr sind konforme Implementierungen erforderlich, um (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren, wie nachstehend erläutert wird.

C++ konnte keinen Endianness-Qualifier definieren, da er kein Konzept für Endianness hat.

Diskussion

Über den Unterschied zwischen Unterscheidbarkeit und Endlichkeit schrieb OP

Endianness ist meines Erachtens nur eine Variation desselben Prinzips [(Signness)]: eine andere Interpretation eines binären Musters basierend auf Kompilierzeitinformationen über den Speicher, in dem es gespeichert wird.

Ich würde argumentieren, dass Signness sowohl einen semantischen als auch einen repräsentativen Aspekt hat1. Was [intro.abstract]/1 bedeutet, ist, dass C++ sich nur für semantic interessiert und niemals die Art und Weise berücksichtigt, wie eine signierte Zahl im Speicher dargestellt werden sollte2. Tatsächlich erscheint "sign bit" nur einmal in den C++ - Spezifikationen und bezieht sich auf einen implementierungsdefinierten Wert.
Andererseits hat Endianness nur einen repräsentativen Aspekt: ​​Endianness vermittelt keine Bedeutung.

Bei C++ 20 erscheint std::endian . Es ist immer noch implementierungsdefiniert, aber lassen Sie uns den Endian des Hosts testen, ohne von alten Tricks abhängig von undefiniertem Verhalten abhängig zu sein.


1) Semantischer Aspekt: ​​Eine vorzeichenbehaftete Ganzzahl kann Werte unter Null darstellen. Repräsentativer Aspekt: ​​Man muss beispielsweise etwas reservieren, um das positive/negative Vorzeichen zu vermitteln.
2) In derselben Weise beschreibt C++ nie, wie eine Fließkommazahl dargestellt werden sollte. IEEE-754 wird häufig verwendet. Dies ist jedoch eine Wahl, die von der Implementierung getroffen wird, in jedem Fall jedoch durch den Standard: [basic.fundamental]/8" Die Wertdarstellung von Gleitkommatypen ist implementierungsdefiniert ".

52
YSC

Nehmen wir zusätzlich zur Antwort von YSC Ihren Beispielcode und überlegen Sie, was er erreichen könnte

struct foo {
    little int x;   // little-endian
    big long int y; // big-endian
    short z;        // native endianness
};

Sie könnten hoffen, dass dies genau das Layout für den architekturunabhängigen Datenaustausch (Datei, Netzwerk, was auch immer) angibt.

Dies kann jedoch unmöglich funktionieren, da einige Dinge noch nicht spezifiziert sind:

  • datentypgröße: Sie müssen little int32_t, big int64_t und int16_t verwenden, wenn Sie dies möchten
  • auffüllen und Ausrichten, die nicht streng innerhalb der Sprache gesteuert werden können: Verwenden Sie #pragma oder __attribute__((packed)) oder eine andere compilerspezifische Erweiterung
  • aktuelles Format (1s- oder 2s-Komplement mit Vorzeichen, Fließkomma-Layout, Trap-Darstellungen)

Alternativ möchten Sie vielleicht nur die Endianness einer bestimmten Hardware widerspiegeln - big und little decken jedoch nicht alle Möglichkeiten ab (nur die zwei häufigsten).

Der Vorschlag ist also unvollständig (er unterscheidet nicht alle vernünftigen Bytereihenfolgen), ist unwirksam (er erreicht nicht das, was er beabsichtigt) und weist zusätzliche Nachteile auf:

  • Performance

    Durch das Ändern der Endianness einer Variablen aus der nativen Byte-Reihenfolge sollten entweder Arithmetik, Vergleiche usw. deaktiviert werden (da die Hardware sie bei diesem Typ nicht korrekt ausführen kann), oder sie muss mehr Code injizieren, wodurch nativ geordnete temporäre Variablen für die Bearbeitung entstehen .

    Das Argument ist hier nicht, dass die manuelle Konvertierung in/aus der systemeigenen Byte-Reihenfolge schneller ist. Die explizite Steuerung macht es einfacher, die Anzahl der unnötigen Konvertierungen zu minimieren, und es ist viel einfacher zu begründen, wie sich Code verhält wenn die Konvertierungen implizit sind.

  • Komplexität

    Alles, was überladen oder auf Ganzzahltypen spezialisiert ist, benötigt jetzt doppelt so viele Versionen, um mit dem seltenen Ereignis fertig zu werden, dass ein nicht-nativer Endianness-Wert übergeben wird. Auch wenn es sich nur um einen Forwarding-Wrapper handelt (mit ein paar Casts, die in/aus der ursprünglichen Reihenfolge übersetzt werden sollen), ist dennoch viel Code für einen erkennbaren Vorteil.

Das letzte Argument gegen das Ändern der Sprache zur Unterstützung ist, dass Sie dies leicht im Code tun können. Das Ändern der Sprachsyntax ist eine große Sache und bietet gegenüber einem Typ-Wrapper keinen offensichtlichen Vorteil:

// store T with reversed byte order
template <typename T>
class Reversed {
    T val_;
    static T reverse(T); // platform-specific implementation
public:
    explicit Reversed(T t) : val_(reverse(t)) {}
    Reversed(Reversed const &other) : val_(other.val_) {}
    // assignment, move, arithmetic, comparison etc. etc.
    operator T () const { return reverse(val_); }
};
37
Useless

Ganze Zahlen (als mathematisches Konzept) haben das Konzept von positiven und negativen Zahlen. Dieses abstrakte Zeichenkonzept weist eine Reihe verschiedener Implementierungen in Hardware auf.

Endianness ist kein mathematisches Konzept. Little-Endian ist ein Hardware-Implementierungstrick, um die Leistung von Multi-Byte-Zweierkomplement-Ganzzahlarithmetik auf einem Mikroprozessor mit 16- oder 32-Bit-Registern und einem 8-Bit-Speicherbus zu verbessern. Bei seiner Erstellung musste der Begriff Big-Endian verwendet werden, um alles andere zu beschreiben, das in Registern und im Speicher die gleiche Byte-Reihenfolge hatte.

Die abstrakte C-Maschine umfasst das Konzept von vorzeichenbehafteten und vorzeichenlosen Ganzzahlen ohne Details - ohne Zwei-Komplement-Arithmetik, 8-Bit-Bytes oder das Speichern einer binären Zahl im Speicher.

PS: Ich bin damit einverstanden, dass die Kompatibilität von Binärdaten im Netz oder im Speicher/Speicher eine PIA ist.

3
Chad Farmer

Das ist eine gute Frage und ich habe oft gedacht, so etwas wäre nützlich. Sie müssen jedoch bedenken, dass C die Plattformunabhängigkeit und Endianness anstrebt, wenn eine solche Struktur in ein zugrunde liegendes Speicherlayout umgewandelt wird. Diese Konvertierung kann passieren, wenn Sie beispielsweise einen uint8_t-Puffer in ein int umwandeln. Während ein Endianness-Modifikator ordentlich aussieht, muss der Programmierer noch andere Plattformunterschiede berücksichtigen, wie z. B. int-Größen und Strukturausrichtung und -packung. Für die defensive Programmierung, wenn Sie die Kontrolle über die Darstellung einiger Variablen oder Strukturen in einem Speicherpuffer erhalten möchten Es ist am besten, explizite Konvertierungsfunktionen zu codieren und den Compiler-Optimierer dann den effizientesten Code für jede unterstützte Plattform generieren zu lassen.

2
D Dowling

Endianness ist nicht inhärent Teil eines Datentyps, sondern des Speicherlayouts.

Als solches wäre es nicht wirklich mit vorzeichenbehaftet/vorzeichenlos vergleichbar, sondern eher wie Bitfeldbreiten in Strukturen. Ähnlich wie diese können sie zur Definition binärer APIs verwendet werden.

Also hättest du sowas

int ip : big 32;

dies würde sowohl das Speicherlayout als auch die Ganzzahlgröße definieren und es dem Compiler überlassen, das Feld optimal an den Zugriff des Feldes anzupassen. Mir ist nicht klar, wie die erlaubten Deklarationen sein sollen.

2
user9026597

Kurze Antwort: Wenn es nicht möglich sein sollte, Objekte in arithmetischen Ausdrücken (ohne überladene Operatoren) mit ints zu verwenden, sollten diese Objekte keine Ganzzahltypen sein. Und es hat keinen Sinn, Additionen und Multiplikationen von Big-Endian- und Little-Endian-Ints in demselben Ausdruck zuzulassen.

Längere Antwort:

Wie bereits erwähnt, ist Endianness prozessorspezifisch. Was wirklich bedeutet, dass Zahlen so dargestellt werden, wenn sie als Zahlen in der Maschinensprache verwendet werden (als Adressen und als Operanden/Ergebnisse von Rechenoperationen).

Das gleiche gilt für "Beschilderung". Aber nicht im selben Maße. Die Umwandlung von sprachsemantischer Beschilderung in von einem Prozessor akzeptierte Beschilderung ist erforderlich, um Zahlen als Zahlen zu verwenden. Eine Konvertierung von Big Endian zu Little Endian und umgekehrt ist erforderlich, um Zahlen als Daten zu verwenden (sie über das Netzwerk zu senden oder Metadaten über Daten, die über das Netzwerk gesendet werden, wie z. B. Payload-Längen) zu repräsentieren. 

Allerdings scheint diese Entscheidung hauptsächlich von Anwendungsfällen bestimmt zu sein. Umgekehrt gibt es einen guten pragmatischen Grund, bestimmte Anwendungsfälle zu ignorieren. Der Pragmatismus ergibt sich aus der Tatsache, dass die Umwandlung von Endiananness teurer ist als die meisten Rechenoperationen. 

Wenn eine Sprache eine Semantik für das Beibehalten von Zahlen als Little Endian hätte, würde es Entwicklern ermöglichen, sich in den Fuß zu schießen, indem sie in einem Programm, das viel Arithmetik ausführt, wenig Endianness von Zahlen erzwingen. Bei einer Entwicklung auf einem Little-Endian-Rechner wäre diese Durchsetzung von Endianness ein No-Op. Bei der Portierung auf einen Big-Endian-Rechner würde es jedoch zu unerwarteten Verzögerungen kommen. Wenn die fraglichen Variablen sowohl für die Arithmetik als auch für die Netzwerkdaten verwendet würden, würde dies den Code vollständig nicht portierbar machen. 

Wenn diese Endian-Semantik nicht vorhanden ist oder sie dazu gezwungen werden, explizit compilerspezifisch zu sein, zwingen die Entwickler den mentalen Schritt des Denkens der Zahlen als "gelesen" oder "geschrieben" in/aus dem Netzwerkformat. Dies macht den Code, der zwischen Netzwerk- und Host-Bytereihenfolge in arithmetischen Operationen hin und her konvertiert, umständlich und weniger wahrscheinlich die bevorzugte Schreibweise eines faulen Entwicklers. 

Und da Entwicklung ein menschliches Unterfangen ist, ist es eine gute Sache, schlechte Entscheidungen unangenehm zu machen.

Edit : Hier ist ein Beispiel, wie das schlecht laufen kann: Angenommen, dass die Typen little_endian_int32 und big_endian_int32 eingeführt werden. Dann ist little_endian_int32(7) % big_endian_int32(5) ein konstanter Ausdruck. Was ist das Ergebnis? Werden die Zahlen implizit in das native Format konvertiert? Wenn nicht, wie ist das Ergebnis? Schlimmer noch, welchen Wert hat das Ergebnis (das in diesem Fall wahrscheinlich auf jeder Maschine gleich sein sollte)? 

Wenn Multi-Byte-Zahlen als reine Daten verwendet werden, sind Char-Arrays genauso gut. Selbst wenn es sich um "Ports" handelt (die wirklich in Tabellen oder ihren Hashwerten nachschlagen), handelt es sich nur um Byte-Sequenzen und nicht um Integer-Typen (auf denen Arithmetik ausgeführt werden kann). 

Wenn Sie nun die zulässigen arithmetischen Operationen für explizit endianische Zahlen auf die für Zeigertypen zulässigen Operationen beschränken, haben Sie möglicherweise einen besseren Fall für die Vorhersagbarkeit. Dann macht myPort + 5 tatsächlich Sinn, wenn myPort auf einem Big-Endian-Rechner als little_endian_int16 deklariert ist. Gleiches für lastPortInRange - firstPortInRange + 1. Wenn die Arithmetik wie für Zeigertypen funktioniert, würde dies das tun, was Sie erwarten würden, aber firstPort * 10000 wäre illegal. 

Dann geraten Sie natürlich in das Argument, ob das Merkmal Aufblasen durch einen möglichen Nutzen gerechtfertigt ist.

2

Aus der Perspektive des pragmatischen Programmierers bei der Suche nach Stack Overflow ist es erwähnenswert, dass der Geist dieser Frage mit einer Utility-Bibliothek beantwortet werden kann. Boost hat eine solche Bibliothek:

http://www.boost.org/doc/libs/1_65_1/libs/endian/doc/index.html

Das Merkmal der Bibliothek, das dem besprochenen Sprachmerkmal am ähnlichsten ist, besteht aus einer Reihe arithmetischer Typen wie big_int16_t.

1
Peter

Weil niemand vorgeschlagen hat, es in den Standard aufzunehmen, und/oder weil der Compiler-Implementierer es nie für nötig gehalten hat.

Vielleicht könnten Sie es dem Ausschuss vorschlagen. Ich glaube nicht, dass es schwierig ist, sie in einem Compiler zu implementieren: Compiler schlagen bereits grundlegende Typen vor, die keine grundlegenden Typen für die Zielmaschine sind.

Die Entwicklung von C++ ist eine Angelegenheit aller C++ - Codierer.

@Schimmel. Hören Sie nicht auf Personen, die den Status Quo rechtfertigen! Alle angeführten Argumente, die diese Abwesenheit rechtfertigen, sind mehr als fragil. Ein studentischer Logiker konnte seine Inkonsistenz feststellen, ohne etwas über Informatik zu wissen. Schlagen Sie es einfach vor und kümmern Sie sich nicht um pathologische Konservative. (Hinweis: Schlagen Sie neue Typen vor und nicht ein Qualifikationsmerkmal, da die Keywords unsigned und signed als Fehler betrachtet werden.

0
Oliv