Ist a ^ a oder aa undefiniertes Verhalten, wenn a nicht initialisiert ist?

76

Betrachten Sie dieses Programm:

Ist es undefiniertes Verhalten?

Auf den ersten Blick aist eine nicht initialisierte Variable. Das deutet also auf undefiniertes Verhalten hin. Aber a^aund a-asind 0für alle Werte gleich a, zumindest denke ich, dass dies der Fall ist. Ist es möglich zu argumentieren, dass das Verhalten gut definiert ist?

David Heffernan
quelle
Ich würde erwarten, dass dies genau definiert ist, da der Wert von a unbekannt, aber fest ist und sich nicht ändern sollte. Die Frage ist, ob der Compiler den Speicherplatz für aden dort befindlichen Müll zuweisen und anschließend daraus lesen würde . Wenn nicht, ist das Verhalten undefiniert.
Martin
Hmm, solange die Variable nicht markiert volatileist, würde ich das als definiertes Verhalten akzeptieren. a ^= a, ist genau gleichbedeutend mita = 0
fileoffset
30
@martin: Es ist nicht behoben. Der Wert darf sich ändern. Dies ist eine sehr praktische Überlegung. Eine Variable kann einem CPU-Register zugewiesen werden, aber während sie nicht initialisiert ist (dh ihre effektive Wertlebensdauer hat noch nicht begonnen), kann dasselbe CPU-Register von einer anderen Variablen belegt werden. Die Änderungen in dieser anderen Variablen werden als "instabiler" Wert dieser nicht initialisierten Variablen angesehen. Dies ist etwas , das wird oft mit nicht initialisierten Variablen in der Praxis beobachtet.
Am
@ AndrereyT das ist eine schöne Erklärung
Martin
1
Egal , fand es, mein Fehler: stackoverflow.com/questions/20300665/… , und es war in der Tat für C.
Thomas

Antworten:

73

In C11:

  • Es ist gemäß 6.3.2.1/2 explizit undefiniert, wenn aseine Adresse nie vergeben wurde ( siehe unten).
  • Es könnte sich um eine Trap-Darstellung handeln (die beim Zugriff UB verursacht). 6.2.6.1/5:

Bestimmte Objektdarstellungen müssen keinen Wert des Objekttyps darstellen.

Vorzeichenlose Ints können Trap-Darstellungen haben (z. B. wenn 15 Präzisionsbits und 1 Paritätsbit vorhanden sind, kann der Zugriff aeinen Paritätsfehler verursachen).

6.2.4 / 6 besagt, dass der Anfangswert unbestimmt ist und die Definition unter 3.19.2 entweder ein nicht spezifizierter Wert oder eine Trap-Darstellung ist .

Weiter: in C11 6.3.2.1/2, wie von Pascal Cuoq ausgeführt:

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.

Dies hat keine Ausnahme für Zeichentypen, daher scheint diese Klausel die vorhergehende Diskussion zu ersetzen. Der Zugriff xist sofort undefiniert, auch wenn keine Trap-Darstellungen vorhanden sind. Diese Klausel wurde zu C11 hinzugefügt, um Itanium-CPUs zu unterstützen, die tatsächlich einen Trap-Status für Register haben.


Systeme ohne Trap-Darstellungen: Aber was ist, wenn wir einwerfen &x;, damit der Einwand von 6.3.2.1/2 nicht mehr gilt und wir uns auf einem System befinden, von dem bekannt ist, dass es keine Trap-Darstellungen hat? Dann ist der Wert ein nicht spezifizierter Wert . Die Definition des nicht spezifizierten Wertes in 3.19.3 ist etwas vage, wird jedoch durch DR 451 klargestellt , was zu folgendem Schluss führt:

  • Ein nicht initialisierter Wert kann unter den beschriebenen Bedingungen seinen Wert ändern.
  • Jede Operation, die mit unbestimmten Werten ausgeführt wird, hat als Ergebnis einen unbestimmten Wert.
  • Bibliotheksfunktionen zeigen ein undefiniertes Verhalten, wenn sie für unbestimmte Werte verwendet werden.
  • Diese Antworten sind für alle Typen geeignet, die keine Trap-Darstellungen haben.

Bei dieser Auflösung int a; &a; int b = a - a;ergibt sich bimmer noch ein unbestimmter Wert.

Beachten Sie, dass wir uns immer noch im Bereich eines nicht spezifizierten Verhaltens (nicht undefinierten Verhaltens) befinden, wenn der unbestimmte Wert nicht an eine Bibliotheksfunktion übergeben wird. Die Ergebnisse mögen seltsam sein, z. B. if ( j != j ) foo();könnte man foo nennen, aber die Dämonen müssen in der Nasenhöhle gefangen bleiben.

MM
quelle
Angenommen, wir wüssten, dass es keine Trap-Werte gibt, könnten wir dann ein definiertes Verhalten argumentieren?
David Heffernan
15
@DavidHeffernan Sie können den Zugriff auf unbestimmte Daten genauso gut wie UB behandeln, da dies auch Ihr Compiler tun kann , selbst wenn keine Trap-Werte vorhanden sind. Bitte sehen Sie blog.frama-c.com/index.php?post/2013/03/13/…
Pascal Cuoq
@Pascal Das verstehe ich jetzt. Das ist der letzte Absatz von Andreys Antwort.
David Heffernan
@DavidHeffernan Die Beispiele sind sogar 2 * jseltsam, was etwas schlimmer ist als das Bild in Andreys Antwort, aber Sie haben die Idee.
Pascal Cuoq
Als der C89-Standard geschrieben wurde, wurde erwartet, dass Implementierungen viele Dinge spezifizieren würden, die der Standard nicht spezifizierte, und die Autoren des Standards sahen keinen Grund, alle Fälle detailliert zu beschreiben, in denen eine Aktion als definiert für Implementierungen betrachtet werden sollte, die bestimmte Dinge spezifizieren ( zB die Tatsache, dass "unsigned int" keine Trap-Darstellungen hat), aber bei Implementierungen, die dies nicht tun, undefiniert ist (z. B. wenn das Lesen eines unbestimmten Bitmusters als "unsigned int" eine Trap-Darstellung ergeben könnte).
Supercat
32

Ja, es ist undefiniertes Verhalten.

Erstens kann jede nicht initialisierte Variable eine "kaputte" (auch als "Trap" bezeichnete) Darstellung haben. Schon ein einziger Versuch, auf diese Darstellung zuzugreifen, löst ein undefiniertes Verhalten aus. Darüber hinaus können auch Objekte nicht einfangender Typen (wie unsigned char) noch spezielle plattformabhängige Zustände (wie NaT - Not-A-Thing - auf Itanium) annehmen, die als Manifestation ihres "unbestimmten Wertes" erscheinen könnten.

Zweitens kann nicht garantiert werden, dass eine nicht initialisierte Variable einen stabilen Wert hat. Zwei sequentielle Zugriffe auf dieselbe nicht initialisierte Variable können völlig unterschiedliche Werte lesen , weshalb selbst wenn beide Zugriffe a - a"erfolgreich" sind (kein Überfüllen), immer noch nicht garantiert werden kann, dass a - asie auf Null ausgewertet werden.

Ameise
quelle
1
Haben Sie ein Zitat für diesen letzten Absatz? Wenn dem so ist, brauchen wir nicht einmal Fallen in Betracht zu ziehen.
David Heffernan
2
@ Matt McNabb: Nun, dies könnte ein Problem sein, das durch verschiedene Versionen der Sprachspezifikation unterschiedlich gelöst wurde. In der Auflösung für DR # 260 ( open-std.org/jtc1/sc22/wg14/www/docs/dr_260.htm ) heißt es jedoch eindeutig, dass sich Variablen mit unbestimmten Werten "von selbst" willkürlich ändern können.
Am
4
@Matt McNabb: DR # 451 bestätigte sowohl im Oktober 2013 als auch im April 2014 im Wesentlichen die gleichen Entscheidungen von DR # 260 open-std.org/Jtc1/sc22/WG14/www/docs/dr_451.htm . Die Commitree-Antwort für DR # 451 besagt ausdrücklich "Dieser Standpunkt bestätigt die Position des C99 DR260"
AnT
1
@hyde Am nächsten an einer Trap-Darstellung, die Sie möglicherweise zur Hand haben, signalisieren Sie NaNs. en.wikipedia.org/wiki/NaN#Signaling_NaN Andernfalls benötigen Sie einen Computer mit expliziten Paritätsbits, einen Computer mit Vorzeichengröße, bei dem -0 als Trap-Wert betrachtet wird, oder etwas ähnlich Exotisches.
Pascal Cuoq
1
@chux: Nein. Es gibt nichts, was undefiniertes Verhalten auf "tut, was Sie denken, aber wenn nicht, Fallen" beschränkt. Buchstäblich jedes Verhalten ist erlaubt.
Ben Voigt
1

Wenn ein Objekt eine automatische Speicherdauer hat und seine Adresse nicht verwendet wird, führt der Versuch, es zu lesen, zu einem undefinierten Verhalten. Wenn die Adresse eines solchen Objekts verwendet wird und Zeiger vom Typ "vorzeichenloses Zeichen" zum Auslesen der Bytes verwendet werden, wird vom Standard garantiert, dass ein Wert vom Typ "vorzeichenloses Zeichen" erhalten wird, aber nicht alle Compiler halten sich in dieser Hinsicht an den Standard . ARM GCC 5.1, zum Beispiel, wenn angegeben:

generiert Code, der x zurückgibt, wenn y Null ist, auch wenn x außerhalb des Bereichs 0-65535 liegt. Der Standard macht deutlich, dass vorzeichenlose Zeichenlesevorgänge mit unbestimmtem Wert garantiert einen Wert im Bereich von ergeben unsigned charund das Verhalten von memmoveals einer Folge von Zeichenlese- und -schreibvorgängen äquivalent definiert ist. Daher sollte temp2 einen Wert haben, der über eine Folge von Zeichenschreibvorgängen darin gespeichert werden könnte, aber gcc beschließt, den memmove durch eine Zuweisung zu ersetzen und die Tatsache zu ignorieren, dass der Code die Adresse von temp1 und temp2 angenommen hat.

Es wäre hilfreich, ein Mittel zu haben, um einen Compiler zu zwingen, eine Variable als einen beliebigen Wert seines Typs zu betrachten, wenn ein solcher Wert gleichermaßen akzeptabel wäre, aber der Standard gibt kein sauberes Mittel dafür an (speichern) zum Speichern eines bestimmten Wertes, der funktionieren würde, aber oft unnötig langsam ist). Selbst Operationen, die eine Variable logisch zwingen sollten, einen Wert zu halten, der als eine Kombination von Bits darstellbar wäre, können nicht für alle Compiler verwendet werden. Folglich kann für solche Variablen nichts Nützliches garantiert werden.

Superkatze
quelle
Um fair zu sein, gibt es einen oben verlinkten Fehlerbericht darüber, was genau Sie mit einem unbestimmten Wert tun können, und ein Teil der Entscheidung bestand darin, anzugeben, dass die Übergabe eines unbestimmten Werts an eine Bibliotheksfunktion UB ist. memmoveist eine Bibliotheksfunktion, die hier gelten würde.
BeeOnRope
@BeeOnRope: Wenn die Autoren des Standards ein Mittel zum Auflösen unbestimmter Werte in schlimmstenfalls nicht spezifizierte Werte aufgenommen hätten, wäre es sinnvoll gewesen, die Verwendung solcher Mittel zu verlangen, bevor ansonsten unbestimmte Werte an Bibliotheksfunktionen übergeben würden. Angesichts des Mangels an solchen Mitteln kann ich nur in ihre Entscheidung einlesen, dass sie mehr daran interessiert sind, eine Sprache "einfach zu optimieren" als ihre Nützlichkeit zu maximieren.
Supercat
@BeeOnRope: Ihre Begründung ist, dass das Undefinieren des Verhaltens Compiler nicht daran hindern sollte, Verhalten zu definieren, wenn sie auf Prozessoren und Anwendungsfelder abzielen, wo dies praktisch und nützlich wäre. Ob solche Entscheidungen des Ausschusses eine solche Wirkung haben sollten oder nicht, ist leider offensichtlich.
Supercat
Ich nehme an, ja, sie hätten eine T std::freeze(T v)Methode einführen können , die einen "wackeligen" unbestimmten Wert in einen nicht spezifizierten, aber stabilen Wert verwandelt. Es hätte jedoch Nützlichkeit "dritter Ordnung": Die Verwendung eines unbestimmten Werts ist bereits unklar und wird nur sehr selten verwendet. Das Hinzufügen eines speziellen Konstrukts, um solche Werte nur zu verfestigen, scheint also nur weiter unten im Kaninchenbau einer bereits dunklen Ecke zu liegen der Standard, und es müsste in den Kerntransformations- / Optimierungsphasen vieler Compiler unterstützt werden.
BeeOnRope
@BeeOnRope: Die Möglichkeit, Werte einzufrieren, würde außerhalb der Situationen, in denen dies unerlässlich wäre, im Wesentlichen keine Kosten verursachen. Der Versuch, optimierten Code in Abwesenheit zu debuggen, ist ein sicherer Weg zum Wahnsinn. Wenn jemand schreibt foo=moo; if (foo < 100) bar(foo);und moounerwartet von einem anderen Thread geändert wird, kann es im Wesentlichen unmöglich sein, zu diagnostizieren, wann und wo etwas schief gelaufen ist. In der Lage zu sein, zu sagen foo=moo; freeze(foo); if (foo < 100) bar(foo);und den Compiler auf einen Wert für foofestlegen zu lassen, würde die Dinge viel robuster machen.
Supercat