gcc: operation on may be undefined

OP #1528062
Lesenswert?

Hallo Gemeinde

Ich verstehe nicht, warum gcc bei folgendem Code motzt:
1
1263: uint32 tCnt, tDir, tIdx;
2

3
1287: for(tCnt = 0; tCnt < NB_TR_CNTS; tCnt++)
4
1288: {
5
1289:   for(tDir = 0; tDir < NB_DIRS; tDir++)
6
1290:   {
7
1291:     tIdx = 8*tCnt + 4*tDir;
8
1292:     G_OpData.TravelCnt[tCnt][tDir] =    tBuf[tIdx++]
9
1293:                                      + (tBuf[tIdx++] << 8)
10
1294:                                      + (tBuf[tIdx++] << 16)
11
1295:                                      + (tBuf[tIdx++] << 24);
12
1296:   }
13
1297: }

file.c:1295: warning: operation on `tIdx' may be undefined
file.c:1295: warning: operation on `tIdx' may be undefined
file.c:1295: warning: operation on `tIdx' may be undefined

Es hat wohl mit dem Post-Inkrement zu tun. Trotzdem finde ich, dass das 
so in Ordnung ist.
Gast #1528102
Lesenswert?

In Java wäre das wohl ok. In C isses undefiniert, da niemand weiß, wann 
der um 1 erhöhte Wert wieder an die Variable zurückgeht.
Ein Übersetzer könnte etwa folgendes draus machen:
1
     G_OpData.TravelCnt[tCnt][tDir] =    tBuf[tIdx]
2
                                      + (tBuf[tIdx] << 8)
3
                                      + (tBuf[tIdx] << 16)
4
                                      + (tBuf[tIdx] << 24);
5
tIdx += 4;

Es fehlt ein Sequenzpunkt dazwischen. Ansonsten halte ich auch wenig von 
solchen Spielereien.

Lösung:
1
     G_OpData.TravelCnt[tCnt][tDir] =    tBuf[tIdx + 0]
2
                                      + (tBuf[tIdx + 1] << 8)
3
                                      + (tBuf[tIdx + 2] << 16)
4
                                      + (tBuf[tIdx + 3] << 24);
5
tIdx += 4;
Übersichtlich und jeder verstehts, ohnes Gehirn zu verknoten.
Gast #1528121
Lesenswert?

> Es hat wohl mit dem Post-Inkrement zu tun. Trotzdem finde ich, dass das
> so in Ordnung ist.

Ob du das findest, ist aber dem Compiler egal. ;-)
Wie Sven schreibt, fehlt ein Sequenzpunkt zwischen den Inkrements. Das 
Verhalten von Code, der eine Variable mehrmals ändert oder ändert und 
liest, ohne daß dazwischen ein Sequenzpunkt liegt, ist in C undefiniert. 
Daher zurecht die Warnung.
Gast #1528142
Lesenswert?

A. K. schrieb:
> Sven P. schrieb:
>
>> In Java wäre das wohl ok.
>
> Wie ist das denn in Java definiert?
Keine Ahnung, ich erinnere mich nur an so ein hirnverbranntes Beispiel 
für Java, und der Compiler hat keine Warnung ausgespuckt.

Hier isses:
http://www.thestudentroom.co.uk/showthread.php?t=639421
In C/++ wäre das hochgradig undefiniert und totaler Schwachsinn isses 
sowieso.
Gast #1528186
Lesenswert?

Hi, ich habe letzte Nacht kaum geschlafen, die Heizung war kaputt, hab 
seit 7 Uhr Handwerker im Haus, die die Eingangstür wechseln - und Kaffe 
ist das einzige was mich jetzt noch am leben hält ;-) Vielleicht liegt 
es ja daran, dass ich total auf den Schlauch stehe und das Problem nicht 
nachvollziehen kann.
Mein gcc (version 3.4.5 (mingw-vista special)), aufgerufen mit -Wall, 
meckert auf jedenfall nicht. Welche gcc version für welches Target mit 
welchen compiler flags benutzt Du denn? Und wieso ist das Verhalten 
undefiniert? Gibt es deswegen nicht auch die C-Prioritäten? IMHO steht 
da doch: Greife auf das Array tBuff mit dem Index tIdx zu, danach 
inkrementiere tIdx, danach greife auf das Array tBuff mit dem neuen Wert 
von tIdx zu, danach inkrementiere tIdx, danach addiere die Inhalte der 
ersten beiden Array zugriffe usw.

Grüße Daniel
Gast #1528198
Lesenswert?

Daniel H. schrieb:
> IMHO steht
> da doch: Greife auf das Array tBuff mit dem Index tIdx zu, danach
> inkrementiere tIdx, danach greife auf das Array tBuff mit dem neuen Wert
> von tIdx zu, danach inkrementiere tIdx, danach addiere die Inhalte der
> ersten beiden Array zugriffe usw.
Ne, es steht nirgendwo, WANN tIdx seinen neuen Wert bekommt. Dafür gibts 
ja Sequenzpunkte, und die fehlen hier.
#1528201
Lesenswert?

Daniel H. schrieb:
> IMHO steht
> da doch: Greife auf das Array tBuff mit dem Index tIdx zu, danach
> inkrementiere tIdx,

Nein.
Das steht da eben nicht.

Der Compiler ist in seiner Entscheidung frei, wann genau er das 
Inkrementieren durchführen will. Er muss es nur irgendwann machen, ehe 
das nächste Statement beginnt (Exakt gesagt: Vor dem nächsten Sequence 
Point, aber in dem Fall ist das der ';', also der Zeitpunkt an dem das 
nächste Statement beginnt)

Wenn der Compiler will, kann er alle 4 Inkrements bis ans Ende der 
Ausführungskette verschieben.
#1528203
Lesenswert?

@Daniel: Deine postulierte Abfolge setzt voraus, dass von links nach 
rechts ausgewertet wird, und dass in einem Ausdruck enthaltene 
Zuweisungen in einer bestimmten Reihenfolge entsprechend der Prioritäten 
ausgeführt werden. Dem ist nicht so. Zuweisungen in einem Ausdruck (a++ 
und ++a sind Zuweisungen) sind erst am schon sattsam erwähnten sequence 
point mit Sicherheit durchgeführt, davor kann es sein muss aber nicht 
sein. So ist C nun einmal definiert. Beschwerden bitte an Dennis Ritchie 
richten.
#1528214
Lesenswert?

A. K. schrieb:
> @Daniel: Deine postulierte Abfolge setzt voraus, dass von links nach
> rechts ausgewertet wird,

Darauf möchte ich noch näher eingehen, weil es wichtig ist.
Oft wird nämlich Operatoren-Reihenfolge mit Auswerte-Reihenfolge 
verwechselt.
Operatoren-Reihenfolge ist definiert.
Es ist zb exakt festgelegt, dass
    a + b + c
als
    ( a + b ) + c
berechnet wird.

Es ist aber nicht festgelegt, in welcher Reihenfolge die Teilausdrücke 
a, b, und c ausgewertet (dh. ein Zahlenwert dafür bestimmt) werden.

Der Compiler kann zuerst c auswerten, dann b und dann a (oder jede 
andere beliebige Reihenfolge) und dann die Teilwerte gemäss

    ( a + b ) + c

zum Gesamtwert zusammenführen.

Das ist mit Auswerte-Reihenfolge gemeint: Die Reihenfolge, in der 
Teilausdrücke bewertet werden.
Wenn die Teilausdrücke Nebeneffekte besitzen (wie zb hier das Inkrement) 
ist alleine dadurch schon ein undefiniertes Verhalten gegeben.
Ein anderes Beispiel:

   result = foo() + bar();

Es ist durch die undefinierte Auswertereihenfolge nicht definiert, ob 
zuerst die Funktion foo() und erst dann die Funktion bar() aufgerufen 
wird, oder umgekehrt. Wenn daher bar() darauf angewiesen ist, dass foo() 
zuerst aufgerufen wird (weil es zb einen Wert für bar() einstellen 
muss), dann hat man hier ein Problem.
Gast #1528225
Lesenswert?

Danke an alle! Jetzt habe auch ich es geschnallt :-)

Bei a = b + b++; kann das Inkrement z.B vor oder nach der Addition 
erfolgen, was natürlich zu unterschiedlichen Verhalten führt. Sicher ist 
nur, dass b nach dieser Zeile inkrementiert wurde.

Grüße, Daniel
Gast #1528231
Lesenswert?

Das wiederum ist dann nicht mit der Kurzschluss-Bewertung zu 
verwechseln, bei welcher die Auswertereihenfolge festgelegt ist:
1
if ( foo() && bar() ) {
2

3
}
4

5
if ( foo() || bar() ) {
6

7
}

In beiden Fällen wird foo() zuerst bewertet. Dadurch lassen sich solche 
Sachen formulieren:
1
Zeiger p;
2
if ( (p != NULL) && (p->bla) ) ..
#1528267
Lesenswert?

Sven P. schrieb:
> Das wiederum ist dann nicht mit der Kurzschluss-Bewertung zu
> verwechseln, bei welcher die Auswertereihenfolge festgelegt ist:

Ganz genau.
Geregelt ist das dadurch, dass sowohl && als auch || als Sequence Point 
gelten.

Die wichtigsten Sequence Points sind
* && || und Komma Operator (nit zu verwechseln mit dem normalen Komma)
* beim ? in einem ?: Ausdruck
* Am Ende eines vollständigen Ausdrucks (vulgo: beim ';')
  Dazu zählt aber auch: Der Ausdruck, der ein if, while etc. steuert
* Beim Aufruf einer Funktion, nachdem die Funktionsargumente
  ausgewertet wurden. Aber Achtung: Die Reihenfolge der Auswertung der
  Funktionsargumente ist nach wie vor undefiniert, genauso wie der
  exakte Zeitpunkt, wann Nebeneffekte dabei abgearbeitet werden.
  Definiert ist lediglich, dass Nebeneffekte abgeschlossen sind, wenn
  die Funktion die Kontrolle erhält.
  foo( i++, i++ );
  hat daher undefiniertes Verhalten.
  Und nein: Dieses ',' hier ist nicht der Komma-Operator.


Grob gesagt kann man festhalten:
Der Compiler ist zwischen 2 Sequence-Points frei in seiner Entscheidung, 
in welcher Reihenfolge er was abarbeiten möchte. Normalerweise hat ein 
bestimmter Compiler eine bestimmte Auswertereihenfolge, die sich oft aus 
den Eigenschaften der zugrundeliegenden Hardware ergibt. Zb werden oft 
Funktionsargumente von rechts nach links ausgewertet, weil sie dann in 
der richtigen Reihenfolge auf dem Stack abgelegt werden können. 
Gefährlich wird es erst, wenn man implizit von einer bestimmten 
Auswertereihenfolge ausgeht und das Programm von einem Compiler auf 
einen anderen portiert wird, der eine andere Reihenfolge benutzt.
Gast #1528273
Lesenswert?

> if ( foo() && bar() ) {
>
> }
>
> if ( foo() || bar() ) {
>
> }
>
>
> In beiden Fällen wird foo() zuerst bewertet.

Deshalb stellen in C das && und das || auch Sequenzpunkte dar.

> Dadurch lassen sich solche
> Sachen formulieren:
> Zeiger p;
> if ( (p != NULL) && (p->bla) ) ..

Theretisch geht sogar das::
1
  p && p->x++ || printf("Kein p\n");
(zugegebenermaßen arg konstrukiert)
Gast #1528280
Lesenswert?

Achso, dabei am Rande ein Zitat:

Die Programmiersprache C. Ein Nachschlagewerk.
Regionales Rechenzentrum für Niedersachsen/Leibniz-Universität Hannover, 
Zentralinstitut für angewandte Mathematik, Forschungsgruppe Jülich GmbH:

Dieses Institut hat so ein grünes Heftchen herausgebracht und freut 
sich:
17. unveränderte Auflage, September 2008.
Auflabe: bisher 163.000 Exemplare.

Und in jedem dieser Exemplare steht auf Seite 77 unten:
1
int i, j;
2
/* ... */
3

4
i = 1;
5
i = ++i + 1; /* dies ist auch möglich; i hat Wert 3 */
Gast #1528282
Lesenswert?

Rolf Magnus schrieb:
>
1
>   p && p->x++ || printf("Kein p\n");
2
>
> (zugegebenermaßen arg konstrukiert)

Garnicht mal. Frag mal Perl-Leute, oder benutz ne anständige Konsole:
1
cc -o test test.c && ./test
Gast #1528344
Lesenswert?

>>
>>   p && p->x++ || printf("Kein p\n");
>>
>> (zugegebenermaßen arg konstrukiert)
>
> Garnicht mal. Frag mal Perl-Leute, oder benutz ne anständige Konsole:
> cc -o test test.c && ./test

Ist mir schon klar. In Shellskripten ist das recht üblich.
Aus einem Makefile, das ich letzte Woche schrieb:
1
        @decompyle --verify -o $(dir $<) $< > $(patsubst %.pyc,%.log,$<) 2>&1 && echo ok || ( [ $$? -eq 3 ] && echo verify failed || echo failed; true)

Ich meinte, daß es für C eher konstruiert ist. Da ergibt sich bei dieser 
Art der Verwendung von Operatoren nämlich das Problem, daß sich in C 
nicht jeder Ausdruck als logischer Wert verwenden läßt. Das Beispiel 
klappt nur, weil p->x ein int ist, printf einen int zurückgibt und p 
sich in einen konvertieren läßt.
Gast #1528355
Lesenswert?

> i = ++i + 1; /* dies ist auch möglich; i hat Wert 3 */

Bevor ihr alle zu sehr über dieses Beispiel lästert: Kann jemand eine
Auswertereihenfolge angeben, bei der am Ende nicht 3 herauskommt? Mir
fällt spontan jedenfalls keine ein und bin deswegen fast überzeugt, dass
dieser Code nicht undefined ist.

"Fast" deswegen, weil ich noch nicht lange genug überlegt habe ;-)
Gast #1528362
Lesenswert?

> Mir fällt spontan jedenfalls keine ein und bin deswegen fast
> überzeugt, dass dieser Code nicht undefined ist.

Nehme alles zurück, da exakt dieses Beispiel im ISO-Standard als
Beispiel für einen undefinierten Ausdruck angegeben ist :)
Gast #1528376
Lesenswert?

yalu schrieb:
>> i = ++i + 1; /* dies ist auch möglich; i hat Wert 3 */
>
> Bevor ihr alle zu sehr über dieses Beispiel lästert: Kann jemand eine
> Auswertereihenfolge angeben, bei der am Ende nicht 3 herauskommt?
1
i = 1;
2

3
zwischen = i + 1;
4
i = zwischen + 1;
5
i = zwischen;
6

7
/* i ist 2 */
Gast #1528385
Lesenswert?

>> i = ++i + 1; /* dies ist auch möglich; i hat Wert 3 */
>
> Bevor ihr alle zu sehr über dieses Beispiel lästert: Kann jemand eine
> Auswertereihenfolge angeben, bei der am Ende nicht 3 herauskommt?

Spielt eigentlich keine Rolle. Laut ISO-C braucht i danach gar keinen 
Wert zu haben. Das Programm darf an der Stelle auch einfach 
stehenbleiben oder danach beliebigen Blödsinn veranstalten. Nicht der 
Wert gilt als undefiniert, sondern das gesamte Verhalten des Programms.

> Mir fällt spontan jedenfalls keine ein und bin deswegen fast überzeugt,
> dass dieser Code nicht undefined ist.

Er könnte z.B. beim Operator++ den inkrementierten Wert zurückgeben, da 
1 dazuzählen und an i zuweisen. Danach wird dann das vorher gemerkte 
Ergebnis vom Operator++ nach i geschrieben. Dann steht danach 2 in i.
Gast #1528406
Lesenswert?

A. K. schrieb:
> temp = i + 1;
> i = temp + 1;
> i = temp;

Sven P. schrieb:
> zwischen = i + 1;
> i = zwischen + 1;
> i = zwischen;

@Sven P.: Danke für die Übersetzung des Codes von A. K. ins Deutsche.
Spätestens jetzt habe ich's wirklich kapiert ;-)
#1528416
Lesenswert?

yalu schrieb:
>> i = ++i + 1; /* dies ist auch möglich; i hat Wert 3 */
>
> Bevor ihr alle zu sehr über dieses Beispiel lästert: Kann jemand eine
> Auswertereihenfolge angeben, bei der am Ende nicht 3 herauskommt?


Du machst hier noch immer den Fehler, dass du ++i als eine Einheit 
ansiehst, die 2 Aktion macht
  i erhöhen
  mit dem erhöhten Wert weiterrechnen

Tatsächlich ist die Sache aber etwas anders
* ++i
  liefert den um 1 erhöhten Wert von i

  das ist der Haupteffekt dieses Ausdrucks

* der Nebeneffekt dieses Ausdrucks ist es, dass dieser um 1 erhöhte
  Wert wieder in i abgespeichert wird.

Nur ist in C nicht festgelegt, wann genau Nebeneffekte abgearbeitet 
werden. Spätestens mit dem nächsten Sequence Point muss der Nebeneffekt 
ausgeführt worden sein. Aber bis dorthin darf der Compiler den 
Nebeneffekt schieben, wie er lustig ist.
Der Ausdruck ++i (oder i++) besteht also aus 2 Effekten, die zeitlich 
nichts miteinander zu tun haben. Auch bei i++ hindert zb keine C-Regel 
den Compiler daran, zunächst mit der Inkrementierung zu beginnen um 
dann, wenn der Wert des Ausdrucks benötigt wird, von diesem 
Zwischenergebnis wieder 1 abzuziehen. Theoretisch wäre das möglich, auch 
wenn es natürlich kein Compiler so machen wird.
#1528422
Lesenswert?

Fazit:
Wenn man eine bestimmte Reihenfolge der Auswertung erzwingen will (und 
man hat nicht den Fall && ||) dann ist es am besten, selbst die 
Anweisung in Teilanweisungen aufzusplitten. Dadurch erlangt man die 
Kontrolle über die Auswertereihenfolge

    result = foo() + bar();

vs

    result = foo();
    result += bar();
Gast #1528474
Lesenswert?

getc() gibt einen Integer zurück oder ein Zeichen. (c = getc(..)) ist 
wegen c vom Typ char. Dadurch kann EOF (-1) nicht mehr vom Zeichen #255 
unterschieden werden.

Karl:
An deinem Beispiel ist mir grad nicht klar, was schief gehen soll --
Gast #1528490
Lesenswert?

Sven P. schrieb:
> getc() gibt einen Integer zurück oder ein Zeichen. (c = getc(..)) ist
> wegen c vom Typ char. Dadurch kann EOF (-1) nicht mehr vom Zeichen #255
> unterschieden werden.

AHA! Das hab ich doch glatt übersehen.

Sven P. schrieb:
> Karl:
> An deinem Beispiel ist mir grad nicht klar, was schief gehen soll --

Ich könnte mir höchstens vorstellen, dass das EOF-Flag noch nicht 
gesetzt ist, wenn eine leere Datei geöffnet wird. Und damit wird die 
Schleife trotzdem einmal ausgeführt, obwohl nichts gelesen werden kann.

Zitat:
> This indicator is generally set by a previous operation on the stream that > 
reached the End-of-File.
Gast #1528495
Lesenswert?

> Karl:
> An deinem Beispiel ist mir grad nicht klar, was schief gehen soll --

feof(f) wird erst wahr, nachdem man versucht hat, über das Dateiende 
hinaus zu lesen. Die Schleife wird also einmal zu oft durchlaufen.
Gast #1528733
Lesenswert?

Sven P. schrieb:
> Im
> Standard steht ja nicht 'must be undefined'.

Genau, der Compiler muss in dem Fall, wenn er standardgemäß arbeiten 
soll, eine zufällige binäre Sequenz in den Code einbauen ;)
Gast #1528753
Lesenswert?

> Wieso "may be"? Gemäss Standard ist das Verhalten undefiniert.

Ich nehme an, daß die Erkennung dieses Falls nicht 100% zuverlässig ist, 
da sie doch schon ziemlich extensive Analyse des Codeflusses erfordert. 
Es könnte also einen Fall geben, in dem die Warnung kommt, obwohl alles 
in Ordnung ist. Deshalb "may be".
Gast #1528765
Lesenswert?

> Das hindert aber niemanden, der einen Compiler baut,
> es in irgendeiner Weise zu festzulegen.

Und was hat er davon? Gar nichts, ausser zusätzlichen Implementations- 
und Dokumentationsaufwand, weil der nächste Compiler folgende Variante 
implementiert hat:

> Genau, der Compiler muss in dem Fall, wenn er standardgemäß arbeiten
> soll, eine zufällige binäre Sequenz in den Code einbauen

So gesehen ist der Wortlaut "is undefined" das einzig Vernünftige. Wenns 
mir mal langweilig genug ist, schreib ich einen Bugreport.
Gast #1528771
Lesenswert?

Bartli schrieb:
>> Das hindert aber niemanden, der einen Compiler baut,
>> es in irgendeiner Weise zu festzulegen.
>
> Und was hat er davon? Gar nichts, ausser zusätzlichen Implementations-
> und Dokumentationsaufwand, weil der nächste Compiler folgende Variante
> implementiert hat:
Du kannst davon ausgehen, dass fast jeder Compiler es in irgendeiner 
Weise definiert. Nur die allerwenigsten werden bei solch einer 
Konstruktion einen Zufallsgenerator bemühen.

Ansonsten schließe ich mich Rolf Magnus an.
Gast #1528797
Lesenswert?

> Du kannst davon ausgehen, dass fast jeder Compiler es in irgendeiner
> Weise definiert.

Nee, definitiv nicht. Aufgrund ihrer Implementation dürften die meisten 
Compiler ein deterministisches Verhalten haben, aber du glaubst doch 
nicht im Ernst das da jemand hingeht, sich zuerst überm ganzen Problem 
den Kopf zerbricht und dann erst das Verhalten definiert und 
dokumentiert?
Gast #1528871
Lesenswert?

Bartli schrieb:
>> Du kannst davon ausgehen, dass fast jeder Compiler es in irgendeiner
>> Weise definiert.
>
> Nee, definitiv nicht. Aufgrund ihrer Implementation dürften die meisten
> Compiler ein deterministisches Verhalten haben, aber du glaubst doch
> nicht im Ernst das da jemand hingeht, sich zuerst überm ganzen Problem
> den Kopf zerbricht und dann erst das Verhalten definiert und
> dokumentiert?
Nö, hab ich das behauptet? Aber du siehst ja, die Java-Leute haben sich 
zuerst den Kopf darüber zerbrochen und das Verhalten dann fest 
definiert.

Laut C-Standard musst du auch damit rechnen, dass ein char 43 Bits hat, 
davon aber 13 Bits nicht zum Wert gehören. Ein Zeiger ist dann 7 chars 
breit und ein Funktionszeiger darf auch 5mal so groß sein, wie ein 
Datenzeiger. Außerdem sind char, short, int und long alle gleichbreit.

Und? Das wäre eine vollkommen Standard-C-Konforme Umgebung.
Gast #1528892
Lesenswert?

Schön, Sven. Und jetzt?

Trotzdem glaube ich nicht, dass gcc's Implementation auf eine bewusst 
definierte Art arbeitet, und die Formulation "may be" ist und bleibt 
Quark.

Was anderes ist es im Fall von "blah may be used uninitialized", aber 
darum gehts nicht, ebenso wenig um 47 Bits breite chars.

Und was Java betrifft: da gabs noch keine Altlasten in Form von 
Compilern die lange vor irgendwelchen Standards existierten. Da hat sich 
die Mühe das Zeugs vorwegs zu definieren sogar gelohnt.
Gast #1528965
Lesenswert?

> Laut C-Standard musst du auch damit rechnen, dass ein char 43 Bits hat,
> davon aber 13 Bits nicht zum Wert gehören.

Nicht ganz. Bei char müssen alle Bits zum Wert beitragen.

> Ein Zeiger ist dann 7 chars breit und ein Funktionszeiger darf auch 5mal
> so groß sein, wie ein Datenzeiger. Außerdem sind char, short, int und
> long alle gleichbreit.
>
> Und? Das wäre eine vollkommen Standard-C-Konforme Umgebung.

Ja. Wenn das auf der Zielplattform sinnvoll wäre, warum auch nicht?

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren