Warum ist a = (a+b) – (b=a) eine schlechte Wahl, um zwei ganze Zahlen zu vertauschen?

Lesezeit: 10 Minuten

Benutzeravatar von ashfaque
ashfaque

Ich bin auf diesen Code gestoßen, um zwei Ganzzahlen auszutauschen, ohne eine temporäre Variable oder die Verwendung bitweiser Operatoren zu verwenden.

int main(){

    int a=2,b=3;
    printf("a=%d,b=%d",a,b);
    a=(a+b)-(b=a);
    printf("\na=%d,b=%d",a,b);
    return 0;
}

Aber ich denke, dieser Code hat ein undefiniertes Verhalten in der Swap-Anweisung a = (a+b) - (b=a); da es keine enthält Sequenzpunkte um die Reihenfolge der Auswertung festzulegen.

Meine Frage ist: Ist dies eine akzeptable Lösung, um zwei Ganzzahlen auszutauschen?

  • Verwenden Sie in C++ einfach std::swap

    – Benutzer1781290

    27. Dezember 2013 um 12:27 Uhr

  • Warum für ein paar Bytes schwer lesbaren Code schreiben?

    – Ed heilen

    27. Dezember 2013 um 12:27 Uhr

  • @EdHeal Byte für Byte wird das Megabyte gemacht (konnte nicht widerstehen)

    – Suvarna Pattayil

    27. Dezember 2013 um 12:31 Uhr

  • @EdHeal “um ein paar Bytes willen” klingt wie das Filmklischee “um ein paar Leben willen”. Wenn Sie es aus dieser Perspektive betrachten, klingen Sie wie ein Monster. |=^]

    – Aber ich bin keine Wrapper-Klasse

    27. Dezember 2013 um 16:50 Uhr

  • Denken Sie daran, dass die eingesparten “Codezeilen” und “Variablen” die Leistung Ihres Programms nicht beschleunigen … im Gegenteil, sie schaden ihm wahrscheinlich. Ich bin bereit zu wetten, wenn Sie sich das verknüpfte ASM ansehen, dass wahrscheinlich mehr Anweisungen und mehr Zyklen zur Ausführung erforderlich sind als eine naive Lösung.

    – Nik T

    27. Dezember 2013 um 16:50 Uhr

Benutzeravatar von hackks
hackt

Nein. Dies ist nicht akzeptabel. Dieser Code ruft auf Undefiniertes Verhalten. Dies liegt an der Operation b ist nicht definiert. Im Ausdruck

a=(a+b)-(b=a);  

es ist nicht sicher, ob b wird zuerst modifiziert oder sein Wert wird im Ausdruck verwendet (a+b) wegen des Fehlens der Sequenzpunkt.
Sehen Sie, welche Standard-Syas:

C11: 6.5 Ausdrücke:

Wenn eine Nebenwirkung auf ein skalares Objekt relativ zu entweder einer anderen Nebenwirkung auf dasselbe skalare Objekt oder nicht sequenziert ist eine Wertberechnung unter Verwendung des Werts desselben skalaren Objektsist das Verhalten undefiniert. Wenn es mehrere zulässige Reihenfolgen der Unterausdrücke eines Ausdrucks gibt, ist das Verhalten undefiniert, wenn ein solcher nicht sequenzierter Nebeneffekt in einer der Reihenfolgen auftritt.84)1.

Lesen C-FAQ-3.8 und diese Antwort für eine detailliertere Erklärung des Sequenzpunkts und des undefinierten Verhaltens.


1. Die Betonung liegt bei mir.

  • @LightnessRacesinOrbit; Was sollte bewiesen werden, um dies zu erklären? OP war sich nicht sicher, ob es akzeptabel ist oder nicht, und ich habe das klargestellt.

    – Hacken

    27. Dezember 2013 um 16:00 Uhr


  • Das, was du sagst, ist wahr. Im Moment ist es nur eine Behauptung ohne Beweise. Normalerweise zitieren wir den Standard für solche Dinge.

    – Leichtigkeitsrennen im Orbit

    27. Dezember 2013 um 16:06 Uhr

  • @mafso; NEIN. Es ist ein undefiniertes Verhalten. Warte ab. Ich werde Standard zitieren.

    – Hacken

    27. Dezember 2013 um 16:10 Uhr


  • @JackM: Der Compiler, das Datum und die Uhrzeit, die Raumtemperatur, die Bits, die beim Programmstart zufällig im Datensegment 0xFC0143FC liegen, der Name Ihres Hundes..

    – Leichtigkeitsrennen im Orbit

    27. Dezember 2013 um 18:36 Uhr

  • Ein Problem bei dieser Antwort ist, dass das Standardzitat das neue Konzept von verwendet Sequenzierungwährend die Frage und die Antwort über das alte Konzept von sprechen Sequenzpunkte. Sequenzierung ersetzt Sequenzpunkte. Und Sequenzierung ist ganz anders als Sequenzpunkte. Tatsächlich wurden einige ehemals undefinierte Ausdrücke nach der Umstellung auf das neue Konzept perfekt definiert, insbesondere diejenigen, die den Zuweisungsoperator in C++ verwenden. Es ist wahr, dass dieser Ausdruck unter beiden undefiniert ist, aber die Mischung kann dennoch verwirrend sein.

    – AnT steht zu Russland

    27. Dezember 2013 um 18:51 Uhr


Benutzeravatar von Eric Lippert
Erich Lippert

Meine Frage ist: Ist dies eine akzeptable Lösung, um zwei Ganzzahlen auszutauschen?

Akzeptabel für wen? Wenn Sie fragen, ob es für mich akzeptabel ist, würde das an keiner Codeüberprüfung vorbeikommen, an der ich teilgenommen habe, glauben Sie mir.

Warum ist a=(a+b)-(b=a) eine schlechte Wahl, um zwei ganze Zahlen auszutauschen?

Aus den folgenden Gründen:

1) Wie Sie bemerken, gibt es in C keine Garantie dafür, dass es dies tatsächlich tut. Es könnte alles tun.

2) Nehmen wir aus Gründen der Argumentation an, dass es wirklich zwei Ganzzahlen vertauscht, wie es in C# der Fall ist. (C# garantiert, dass Nebeneffekte von links nach rechts auftreten.) Der Code wäre immer noch inakzeptabel, da seine Bedeutung völlig unklar ist! Code sollte kein Haufen cleverer Tricks sein. Schreiben Sie Code für die Person, die nach Ihnen kommt, die ihn lesen und verstehen muss.

3) Angenommen, es funktioniert. Der Code ist immer noch nicht akzeptabel, weil dies einfach falsch ist:

Ich bin auf diesen Code gestoßen, um zwei Ganzzahlen auszutauschen, ohne eine temporäre Variable oder die Verwendung bitweiser Operatoren zu verwenden.

Das ist einfach falsch. Dieser Trick verwendet eine temporäre Variable, um die Berechnung von zu speichern a+b. Die Variable wird vom Compiler für Sie generiert und hat keinen Namen, aber sie ist da. Wenn das Ziel darin besteht, Provisorien zu eliminieren, wird es dadurch schlimmer, nicht besser! Und warum wollen Sie überhaupt auf Provisorien verzichten? Sie sind billig!

4) Dies funktioniert nur für ganze Zahlen. Viele Dinge müssen außer ganzen Zahlen getauscht werden.

Kurz gesagt, verbringen Sie Ihre Zeit damit, sich darauf zu konzentrieren, Code zu schreiben, der offensichtlich korrekt ist, anstatt zu versuchen, clevere Tricks zu entwickeln, die die Dinge tatsächlich verschlimmern.

Jonis Benutzeravatar
Joni

Es gibt mindestens zwei Probleme mit a=(a+b)-(b=a).

Eine erwähnen Sie selbst: Das Fehlen von Sequenzpunkten bedeutet, dass das Verhalten undefiniert ist. Als solches könnte überhaupt alles passieren. Beispielsweise gibt es keine Garantie dafür, was zuerst ausgewertet wird: a+b oder b=a. Der Compiler kann sich dafür entscheiden, zuerst Code für die Zuweisung zu generieren oder etwas völlig anderes zu tun.

Ein weiteres Problem ist die Tatsache, dass der Überlauf der vorzeichenbehafteten Arithmetik ein undefiniertes Verhalten ist. Wenn a+b Überläufe gibt es keine Garantie für die Ergebnisse; sogar eine Ausnahme könnte geworfen werden.

  • Das Fehlen von Sequenzpunkten bedeutet, dass dies UB ist. Es gibt keinen Grund, nur eines von zwei möglichen Ergebnissen zu erwarten. Das Ergebnis könnte zB von dem Wert vorher in irgendeinem Scratch-Register abhängen.

    – Christoph Creutzig

    27. Dezember 2013 um 18:00 Uhr

  • @mafso: Falsch. Und falsches Zitat. Das von Ihnen bereitgestellte Angebot gilt für ganzzahlige arithmetische Umwandlungen nur, nicht auf vorzeichenbehaftete Arithmetik im Allgemeinen. So war es immer mit Konvertierungen. Währenddessen überlaufen vorzeichenbehaftete ganzzahlige Arithmetik löst undefiniertes Verhalten aus. C11 hat diesbezüglich keine Änderungen vorgenommen. Die obige Antwort ist absolut richtig. 6.5/5 „Wenn während der Auswertung eines Ausdrucks eine außergewöhnliche Bedingung eintritt (das heißt, wenn das Ergebnis nicht mathematisch definiert ist oder nicht im Bereich darstellbarer Werte für seinen Typ liegt), ist das Verhalten undefiniert.“

    – AnT steht zu Russland

    27. Dezember 2013 um 19:01 Uhr


  • @Joni: Überlauf während der vorzeichenbehafteten Ganzzahlarithmetik verursacht undefiniertes Verhalten in C und C++. Darin sind sich beide Sprachen einig. Der Kommentar von Mafso ist falsch. Das Zitat in diesem Kommentar stammt aus einem irrelevanten Teil der Sprachspezifikation.

    – AnT steht zu Russland

    27. Dezember 2013 um 19:06 Uhr

  • Du hattest Recht… es ist UB, zumindest in C nicht implementierungsdefiniert. Entschuldigung.

    – Mafso

    27. Dezember 2013 um 19:11 Uhr

  • Danke dafür @AndreyT, ich vermutete, dass das der Fall sein würde, nachdem ich es gesehen hatte Ein Beispiel für undefiniertes Verhalten ist das Verhalten bei Ganzzahlüberlauf in C11 3.4.3, konnte aber die richtige Stelle zum Zitieren nicht finden

    – Joni

    27. Dezember 2013 um 19:14 Uhr

Benutzeravatar von jcoder
Coder

Abgesehen von den anderen Antworten zu undefiniertem Verhalten und Stil kann der Compiler, wenn Sie einfachen Code schreiben, der nur eine temporäre Variable verwendet, wahrscheinlich die Werte verfolgen und sie nicht tatsächlich im generierten Code austauschen und die ausgetauschten Werte später in einigen verwenden Fälle. Das geht mit deinem Code nicht. Der Compiler ist in der Regel besser als Sie bei Mikrooptimierungen.

Es ist also wahrscheinlich, dass Ihr Code langsamer, schwerer zu verstehen und wahrscheinlich auch unzuverlässiges undefiniertes Verhalten aufweist.

Benutzeravatar von Olaf Dietsche
Olaf Dietsch

Wenn Sie gcc und -Wall Der Compiler warnt Sie bereits

ac:3:26: Warnung: Operation auf ‘b’ kann undefiniert sein [-Wsequence-point]

Ob ein solches Konstrukt verwendet werden soll, ist auch unter Leistungsgesichtspunkten umstritten. Beim Anschauen

void swap1(int *a, int *b)
{
    *a = (*a + *b) - (*b = *a);
}

void swap2(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

und untersuchen Sie den Assemblercode

swap1:
.LFB0:
    .cfi_startproc
    movl    (%rdi), %edx
    movl    (%rsi), %eax
    movl    %edx, (%rsi)
    movl    %eax, (%rdi)
    ret
    .cfi_endproc

swap2:
.LFB1:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

Sie sehen keinen Vorteil darin, den Code zu verschleiern.


Betrachten Sie den C++ (g++) Code, der im Grunde dasselbe tut, aber dauert move berücksichtigen

#include <algorithm>

void swap3(int *a, int *b)
{
    std::swap(*a, *b);
}

gibt identische Assembly-Ausgabe

_Z5swap3PiS_:
.LFB417:
    .cfi_startproc
    movl    (%rdi), %eax
    movl    (%rsi), %edx
    movl    %edx, (%rdi)
    movl    %eax, (%rsi)
    ret
    .cfi_endproc

Wenn man die Warnung von gcc berücksichtigt und keinen technischen Vorteil sieht, würde ich sagen, bleiben Sie bei Standardtechniken. Wenn dies jemals zu einem Engpass wird, können Sie immer noch untersuchen, wie Sie dieses kleine Stück Code verbessern oder vermeiden können.

  • Verschleierung ist seine eigene Belohnung.

    – Edward Seltsam

    28. Dezember 2013 um 11:30 Uhr

Benutzeravatar von ouah
ouah

Die Aussage:

a=(a+b)-(b=a);

ruft undefiniertes Verhalten auf. Der zweite Satz im zitierten Absatz wird verletzt:

(C99, 6.5p2) „Zwischen dem vorherigen und dem nächsten Sequenzpunkt darf ein Objekt seinen gespeicherten Wert höchstens einmal durch die Auswertung eines Ausdrucks ändern. Außerdem soll der vorherige Wert nur gelesen werden, um den zu speichernden Wert zu bestimmen.

  • Verschleierung ist seine eigene Belohnung.

    – Edward Seltsam

    28. Dezember 2013 um 11:30 Uhr

Benutzeravatar der Community
Gemeinschaft

Eine Frage wurde bereits 2010 mit genau demselben Beispiel gepostet.

a = (a+b) - (b=a);

Steve Jessop warnt davor:

Das Verhalten dieses Codes ist übrigens undefiniert. Sowohl a als auch b werden ohne dazwischenliegenden Sequenzpunkt gelesen und geschrieben. Für den Anfang hätte der Compiler durchaus das Recht, b=a auszuwerten, bevor er a+b auswertet.

Hier ist eine Erklärung aus einer Frage, die 2012 gepostet wurde. Beachten Sie, dass das Beispiel dies nicht ist exakt das gleiche wegen des Fehlens von Klammern, aber die Antwort ist trotzdem relevant.

In C++ haben Teilausdrücke in arithmetischen Ausdrücken keine zeitliche Reihenfolge.

a = x + y;

Wird x zuerst ausgewertet oder y? Der Compiler kann entweder wählen, oder er kann etwas völlig anderes wählen. Die Reihenfolge der Auswertung ist nicht dasselbe wie die Operatorpriorität: Die Operatorpriorität ist streng definiert, und Die Reihenfolge der Auswertung ist nur in der Granularität definiert, in der Ihr Programm Sequenzpunkte hat.

Tatsächlich ist es auf einigen Architekturen möglich, Code auszugeben, der sowohl x als auch y gleichzeitig auswertet – zum Beispiel VLIW-Architekturen.

Jetzt für C11-Standardzitate von N1570:

Anlage J.1/1

Es handelt sich um nicht spezifiziertes Verhalten, wenn:

— Die Reihenfolge, in der Teilausdrücke ausgewertet werden, und die Reihenfolge, in der Seiteneffekte auftreten, außer wie für den Funktionsaufruf angegeben (), &&,
||, ? :und Kommaoperatoren (6.5).

— Die Reihenfolge, in der die Operanden eines Zuweisungsoperators ausgewertet werden (6.5.16).

Anlage J.2/1

Es ist ein undefiniertes Verhalten, wenn:

— Eine Nebenwirkung auf ein Skalarobjekt ist relativ entweder zu einer anderen Nebenwirkung auf dasselbe Skalarobjekt oder zu einer Wertberechnung unter Verwendung des Werts desselben Skalarobjekts (6.5) nicht sequenziert.

6,5/1

Ein Ausdruck ist eine Folge von Operatoren und Operanden, die die Berechnung eines Werts angibt oder ein Objekt oder eine Funktion bezeichnet oder Nebeneffekte erzeugt oder eine Kombination davon ausführt. Die Wertberechnungen der Operanden eines Operators werden vor der Wertberechnung des Ergebnisses des Operators sequenziert.

6,5/2

Wenn eine Nebenwirkung auf ein skalares Objekt relativ zu einer anderen Nebenwirkung auf dasselbe skalare Objekt oder einer Wertberechnung unter Verwendung des Werts desselben skalaren Objekts nicht sequenziert ist, ist das Verhalten undefiniert. Wenn es mehrere zulässige Reihenfolgen der Unterausdrücke eines Ausdrucks gibt, ist das Verhalten undefiniert, wenn ein solcher nicht sequenzierter Nebeneffekt in einer der Reihenfolgen auftritt.84)

6,5/3

Die Gruppierung von Operatoren und Operanden wird durch die Syntax angegeben.85) Außer wie später angegeben, sind Seiteneffekte und Wertberechnungen von Teilausdrücken unsequenzt.86)

Sie sollten sich nicht auf undefiniertes Verhalten verlassen.

Einige Alternativen: In C++ können Sie verwenden

  std::swap(a, b);

XOR-Swap:

  a = a^b;
  b = a^b;
  a = a^b;

1412470cookie-checkWarum ist a = (a+b) – (b=a) eine schlechte Wahl, um zwei ganze Zahlen zu vertauschen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy