C++ Core Guidelines: Nichtregeln und Mythen

Modernes C++  –  8 Kommentare

Natürlich kennst du schon viele Nichtregeln und Mythen zu C++. Nichtregeln und Mythen, die wir widerlegen müssen, wenn wir modernes C++ programmieren wollen. Die unterstützenden Bereiche der C++ Core Guidelines beschäftigen sich mit den widerspenstigen Nichtregeln und Mythen, bieten aber auch Alternativen an.

Dies sind die vier Regeln für den heutigen Artikel.

Viele Programmierer wenden die erste Regel an.

NR.1: Don’t: All declarations should be at the top of a function

Die Regel ist ein Relikt alter Programmiersprachen, die die Initialisierung von Variablen und Konstanten nach einer Anweisung nicht erlauben. Das Ergebnis einer signifikanten Trennung der Variablendeklaration von ihrer Verwendung ist gerne, dass die Variable nicht initialisiert verwendet wird. Genau das passiert in dem Beispiel der C++ Core Guidelines:

int use(int x)
{
int i;
char c;
double d;
// ... some stuff ...
if (x < i) {
// ...
i = f(x, d);
}
if (i < x) {
// ...
i = g(x, c);
}
return ;
}

Ich denke, du hast das Problem in dem Codeschnipsel bereits gefunden. Die Variable i (die gleiche Argumentation gilt für c und d) wird nicht initialisiert, da sie eine Built-in-Variable ist, die in einem lokalen Bereich verwendet wird. Damit besitzt das Programm undefiniertes Verhalten. Wenn i hingegen eine benutzerdefinierte Variable wie ein std::string ist, würde diese initialisiert werden. Nun ist die Frage, was du tun sollst.

  • Deklariere die Variable i direkt vor ihrer Verwendung.
  • Initialisiere ein Variable immer zum Beispiel mit int i{} oder noch besser mit auto. Der Compiler kann aus einer Deklaration wie auto i den Typ von i nicht erraten und in diesem Fall das Programm nicht übersetzen. Andersherum gesagt, bedeutet dies, dass die Verwendung von auto dich dazu zwingt Variablen zu initialisieren.

Ich kenne bereits die nächste Regel aus häufigen Diskussionen.

NR.2: Don’t: Have only a single return-statement in a function

Setzt du diese Regel um, setzt du implizit die erste Nicht-Regel ein.

template<class T>
std::string sign(T x) // bad
{
std::string res;
if (x < 0)
res = "negative";
else if (x > 0)
res = "positive";
else
res = "zero";
return res;
}

Dank mehrerer Rückgabeanweisungen wird der Code einfacher zu lesen und schneller.

template<class T>
std::string sign(T x)
{
if (x < 0)
return "negative";
else if (x > 0)
return "positive";
return "zero";
}

Was passiert, wenn ich die automatische Ermittlung des Rückgabetyps mit verschiedenen Rückgabetypen anwende?

// differentReturnTypes.cpp

template <typename T>
auto getValue(T x){
if (x < 0) // int
return -1;
else if (x > 0)
return 1.0; // double
else return 0.0f; // float
}

int main(){

getValue(5.5);

}

Wie vermutet, führt dies zu einem Fehler.

Vermutlich ist die nächste Nichtregel die, die am kontroversesten diskutiert wird.

NR.3: Don’t: Don’t use exceptions

Zuerst einmal, zählen die Guidelines die gewichtigsten Argumente gegen Ausnahmen auf.

  1. exceptions are inefficient
  2. exceptions lead to leaks and errors
  3. exception performance is not predictable

Die Guidelines antworten mit schwergewichtigen Argumenten auf diese Behauptungen.

1. Oft wird die Effizienz der Ausnahmebehandlung mit einem Programm verglichen, dass sich schlicht beendet oder den Fehlercode darstellt. Viele Implementierungen der Ausnahmebehandlung sind auch nicht besonders gut. Natürlich macht der Vergleich bei all diesen Punkten keinen Sinn. Daher möchte ich auf das Dokument Technical Report on C++ Performance (TR18015.pdf) verweisen, das zwei typische Arten vorstellt, wie Ausnahmebehandlung implementiert ist.

  1. Der Codeansatz, in der Code mit jedem try-Block assoziiert ist.
  2. Der Tabellenansatz, der vom Compiler erzeugte statische Tabellen verwendet.

Vereinfachend gesagt, besitzt der Codeansatz den Nachteil, dass selbst dann die Buchhaltung der Ausnahmebehandlung durchgeführt werden muss, wenn keine Ausnahme auftritt. Das bedeutet natürlich, dass der Stack größer und die Applikation langsamer wird. Dieser Nachteil gilt nicht für den Tabellenansatz, denn dieser impliziert keine zusätzlichen Kosten für den Stack oder die Laufzeit des Programms, wenn keine Ausnahmen auftreten. Im Gegensatz dazu ist der Tabellenansatz komplizierter zu implementieren und die statischen Tabellen können relativ groß werden.

2. Zum Punkt 2 habe ich nichts hinzuzufügen. Ausnahmen können nicht dafür getadelt werden, falls das Programm keine Strategie für Ressourcenmanagement besitzt.

3. Wenn du harte Echtzeitbedingungen umzusetzen hast, sodass eine späte Antwort eine falsche Antwort ist, wird eine Ausnahmebehandlung auf der Basis des Tabellenansatzes – wie wir sahen – keinen Schaden im Gutfall verursachen. Ehrlich gesagt, auch wenn du harte Echtzeitbedingungen umsetzen musst, betrifft diese Einschränkung meist nur einen kleinen Teil des Programms.

Anstelle gegen die Nicht-Regel zu argumentieren, sind hier die Regeln für die Verwendung von Ausnahmen.

Ausnahmen

  • erlauben es, deutlich zwischen einem Fehlercode als Rückgabewert und einem regulären Rückgabewert zu unterscheiden.
  • können nicht vergessen oder ignoriert werden.
  • lassen sich systematisch anwenden.

Gerne möchte ich eine Anekdote zu Legacy Code erzählen. Dieser Legacy Code verwendete Fehlercode um den Erfolg oder den Misserfolg eines Funktionsaufrufs zu kommunizieren. Der Fehlercode wurde in dem System geprüft. Das war gut, aber dank der Fehlercodes verwendeten die Funktionen keinen Rückgabewert. Die Konsequenz war, dass die Funktionen auf globalen Variablen agierten und daher auch keine Funktionsparameter besaßen. Das Ende der Geschichte war, dass das System nicht mehr wartbar und testbar war und mein Job bestand darin, es zu refaktorieren.

Die typische fehlerbehaftete Anwendung von Ausnahmen ist die folgende. Du fängst jede Ausnahme in jeder Funktion. Letztendlich endest du nun mit einem schwer wartbarem Code mit einer Spaghettistruktur. Ausnahmen sollten nicht das Mittel der Wahl sein um einen schnellen Fix zu machen, sondern sind Bestandteil der Systemarchitektur. Stelle dir daher vor, du entwirfst ein Eingabe-Subsystem. In diesem Fall musst du auch die potenziellen Ausnahmen dokumentieren und testen. Ausnahme sind ein wesentlicher Bestandteil des nichtfunktionalen Kanals und gehören damit zu dem Vertrag, dem du dem Anwender deines Subsystems gibst. Du benötigst eine klare Grenze zwischen der Anwendung und dem Subsystem. Das Ergebnis mag sein, dass das Subsystem die obskure Ausnahmen in einfache Ausnahmen übersetzt, sodass die Applikation darauf reagieren kann. Eine Ausnahme übersetzen heißt, dass die obskure Ausnahme in dem Subsystem gefangen wird und in einer einfacheren Form neu geworfen wird:

try{
// code, that may throw an obscure exception
}
catch (ObscureException18& ob){
throw InputSubsystemError("File has wrong permissions!");
}

Das Ergebnis einer solch vorgestellten Systemarchitektur, die den nichtfunktionalen Kanal (Ausnahmen) umfasst, ist, dass du das Subsystem, dass du die Integration des Subsystems in die Applikation und dass du das System (Applikation) in Isolation testen kannst.

Die letzte Mythos für heute ist sehr einfach zu entlarven.

NR.4: Don’t: Place each class declaration in its own source file

Das richtige Mittel um den Code zu strukturieren sind nicht Dateien; der richtige Weg ist es Namensräume zu verwenden. Wird jede Klasse in einer eigenen Klasse deklariert, ergeben sich viele Dateien und dein Programm ist damit schwieriger zu verwalten und zu testen.

Wie geht's weiter?

Du kannst sicher sein: The C++ Core Guidelines und ich sind noch nicht fertig mit den Nicht-Regeln und Mythen zu C++. Danach solltest du in der Lage sein, Nicht-Regeln und Mythen zu demystifizieren, sobald du ihnen begegnest.