C++ Core Guidelines: Regeln für Ausdrücke und Anweisungen

Modernes C++  –  11 Kommentare

Es gibt relativ viele Regeln für Ausdrücke und Anweisungen in den C++ Core Guidelines. Um genau zu sein, mehr als 50 Regeln beschäftigen sich mit Deklarationen, Ausdrücken, Anweisungen und arithmetischen Ausdrücken.

Ich vergaß, zwei Regeln zu erwähnen, die schlicht general genannt werden. Mit diesen beginnt dieser Artikel.

ES.1: Prefer the standard library to other libraries and to "handcrafted code"

Es gibt keinen Grund, die Summe eines Vektors von Fließkommazahlen mit einer expliziten Schleife zu berechnen:

int max = v.size();             // bad: verbose, purpose unstated
double sum = 0.0;
for (int i = 0; i < max; ++i)
sum = sum + v[i];

Verwende einfach den std::accumulate-Algorithmus der STL:

auto sum = std::accumulate(begin(a), end(a), 0.0); // good

Diese Regel erinnert mich an einen Satz von Sean Parent auf der CppCon 2013: "If you want to improve the code quality in your organization, replace all your coding guidelines with one goal: No raw loops!"

Oder um es ein wenig direkter auszudrücken: Falls du eine nackte Schleife verwendest, kennst du vermutlich die Algorithmen der STL nicht gut genug.

ES.2: Prefer suitable abstractions to direct use of language features

Das nächste Déjà vu. In einem meiner letzten C++-Seminare gab es eine lange Diskussion, gefolgt von einer noch längeren Analyse zu ein paar ziemlich cleveren Funktionen für das Lesen und Schreiben eines strstreams. Die Teilnehmer mussten diese Funktionen verstehen und erweitern, um ihren Legacy-Code zu pflegen. Ihr Kampf ging schon in die zweite Woche.

Ihr größtes Hindernis, die bestehende Funktionen zu verstehen, bestand darin, dass diese nicht die richtige Abstraktion verwendeten.

Betrachte die selbst gestrickte Funktion für das Lesen eines std::istream:

char** read1(istream& is, int maxelem, int maxstring, int* nread)   // bad: verbose and incomplete
{
auto res = new char*[maxelem];
int elemcount = 0;
while (is && elemcount < maxelem) {
auto s = new char[maxstring];
is.read(s, maxstring);
res[elemcount++] = s;
}
nread = &elemcount;
return res;
}

Im Gegensatz zu der schwer verdaulichen Funktion read1 ist die Funktion read2 deutlich bekömmlicher:

vector<string> read2(istream& is)   // good
{
vector<string> res;
for (string s; is >> s;)
res.push_back(s);
return res;
}

Die richtige Abstraktionen bedeutet öfters, dass du dir keine Gedanken zu den Besitzverhältnissen wie in der Funktion read2 zu machen brauchst. Das gilt aber nicht für die Funktion read1. Der Aufrufer der Funktion read1 ist der Besitzer von result und muss es konsequenterweise auch löschen.

Eine Deklaration führt einen Namen in einen Bereich ein. Um ehrlich zu sein, ich bin zwiegespalten. Einerseits könnten die folgenden Regeln ein wenig langweilig für den Leser sein, denn sie sind recht offensichtlich. Anderseits kenne ich viele Codebasen, die diese Regeln permanent brechen. Zum Beispiel hatte ich erst vor kurzem die Diskussion mit einem ehemaligen Fortran-Programmierer, der behauptete, dass jeder Variablenname aus genau drei Buchstaben bestehen solle.

Egal, ich werde die nächsten Regeln vorstellen, den gute Namen sind der entscheidende Grund, damit Code lesbar, verständlich, pflegbar, erweiterbar ist und bleibt.

Hier sind die ersten sechs Regeln.

ES.5: Keep scopes small

Halte Bereiche so klein, dass sie auf den Bildschirm passen. Damit erhältst du sofort eine Idee, was in dem Code steckt. Falls der Bereich zu groß wird, solltest du deinen Code in Funktionen und Objekte strukturieren. Identifiziere dazu logische Einheiten und verwende selbsterklärende Funktionsname in deiner Refaktorisierung. Danach ist es wieder deutlich einfacher, den Code zu verstehen.

ES.6: Declare names in for-statement initializers and conditions to limit scope

Seit dem ersten C++-Standard können wir Variablen direkt in einer for-Schleife deklarieren. Mit C++17 können wir das sogar in einer if- oder switch-Anweisung:

std::map<int,std::string> myMap;

if (auto result = myMap.insert(value); result.second){ // (1)
useResult(result.first);
// ...
}
else{
// ...
} // result is automatically destroyed // (2)

Die Variable result (1) ist nur im Bereich des if- und else-Zweigs der if-Anweisung gültig. result verschmutzt nicht den umgebenden Bereich und wird automatisch zerstört (2). Das war vor C++17 nicht möglich. result musste in diesem Fall im umgebenden Bereich deklariert werden (3):

std::map<int,std::string> myMap;
auto result = myMap.insert(value) // (3)
if (result.second){
useResult(result.first);
// ...
}
else{
// ...
}

ES.7: Keep common and local names short, and keep uncommon and nonlocal names longer

Obwohl sich diese Regel zuerst einmal seltsam anhört, wenden wir sie schon lange an. Indem du eine Variable i oder j nennst oder ihr den Namen T gibst, machst du deine Absicht sofort klar: i und j sind Indizes, und T ist ein Typ-Parameter eines Templates:

template<typename T>    // good
void print(ostream& os, const vector<T>& v)
{
for (int i = 0; i < v.size(); ++i)
os << v[i] << '\n';
}

Hinter dieser Regel verbirgt sich eine Metaregel. Ein Name sollte selbsterklärend sein. In einem kleinen Bereich ist auf einen Blick ersichtlich, für was der Name steht. Das gilt aber nicht automatisch für einen größeren Kontext, der mehrere Bereiche umfasst. Daher werden diese Variablennamen länger sein.

ES.8: Avoid similar-looking names

Kannst du das Beispiel lesen, ohne mit der Wimper zu zucken?

if (readable(i1 + l1 + ol + o1 + o0 + ol + o1 + I0 + l0)) surprise();

Um ehrlich zu sein, ich habe öfters Probleme mit der Zahl 0 und dem großen Buchstaben O. Abhängig vom verwendeten Font können die beiden Zeichen sehr ähnlich aussehen. Vor kurzer Zeit benötigte ich mehrere Versuche, mich in einen Server einzuloggen. Das automatisch erzeugte Passwort enthielt das Zeichen O.

ES.9: Avoid ALL_CAPS names

Falls du All_CAPS verwendest, besteht immer die Gefahr von Makrosubstitutionen, da ALL_CAPS typischerweise für Makros verwendet werden. Daher ist das folgende Programmschnipsel immer für eine Überraschung gut:

// somewhere in some header:
#define NE !=

// somewhere else in some other header:
enum Coord { N, NE, NW, S, SE, SW, E, W };

// somewhere third in some poor programmer's .cpp:
switch (direction) {
case N:
// ...
case NE:
// ...
// ...
}

ES.10: Declare one name (only) per declaration

Hier kommt ein Beispiel. Findest du die zwei Probleme?

char* p, p2;
char a = 'a';
p = &a;
p2 = a; // (1)

int a = 7, b = 9, c, d = 10, e = 3; // (2)

p2 ist kein Zeiger, aber ein char (1) und c ist nicht initialisiert (2).

Mit C++11 erhalten wir eine Ausnahme der Regeln: strukturierte Bindung. Damit lässt sich die if-Anweisung mit Initialisierer aus der Regel ES.6 sauberer und lesbarer schreiben:

std::map<int,std::string> myMap;

if (auto [iter, succeeded] = myMap.insert(value); succedded){ // (1)
useResult(iter);
// ...
}
else{
// ...
} // iter and succeeded are automatically destroyed // (2)

Es gibt noch einige Regeln zur Deklaration von Namen. Genau von diesen handelt mein nächster Artikel.