C++ Core Guidelines: Regeln für Expressions

Entwickler sollten keine komplizierten Ausdrücke anwenden, die Prioritäten für arithmetische oder logische Ausdrücke und die Reihenfolge der Auswertung von Ausdrücken kennen. Wird eine falsche Priorität für Ausdrücke oder Auswertungsreihenfolge von Ausdrücken verwendet, die schlicht falsch oder nicht garantiert ist, dann lauert undefiniertes Verhalten.

In Pocket speichern vorlesen Druckansicht 47 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm

Entwickler sollten keine komplizierten Ausdrücke anwenden, die Prioritäten für arithmetische oder logische Ausdrücke und die Reihenfolge der Auswertung von Ausdrücken kennen. Wird eine falsche Priorität für Ausdrücke oder Auswertungsreihenfolge von Ausdrücken verwendet, die schlicht falsch oder nicht garantiert ist, dann lauert undefiniertes Verhalten.

Hier sind die vier Regeln für heute.

Die Regeln zur Priorität und zur Auswertungsreihenfolge von Ausdrücken sind nicht so einfach, wie sie vielleicht klingen. Zusätzlich haben sie sich mit C++17 geändert. Daher beginnt dieser Artikel sehr behutsam.

ES.40: Avoid complicated expressions

Was bedeutet kompliziert? Hier ist das ursprüngliche Beispiel aus den Guidelines:

// bad: assignment hidden in subexpression                      (1)
while ((c = getc()) != -1)

// bad: two non-local variables assigned in a sub-expressions (1)
while ((cin >> c1, cin >> c2), c1 == c2)

// better, but possibly still too complicated (1)
for (char c1, c2; cin >> c1 >> c2 && c1 == c2;)

// OK: if i and j are not aliased (2)
int x = ++i + ++j;

// OK: if i != j and i != k (2)
v[i] = v[j] + v[k];

// bad: multiple assignments "hidden" in subexpressions (1)
x = a + (b = f()) + (c = g()) * 7;

// bad: relies on commonly misunderstood precedence rules (1)
x = a & b + c * d && e ^ f == 7;

// bad: undefined behavior (3)
x = x++ + x++ + ++x;

Ich habe ein paar (Zahlen) hinzugefügt. Zuerst einmal gilt, dass alle Ausdrücke, die die Ziffer (1) haben, schlechten Stil darstellen und ein Code-Review nicht überstehen sollten. Weißt du zum Beispiel, was hier passiert: x = a & b + c * d && e ^ f == 7. Klar, du musst die Regeln für die Priorität von Operationen nachschauen. Ich werde mich auf diese in der nächsten Regel beziehen.

Die Ausdrücke (2) sind dann richtig, wenn folgende Bedingungen gelten: i und j müssen verschieden und die Indizes i,j und i,j müssen paarweise verschieden sein.

(3) stellt undefiniertes Verhalten dar, denn es ist nicht definiert, in welcher Reihen x ausgewertet wird. Warum? Der Grund ist das abschließende Semikolonzeichen. ";" ist ein Sequenzpunkt und für diesen gilt die Garantie, dass alle Effekte von Ausdrücken vor diesem bereits stattgefunden haben müssen.

Mit C++17 haben sie die Regeln für die Priorität von Operatoren geändert: Es gilt "links nach rechts" für Ausdrücke mit der Ausnahme von Zuweisungen. Für diese gilt "rechts nach links". In der Regel ES.43 werde ich auf die zugesicherte Prioritäten von Operatoren in C++17 genauer eingehen.

ES.41: If in doubt about operator precedence, parenthesize

Einerseits sagen die Guidelines: Falls du dir bei über die Priorität von Operatoren nicht sicher bist, verwende Klammern (1). Anderseits sagen siee: Du solltest dich gut genug mit den Prioritäten von Operatoren auskennen, um hier (2) keine Klammern zu benötigen.

const unsigned int flag = 2;
unsigned int a = flag;

if (a & flag != 0) // bad: means a&(flag != 0) (1)

if (a < 0 || a <= max) { // good: quite obvious (2)
// ...
}

Okay. Für einen Experten mag der Ausdruck (1) eine Selbstverständlichkeit sein, für einen Anfänger der Ausdruck (2) eine Herausforderung.

Mir fallen nur zwei Tipps zu dieser Regeln ein.

  1. Falls du Zweifel bei der Priorität von Operatoren hast, verwende Klammer. Denke dabei aber an die Anfänger.
  2. Behalte die Tabelle auf cppreference.com zu den Prioritäten der Operatoren unter deinem Kissen.

Ich springe gleich zu den Regeln ES.43 und ES.44 und werde mich mit der Regel ES.42 erst im nächsten Artikel befassen. Mit C++17 hat sich die Reihenfolge der Auswertung von Ausdrücken geändert.

ES.43: Avoid expressions with undefined order of evaluation

In C++14 besitzt der folgende Ausdruck undefiniertes Verhalten:

v[i] = ++i;   //  the result is undefined

Das gilt aber nicht für C++17. Mit C++17 ist die Auswertungsreihenfolge des kleinen Codeschnipsels von "rechts nach links" garantiert; damit verfügt das Programm über wohldefiniertes Verhalten.

Welche Zusicherungen bietet C++17 noch an?

  1. Postfix-Ausdrücke werden von links nach rechts ausgewertet. Dies trifft auch auf Funktionsaufrufe und Zugriffe auf Klassenmitglieder.
  2. Zuweisungsausdrücke werden von rechts nach links ausgewertet. Dies schließt die zusammengesetzten Zuweisungen mit ein.
  3. Die Operatoren von Shift-Operationen werden von links nach rechts ausgewertet.

Dies war der sinngemäße Wortlaut des ursprünglichen Proposals. Dazu gibt es noch ein paar Beispiele:

a.b
a->b
a->*b
a(b1, b2, b3) // (1)
b @= a
a[b]
a << b
a >> b

Wie sollten die Beispiele gelesen werden? Sehr einfach. Jeder Ausdruck wird in der Reihenfolge a, dann b, dann c und dann d ausgewertet.

Der Ausdruck (1) ist ein wenig trickreicher. Mit C++17 bekommen wir nur die Garantie, dass die Funktion vor ihren Argumenten evaluiert wird, aber die Auswertungsreihenfolge der Argumente ist immer noch nicht spezifiziert.

Mir ist klar. Der letzte Satz war nicht sehr leicht verdaulich. Daher werde ich ein wenig ausholen.

ES.44: Don’t depend on order of evaluation of function arguments

In den letzten Jahre habe ich sehr viele Fehler gesehen, denn viele Programmierer nahmen irrtümlich an, dass die Argumente einer Funktion von links nach rechts ausgewertet werden. Falsch! Es gibt keine Zusicherungen:

#include <iostream>

void func(int fir, int sec){
std::cout << "(" << fir << "," << sec << ")" << std::endl;
}

int main(){
int i = 0;
func(i++, i++);
}

Hier ist der Beweis. Die Ausgaben von GCC und Clang unterscheidet sich.

GCC

Clang

Mit C++17 ändert sich das Verhalten nicht. Die Auswertungsreihenfolge der Funktionsargumente ist nicht spezifiziert. Aber zumindestens ist die Auswertungsreihenfolge der folgenden Ausdrücke mit C++17 vorgegeben:

f1()->m(f2());          // evaluation left to right  (1)
cout << f1() << f2(); // evaluation left to right (2)

f1() = f(2); // evaluation right to left (3)

Den Begründung liefere ich gleich mit:

(1): Postfix-Ausdrücke werden von links nach rechts evaluiert. Dies schließt Funktionsaufrufe mit ein.

(2): Die Operanden von Shift-Operatoren werden von links nach rechts evaluiert.

(3): Ausdrucke mit Zuweisungen werden von rechts nach links evaluiert.

Ein Punkt muss ich aber noch ganz explizit betonen. Mit C++14 besitzen die drei letzten Ausdrücke undefiniertes Verhalten.

Zugegeben, das war ein herausfordernder Artikel. Dies ist aber eine Herausforderung, die jeder Programmierer auf seinem Weg zu einem guten Programmierer meistern muss. Das zentrale Thema meines nächsten Artikels werden die Konvertierungsoperatoren in C++ sein. ()