(Warum) verwendet eine nicht initialisierte Variable undefiniertes Verhalten?

82

Wenn ich habe:

unsigned int x;
x -= x;

Es ist klar, dass nach diesem Ausdruck Null sein x sollte , aber überall, wo ich hinschaue, sagen sie, dass das Verhalten dieses Codes undefiniert ist, nicht nur der Wert von x(bis vor der Subtraktion).

Zwei Fragen:

  • Ist das Verhalten dieses Codes tatsächlich undefiniert?
    (Könnte der Code beispielsweise auf einem kompatiblen System abstürzen?)

  • Wenn ja, warum sagt C, dass das Verhalten undefiniert ist, wenn klar ist, dass xhier Null sein sollte?

    dh Was ist der Vorteil , wenn das Verhalten hier nicht definiert wird?

Natürlich könnte der Compiler verwenden Sie einfach was auch immer Müll Wert , den es in der Variable „praktisch“ erachtet, und es würde wie beabsichtigt funktionieren ... was mit diesem Ansatz falsch?

user541686
quelle
3
Was ist der Vorteil, wenn hier ein Sonderfall für das Verhalten definiert wird? Sicher, lassen Sie uns alle unsere Programme und Bibliotheken größer und langsamer machen, weil @Mehrdad vermeiden möchte, eine Variable in einem bestimmten und seltenen Fall zu initialisieren.
Paul Tomblin
9
@ W'rkncacnter Ich bin nicht einverstanden, dass das ein Betrüger ist. Unabhängig davon, ob welcher Wert es annimmt, erwartet das OP, dass er danach Null ist x -= x. Die Frage ist, warum der Zugriff auf nicht initialisierte Werte überhaupt UB ist.
Mysticial
6
Es ist interessant, dass die Aussage x = 0; wird normalerweise in Assembly in xor x, x konvertiert. Es ist fast das gleiche wie das, was Sie hier versuchen, aber mit xor anstelle von Subtraktion.
0xFE
1
Was ist der Vorteil, wenn das Verhalten hier nicht definiert wird? '- Ich hätte gedacht, dass der Vorteil des Standards, der nicht die Unendlichkeit von Ausdrücken mit Werten auflistet, die nicht von einer oder mehreren Variablen abhängen, offensichtlich ist. Gleichzeitig, @Paul, würde eine solche Änderung des Standards Programme und Bibliotheken nicht größer machen.
Jim Balter

Antworten:

90

Ja, dieses Verhalten ist undefiniert, aber aus anderen Gründen, als die meisten Menschen wissen.

Erstens ist die Verwendung eines einheitlichen Werts an sich kein undefiniertes Verhalten, sondern der Wert ist einfach unbestimmt. Der Zugriff darauf ist dann UB, wenn der Wert zufällig eine Trap-Darstellung für den Typ ist. Vorzeichenlose Typen haben selten Trap-Darstellungen, sodass Sie auf dieser Seite relativ sicher sind.

Was das Verhalten undefiniert macht, ist eine zusätzliche Eigenschaft Ihrer Variablen, nämlich dass sie "hätte deklariert werden können register", dh ihre Adresse wird niemals verwendet. Solche Variablen werden speziell behandelt, weil es Architekturen gibt, die echte CPU-Register haben, die eine Art zusätzlichen Status haben, der "nicht initialisiert" ist und keinem Wert in der Typdomäne entspricht.

Bearbeiten: Der relevante Satz des Standards ist 6.3.2.1p2:

Wenn der l-Wert ein Objekt mit automatischer Speicherdauer bezeichnet, das mit der Registerspeicherklasse deklariert werden konnte (dessen Adresse nie vergeben wurde), und dieses Objekt nicht initialisiert ist (nicht mit einem Initialisierer deklariert wurde und vor seiner Verwendung keine Zuweisung vorgenommen wurde) ) ist das Verhalten undefiniert.

Und es deutlicher zu machen, der folgende Code ist unter allen Umständen legal:

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • Hier werden die Adressen von aund bgenommen, so dass ihr Wert nur unbestimmt ist.
  • Da es unsigned charniemals Trap-Darstellungen gibt, deren unbestimmter Wert nur nicht spezifiziert ist, kann jeder Wert von unsigned charpassieren.
  • Am Ende a muss der Wert gehalten werden 0.

Edit2: a und bhaben nicht spezifizierte Werte:

3.19.3 nicht spezifizierter Wert Gültiger
Wert des relevanten Typs, wenn diese Internationale Norm keine Anforderungen stellt, für welchen Wert in irgendeinem Fall gewählt wird

Jens Gustedt
quelle
6
Vielleicht fehlt mir etwas, aber es scheint mir, dass unsignedes sicher Fallendarstellungen geben kann. Können Sie auf den Teil des Standards verweisen, der dies sagt? Ich sehe in §6.2.6.2 / 1 Folgendes: "Für vorzeichenlose Ganzzahltypen außer vorzeichenlosen Zeichen werden die Bits der Objektdarstellung in zwei Gruppen unterteilt: Wertbits und Füllbits (es muss keines der letzteren geben). ... dies wird als Wertdarstellung bezeichnet. Die Werte aller Füllbits sind nicht angegeben. ⁴⁴⁾ "mit dem Kommentar:" ⁴⁴⁾ Einige Kombinationen von Füllbits können Trap-Darstellungen erzeugen ".
Conio
6
Fortsetzung des Kommentars: "Einige Kombinationen von Füllbits können Trap-Darstellungen erzeugen, beispielsweise wenn ein Füllbit ein Paritätsbit ist. Unabhängig davon kann keine arithmetische Operation für gültige Werte eine andere Trap-Darstellung erzeugen als als Teil einer Ausnahmebedingung wie z ein Überlauf, und dies kann bei vorzeichenlosen Typen nicht auftreten. " - Das ist toll , wenn wir einen gültigen Wert für die Arbeit mit, aber der unbestimmte Wert könnte eine Falle Darstellung sein vor (zB Paritätsbit Satz falsch) initialisiert wird.
Conio
4
@conio Sie sind für alle Typen außer korrekt unsigned char, aber diese Antwort wird verwendet unsigned char. Beachten Sie jedoch: Ein streng konformes Programm kann sizeof(unsigned) * CHAR_BITbasierend darauf berechnen und bestimmen UINT_MAX, dass bestimmte Implementierungen möglicherweise keine Trap-Darstellungen für haben können unsigned. Nachdem dieses Programm diese Feststellung getroffen hat, kann es genau das tun, was diese Antwort bewirkt unsigned char.
4
@JensGustedt: Ist das nicht memcpyeine Ablenkung, dh würde Ihr Beispiel nicht immer noch zutreffen, wenn es durch ersetzt würde *&a = *&b;.
R .. GitHub STOP HELPING ICE
4
@R .. Ich bin mir nicht mehr sicher. Es gibt eine laufende Diskussion auf der Mailingliste des C-Komitees, und es scheint, dass all dies ein großes Durcheinander ist, nämlich eine große Lücke zwischen dem, was beabsichtigtes Verhalten ist (oder war) und dem, was tatsächlich geschrieben wurde. Was jedoch klar ist, ist, dass der Zugriff auf den Speicher als unsigned charund damit memcpyhilfreich ist, der für *&weniger klar ist. Ich werde berichten, sobald sich dies beruhigt hat.
Jens Gustedt
24

Der C-Standard bietet Compilern viel Spielraum für Optimierungen. Die Konsequenzen dieser Optimierungen können überraschend sein, wenn Sie ein naives Programmmodell annehmen, bei dem der nicht initialisierte Speicher auf ein zufälliges Bitmuster eingestellt ist und alle Operationen in der Reihenfolge ausgeführt werden, in der sie geschrieben wurden.

Hinweis: Die folgenden Beispiele sind nur gültig, da xihre Adresse nie vergeben wurde und sie daher "registerartig" ist. Sie wären auch gültig, wenn die Art der xFallen Darstellungen hätte; Dies ist selten bei nicht signierten Typen der Fall (es erfordert mindestens ein Bit Speicherplatz und muss dokumentiert werden) und ist für unmöglich unsigned char. Wenn xein vorzeichenbehafteter Typ vorhanden wäre, könnte die Implementierung das Bitmuster, das keine Zahl zwischen - (2 n-1 -1) und 2 n-1 -1 ist, als Trap-Darstellung definieren. Siehe Jens Gustedts Antwort .

Compiler versuchen, Variablen Register zuzuweisen, da Register schneller als Speicher sind. Da das Programm möglicherweise mehr Variablen verwendet, als der Prozessor Register hat, führen Compiler eine Registerzuordnung durch, was dazu führt, dass unterschiedliche Variablen dasselbe Register zu unterschiedlichen Zeiten verwenden. Betrachten Sie das Programmfragment

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

Wenn Zeile 3 ausgewertet wird, xist sie noch nicht initialisiert, daher muss Zeile 3 (aus Gründen des Compilers) eine Art Zufall sein, der aufgrund anderer Bedingungen, die der Compiler nicht klug genug war, um dies herauszufinden, nicht auftreten kann. Da zes nicht nach Zeile 4 und xnicht vor Zeile 5 verwendet wird, kann für beide Variablen dasselbe Register verwendet werden. Dieses kleine Programm ist also für die folgenden Operationen an Registern kompiliert:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

Der Endwert von xist der Endwert von r0und der Endwert von yist der Endwert von r1. Diese Werte sind x = -3 und y = -4 und nicht 5 und 4, wie dies bei xordnungsgemäßer Initialisierung der Fall wäre .

Betrachten Sie für ein ausführlicheres Beispiel das folgende Codefragment:

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Angenommen, der Compiler erkennt, dass dies conditionkeine Nebenwirkungen hat. Da conditionsich nichts ändert x, weiß der Compiler, dass der erste Durchlauf durch die Schleife möglicherweise nicht zugänglich ist, xda er noch nicht initialisiert ist. Daher ist die erste Ausführung des Schleifenkörpers äquivalent zu x = some_value(), es besteht keine Notwendigkeit, die Bedingung zu testen. Der Compiler kann diesen Code so kompilieren, als hätten Sie geschrieben

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Die Art und Weise dies innerhalb des Compilers modelliert werden kann , ist zu berücksichtigen , dass jeder Wert in Abhängigkeit von xhat , was Wert ist praktisch , solange xnicht initialisiert ist. Da das Verhalten, wenn eine nicht initialisierte Variable nicht definiert ist, anstatt dass die Variable lediglich einen nicht angegebenen Wert hat, muss der Compiler keine spezielle mathematische Beziehung zwischen den für Sie geeigneten Werten verfolgen. Somit kann der Compiler den obigen Code folgendermaßen analysieren:

  • wird während der ersten Schleifeniteration xnicht initialisiert, wenn die Zeit -xausgewertet wird.
  • -x hat undefiniertes Verhalten, daher ist sein Wert was auch immer-bequem ist.
  • Es gilt die Optimierungsregel , sodass dieser Code vereinfacht werden kann .condition ? value : valuecondition; value

Wenn derselbe Compiler mit dem Code in Ihrer Frage konfrontiert wird, analysiert er, dass bei der x = - xAuswertung der Wert von -xwas auch immer zweckmäßig ist. So kann die Zuordnung weg optimiert werden.

Ich habe nicht nach einem Beispiel für einen Compiler gesucht, der sich wie oben beschrieben verhält, aber es ist die Art von Optimierungen, die gute Compiler versuchen. Ich wäre nicht überrascht, wenn ich einem begegnen würde. Hier ist ein weniger plausibles Beispiel für einen Compiler, mit dem Ihr Programm abstürzt. (Es ist möglicherweise nicht so unplausibel, wenn Sie Ihr Programm in einem erweiterten Debugging-Modus kompilieren.)

Dieser hypothetische Compiler ordnet jede Variable auf einer anderen Speicherseite zu und richtet Seitenattribute so ein, dass das Lesen aus einer nicht initialisierten Variablen einen Prozessor-Trap verursacht, der einen Debugger aufruft. Jede Zuordnung zu einer Variablen stellt zunächst sicher, dass ihre Speicherseite normal zugeordnet ist. Dieser Compiler versucht nicht, eine erweiterte Optimierung durchzuführen. Er befindet sich in einem Debugging-Modus, um Fehler wie nicht initialisierte Variablen leicht zu lokalisieren. Bei der x = - xAuswertung verursacht die rechte Seite eine Falle und der Debugger wird gestartet.

Gilles 'SO - hör auf böse zu sein'
quelle
+1 Schöne Erklärung, der Standard kümmert sich besonders um diese Situation. Für eine Fortsetzung dieser Geschichte siehe meine Antwort unten. (zu lang, um einen Kommentar zu haben).
Jens Gustedt
@JensGustedt Oh, Ihre Antwort macht einen sehr wichtigen Punkt aus, den ich (und andere) übersehen haben: Wenn der Typ keine Trap-Werte hat, die für einen vorzeichenlosen Typ mindestens ein Bit "verschwenden" müssen, xhat er einen nicht initialisierten Wert, aber das Verhalten beim Zugriff würde dies tun definiert werden, wenn x kein registerähnliches Verhalten hatte.
Gilles 'SO - hör auf böse zu sein'
@ Gilles: Zumindest macht Clang die von Ihnen erwähnten Optimierungen: (1) , (2) , (3) .
Vlad
1
Welchen praktischen Vorteil hat es, wenn Dinge auf diese Weise geklirrt werden? Wenn nachgeschalteter Code niemals den Wert von verwendet x, können alle Operationen darauf weggelassen werden, unabhängig davon, ob sein Wert definiert wurde oder nicht. Wenn Code, der zB folgt if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;, gleichermaßen mit einem Wert von 0 bis 255 zufrieden wäre, der xin dem Fall enthalten sein könnte, in dem volatile1Null ergeben hätte, würde ich denken, dass eine Implementierung, die es dem Programmierer ermöglicht, ein unnötiges Schreiben wegzulassen, xals eine höhere Qualität angesehen werden sollte als eine, die würde sich benehmen ...
Supercat
... in diesem Fall auf völlig unvorhersehbare Weise. Eine Implementierung, die in diesem Fall zuverlässig eine implementierungsdefinierte Falle auslösen würde, könnte für bestimmte Zwecke noch als von höherer Qualität angesehen werden, aber ein völlig unvorhersehbares Verhalten scheint mir für so ziemlich jeden Zweck die Verhaltensweise mit der niedrigsten Qualität zu sein.
Superkatze
16

Ja, das Programm könnte abstürzen. Es kann beispielsweise Trap-Darstellungen geben (bestimmte Bitmuster, die nicht behandelt werden können), die einen CPU-Interrupt verursachen können, der das Programm zum Absturz bringen kann.

(6.2.6.1 in einem späten C11-Entwurf besagt) Bestimmte Objektdarstellungen müssen keinen Wert des Objekttyps darstellen. Wenn der gespeicherte Wert eines Objekts eine solche Darstellung hat und von einem lvalue-Ausdruck ohne Zeichentyp gelesen wird, ist das Verhalten undefiniert. Wenn eine solche Darstellung durch einen Nebeneffekt erzeugt wird, der das gesamte Objekt oder einen Teil davon durch einen lvalue-Ausdruck ohne Zeichentyp ändert, ist das Verhalten undefiniert.50) Eine solche Darstellung wird als Trap-Darstellung bezeichnet.

(Diese Erklärung gilt nur für Plattformen, auf denen unsigned intTrap-Darstellungen vorhanden sein können, was auf realen Systemen selten vorkommt. Einzelheiten und Verweise auf alternative und möglicherweise häufigere Ursachen, die zum aktuellen Wortlaut des Standards führen, finden Sie in den Kommentaren.)

Gl.
quelle
3
@VladLazarenko: Hier geht es um C, nicht um bestimmte CPUs. Jeder kann trivial eine CPU entwerfen, die Bitmuster für ganze Zahlen hat, die sie verrückt machen. Stellen Sie sich eine CPU vor, deren Register ein "verrücktes Bit" enthalten.
David Schwartz
2
Kann ich also sagen, dass das Verhalten bei ganzen Zahlen und x86 gut definiert ist?
3
Nun, theoretisch könnten Sie einen Compiler haben, der sich dafür entschieden hat, nur 28-Bit-Ganzzahlen (auf x86) zu verwenden und spezifischen Code hinzuzufügen, um jede Addition, Multiplikation (usw.) zu verarbeiten und sicherzustellen, dass diese 4 Bits nicht verwendet werden (oder andernfalls ein SIGSEGV ausgeben ). Ein nicht initialisierter Wert könnte dies verursachen.
Äq.
4
Ich hasse es, wenn jemand alle anderen beleidigt, weil dieser das Problem nicht versteht. Ob das Verhalten undefiniert ist, hängt ganz davon ab, was der Standard sagt. Oh, und das Szenario von eq ist überhaupt nicht praktisch ... es ist völlig erfunden.
Jim Balter
7
@Vlad Lazarenko: Itanium-CPUs haben für jedes Integer-Register ein NaT-Flag (Not a Thing). Das NaT-Flag wird zur Steuerung der spekulativen Ausführung verwendet und kann in Registern verbleiben, die vor der Verwendung nicht ordnungsgemäß initialisiert wurden. Das Lesen aus einem solchen Register mit einem gesetzten NaT-Bit ergibt eine Ausnahme. Siehe blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx
Nordic Mainframe
13

(Diese Antwort bezieht sich auf C 1999. Für C 2011 siehe Jens Gustedts Antwort.)

Der C-Standard besagt nicht, dass die Verwendung des Werts eines Objekts mit automatischer Speicherdauer, das nicht initialisiert wurde, ein undefiniertes Verhalten ist. In der Norm C 1999 heißt es in 6.7.8 10: „Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt.“ (In diesem Abschnitt wird weiter definiert, wie statische Objekte initialisiert werden. Die einzigen nicht initialisierten Objekte, um die wir uns kümmern, sind automatische Objekte.)

3.17.2 definiert "unbestimmten Wert" als "entweder einen nicht spezifizierten Wert oder eine Trap-Darstellung". 3.17.3 definiert "nicht spezifizierter Wert" als "gültiger Wert des relevanten Typs, wenn diese Internationale Norm keine Anforderungen stellt, für welchen Wert in irgendeinem Fall gewählt wird".

Wenn also der nicht initialisierte unsigned int xWert einen nicht angegebenen Wert hat, x -= xmuss er Null erzeugen. Damit bleibt die Frage, ob es sich möglicherweise um eine Fallendarstellung handelt. Der Zugriff auf einen Trap-Wert führt gemäß 6.2.6.1 zu undefiniertem Verhalten. 5.

Einige Arten von Objekten können Trap-Darstellungen aufweisen, z. B. die Signalisierungs-NaNs von Gleitkommazahlen. Ganzzahlen ohne Vorzeichen sind jedoch etwas Besonderes. Gemäß 6.2.6.2 repräsentiert jedes der N Wertbits eines vorzeichenlosen int eine Potenz von 2, und jede Kombination der Wertbits repräsentiert einen der Werte von 0 bis 2 N -1. Ganzzahlen ohne Vorzeichen können daher nur aufgrund einiger Werte in ihren Füllbits (z. B. eines Paritätsbits) Trap-Darstellungen haben.

Wenn auf Ihrer Zielplattform ein vorzeichenloses int keine Auffüllbits hat, kann ein nicht initialisiertes vorzeichenloses int keine Trap-Darstellung haben, und die Verwendung seines Werts kann kein undefiniertes Verhalten verursachen.

Eric Postpischil
quelle
Wenn xes eine Trap-Darstellung gibt, dann x -= xkönnte Trap, oder? Dennoch muss +1 für den Hinweis auf vorzeichenlose Ganzzahlen ohne zusätzliche Bits ein definiertes Verhalten haben - es ist eindeutig das Gegenteil der anderen Antworten und (laut Zitat) scheint es das zu sein, was der Standard impliziert.
user541686
Ja, wenn der Typ xeine Trap-Darstellung hat, wird x -= xmöglicherweise Trap ausgeführt. Selbst xwenn es einfach als Wert verwendet wird, kann es zu einer Falle kommen. (Es ist sicher xals l-Wert zu verwenden; das Schreiben in ein Objekt wird nicht durch eine darin enthaltene Trap-Darstellung beeinflusst.)
Eric Postpischil
vorzeichenlose Typen haben selten eine Fallendarstellung
Jens Gustedt
Raymond Chen zitiert : "Auf dem ia64 ist jedes 64-Bit-Register tatsächlich 65 Bit. Das zusätzliche Bit heißt" NaT "und steht für" nichts ". Das Bit wird gesetzt, wenn das Register keinen gültigen Wert enthält. Stellen Sie sich das als die ganzzahlige Version des Gleitkomma-NaN vor. ... Wenn Sie ein Register haben, dessen Wert NaT ist, und Sie es sogar falsch einatmen (versuchen Sie beispielsweise, seinen Wert im Speicher zu speichern), das Der Prozessor löst eine STATUS_REG_NAT_CONSUMPTION-Ausnahme aus. " Das heißt, ein Trap-Bit kann vollständig außerhalb des Werts liegen.
Prost und hth. - Alf
−1 Die Anweisung "Wenn auf Ihrer Zielplattform ein vorzeichenloses int keine Auffüllbits hat, kann ein nicht initialisiertes vorzeichenloses int keine Trap-Darstellung haben und die Verwendung seines Werts kann kein undefiniertes Verhalten verursachen." Schemata wie die x64 NaT-Bits werden nicht berücksichtigt.
Prost und hth. - Alf
11

Ja, es ist undefiniert. Der Code kann abstürzen. C sagt, das Verhalten sei undefiniert, da es keinen bestimmten Grund gibt, eine Ausnahme von der allgemeinen Regel zu machen. Der Vorteil ist der gleiche Vorteil wie in allen anderen Fällen von undefiniertem Verhalten - der Compiler muss keinen speziellen Code ausgeben, damit dies funktioniert.

Natürlich könnte der Compiler einfach den Müllwert verwenden, den er als "praktisch" in der Variablen erachtet, und er würde wie beabsichtigt funktionieren ... was ist an diesem Ansatz falsch?

Warum denkst du, passiert das nicht? Das ist genau der Ansatz. Der Compiler ist nicht erforderlich, damit es funktioniert, aber es ist nicht erforderlich, damit es fehlschlägt.

David Schwartz
quelle
1
Der Compiler muss dafür aber auch keinen speziellen Code haben. Einfach die Aufteilung der Raum (wie immer) und nicht Initialisieren Sie die Variable gibt es das richtige Verhalten. Ich denke nicht, dass das spezielle Logik braucht.
user541686
7
1) Sicher, sie könnten haben. Aber ich kann mir kein Argument vorstellen, das das besser machen würde. 2) Die Plattform weiß, dass man sich nicht auf den Wert des nicht initialisierten Speichers verlassen kann, daher kann er ihn frei ändern. Beispielsweise kann nicht initialisierter Speicher im Hintergrund auf Null gesetzt werden, damit auf Null gesetzte Seiten bei Bedarf zur Verwendung bereitstehen. (Überlegen Sie, ob dies passiert: 1) Wir lesen den zu subtrahierenden Wert, sagen wir, wir erhalten 3. 2) Die Seite wird auf Null gesetzt, weil sie nicht initialisiert ist, und ändert den Wert auf 0. 3) Wir subtrahieren atomar, ordnen die Seite zu und erstellen die Wert -3. Ups.)
David Schwartz
2
-1, weil Sie Ihren Anspruch überhaupt nicht begründen. Es gibt Situationen, in denen zu erwarten ist, dass der Compiler nur den Wert verwendet, der in den Speicherort geschrieben ist.
Jens Gustedt
1
@JensGustedt: Ich verstehe deinen Kommentar nicht. Können Sie bitte klarstellen?
David Schwartz
3
Weil Sie nur behaupten, dass es eine allgemeine Regel gibt, ohne sich darauf zu beziehen. Als solches ist es nur ein Versuch des "Beweises durch Autorität", was ich von SO nicht erwarte. Und um nicht effektiv zu argumentieren, warum dies kein unspezifischer Wert sein kann. Der einzige Grund, warum dies im allgemeinen Fall UB ist, ist, dass xdies als deklariert werden könnte register, das heißt, dass seine Adresse niemals vergeben wird. Ich weiß nicht, ob Sie sich dessen bewusst waren (wenn Sie es effektiv versteckt haben), aber eine richtige Antwort muss es erwähnen.
Jens Gustedt
6

Für jede Variable eines beliebigen Typs, die nicht initialisiert ist oder aus anderen Gründen einen unbestimmten Wert enthält, gilt Folgendes für den Code, der diesen Wert liest:

  • Falls die Variable eine automatische Speicherdauer hat und ihre Adresse nicht vergeben ist, ruft der Code immer ein undefiniertes Verhalten auf [1].
  • Andernfalls ruft der Code, falls das System Trap-Darstellungen für den angegebenen Variablentyp unterstützt, immer undefiniertes Verhalten auf [2].
  • Andernfalls nimmt die Variable einen nicht angegebenen Wert an, wenn keine Trap-Darstellungen vorhanden sind. Es gibt keine Garantie dafür, dass dieser nicht angegebene Wert bei jedem Lesen der Variablen konsistent ist. Es ist jedoch garantiert, dass es sich nicht um eine Trap-Darstellung handelt, und es wird daher garantiert, dass kein undefiniertes Verhalten hervorgerufen wird [3].

    Der Wert kann dann sicher verwendet werden, ohne einen Programmabsturz zu verursachen, obwohl dieser Code nicht auf Systeme mit Trap-Darstellungen portierbar ist.


[1]: C11 6.3.2.1:

Wenn der l-Wert ein Objekt mit automatischer Speicherdauer bezeichnet, das mit der Registerspeicherklasse deklariert werden konnte (dessen Adresse nie vergeben wurde), und dieses Objekt nicht initialisiert ist (nicht mit einem Initialisierer deklariert wurde und vor seiner Verwendung keine Zuweisung vorgenommen wurde) ) ist das Verhalten undefiniert.

[2]: C11 6.2.6.1:

Bestimmte Objektdarstellungen müssen keinen Wert des Objekttyps darstellen. Wenn der gespeicherte Wert eines Objekts eine solche Darstellung hat und von einem lvalue-Ausdruck ohne Zeichentyp gelesen wird, ist das Verhalten undefiniert. Wenn eine solche Darstellung durch einen Nebeneffekt erzeugt wird, der das gesamte Objekt oder einen Teil davon durch einen lvalue-Ausdruck ohne Zeichentyp ändert, ist das Verhalten undefiniert.50) Eine solche Darstellung wird als Trap-Darstellung bezeichnet.

[3] C11:

3.19.2
Unbestimmter Wert
entweder ein nicht spezifizierter Wert oder eine Trap-Darstellung

3.19.3 Nicht
spezifizierter Wert Gültiger
Wert des relevanten Typs, bei dem diese Internationale Norm keine Anforderungen an die Auswahl des
Werts stellt. HINWEIS Ein nicht spezifizierter Wert kann keine Trap-Darstellung sein.

3.19.4
Trap-Darstellung
Eine Objektdarstellung, die keinen Wert des Objekttyps darstellen muss

Lundin
quelle
Ich würde argumentieren, dass dies zu "Es ist immer undefiniertes Verhalten" führt, da die abstrakte C-Maschine Trap-Darstellungen haben kann. Nur weil Ihre Implementierung sie nicht verwendet, wird der Code nicht definiert. Tatsächlich würde ein striktes Lesen nicht einmal darauf bestehen, dass die Trap-Darstellungen in Hardware vorliegen müssen, was ich nicht sagen kann. Ich verstehe nicht, warum ein Compiler nicht entscheiden konnte, dass ein bestimmtes Bitmuster ein Trap ist. Überprüfen Sie dies jedes Mal, wenn die Variable gelesen wird und UB aufrufen.
Vality
2
@Vality In der realen Welt sind 99,9999% aller Computer Zweierkomplement-CPUs ohne Trap-Darstellungen. Daher ist keine Trap-Darstellung die Norm und die Diskussion des Verhaltens auf solchen realen Computern ist von hoher Relevanz. Es ist nicht hilfreich anzunehmen, dass wild exotische Computer die Norm sind. Fallendarstellungen in der realen Welt sind so selten, dass das Vorhandensein des Begriffs Fallendarstellung in der Norm meist als Standardfehler anzusehen ist, der aus den 1980er Jahren stammt. Ebenso wie die Unterstützung für die eigenen Ergänzungs- und Zeichen- und Größencomputer.
Lundin
2
Übrigens ist dies ein ausgezeichneter Grund, warum stdint.himmer anstelle der nativen Typen von C verwendet werden sollte. stdint.hErzwingt das 2er-Komplement und keine Auffüllbits. Mit anderen Worten, die stdint.hTypen dürfen nicht voller Mist sein.
Lundin
2
In der Antwort des Ausschusses auf den Fehlerbericht heißt es erneut: "Die Antwort auf Frage 2 lautet, dass jede Operation, die mit unbestimmten Werten durchgeführt wird, einen unbestimmten Wert hat." und "Die Antwort auf Frage 3 lautet, dass Bibliotheksfunktionen ein undefiniertes Verhalten zeigen, wenn sie für unbestimmte Werte verwendet werden."
Antti Haapala
2
DRs 451 und 260
Antti Haapala
2

Während sich viele Antworten auf Prozessoren konzentrieren, die den Zugriff auf nicht initialisierte Register abfangen, können selbst auf Plattformen ohne solche Traps eigenartige Verhaltensweisen auftreten, wenn Compiler verwendet werden, die keine besonderen Anstrengungen unternehmen, um UB auszunutzen. Betrachten Sie den Code:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

Ein Compiler für eine Plattform wie den ARM, bei der alle Befehle außer Laden und Speichern in 32-Bit-Registern ausgeführt werden, kann den Code in angemessener Weise verarbeiten, wie:

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

Wenn einer der flüchtigen Lesevorgänge einen Wert ungleich Null ergibt, wird r0 mit einem Wert im Bereich von 0 bis 65535 geladen. Andernfalls wird alles ausgegeben, was beim Aufruf der Funktion enthalten war (dh der an x ​​übergebene Wert), der möglicherweise kein Wert im Bereich 0..65535 ist. Dem Standard fehlt eine Terminologie zur Beschreibung des Verhaltens von Werten, deren Typ uint16_t ist, deren Wert jedoch außerhalb des Bereichs von 0..65535 liegt, mit der Ausnahme, dass jede Aktion, die ein solches Verhalten erzeugen könnte, UB aufruft.

Superkatze
quelle
Interessant. Wollen Sie damit sagen, dass die akzeptierte Antwort falsch ist? Oder sagen Sie, dass es theoretisch richtig ist, aber in der Praxis können Compiler seltsamere Dinge tun?
user541686
@Mehrdad: Es ist üblich, dass Implementierungen ein Verhalten aufweisen, das über die Grenzen dessen hinausgeht, was ohne UB möglich wäre. Ich denke, es wäre hilfreich, wenn der Standard das Konzept eines teilweise unbestimmten Werts erkennen würde, dessen "zugewiesene" Bits sich im schlimmsten Fall nicht spezifiziert verhalten, aber mit zusätzlichen oberen Bits, die sich nicht deterministisch verhalten (z Das Ergebnis der obigen Funktion wird in einer Variablen vom Typ gespeichert, uint16_tdie manchmal als 123 und manchmal als 6553623 gelesen werden kann. Wenn das Ergebnis ignoriert wird ...
Supercat
... oder so verwendet, dass alle möglichen Lesarten zu Endergebnissen führen, die den Anforderungen entsprechen, sollte das Vorhandensein eines teilweise unbestimmten Werts kein Problem sein. Andererseits enthält der Standard nichts, was die Existenz von teilweise unbestimmten Werten unter Umständen zulassen würde, unter denen der Standard irgendwelche Verhaltensanforderungen auferlegen würde.
Supercat
Es scheint mir , dass das, was Sie beschreiben , ist genau das, was in der akzeptierten Antwort ist - , dass , wenn eine Variable könnte mit deklariert wurde register, dann kann es zusätzliche Bits hat, die das Verhalten potenziell unbestimmt machen. Genau das sagst du, oder?
user541686
@Mehrdad: Die akzeptierte Antwort konzentriert sich auf Architekturen, deren Register einen zusätzlichen "nicht initialisierten" Status haben, und fängt ab, wenn ein nicht initialisiertes Register geladen wird. Solche Architekturen existieren, sind aber nicht alltäglich. Ich beschreibe ein Szenario, in dem alltägliche Hardware ein Verhalten aufweisen kann, das außerhalb des Bereichs des C-Standards liegt, aber sinnvoll eingeschränkt wäre, wenn ein Compiler dem Mix keine eigene zusätzliche Verrücktheit hinzufügt. Wenn eine Funktion beispielsweise einen Parameter hat, der eine auszuführende Operation auswählt und einige Operationen nützliche Daten zurückgeben, andere jedoch nicht ...
Supercat