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.
- 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.
- ES.40: Avoid complicated expressions
- ES.41: If in doubt about operator precedence, parenthesize
- ES.43: Avoid expressions with undefined order of evaluation
- ES.44: Don’t depend on order of evaluation of function arguments
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.
- Falls du Zweifel bei der Priorität von Operatoren hast, verwende Klammer. Denke dabei aber an die Anfänger.
- 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?
- Postfix-AusdrĂĽcke werden von links nach rechts ausgewertet. Dies trifft auch auf Funktionsaufrufe und Zugriffe auf Klassenmitglieder.
- ZuweisungsausdrĂĽcke werden von rechts nach links ausgewertet. Dies schlieĂźt die zusammengesetzten Zuweisungen mit ein.
- 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.
Wie geht's weiter?
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. ()