C++ Core Guidelines: Regeln zu den Namen und zum Layout des Codes

Modernes C++ Rainer Grimm  –  9 Kommentare

Die C++ Core Guidelines besitzen rund zwanzig Regeln zu Namen und zum Layout von Sourcecode. Einige dieser Regeln sind sehr offensichtlich, andere deutlich kontroverser. Auf diese werde ich genauer eingehen.

Regeln zu Namen und zum Layout des Codes

Zuerst einmal ist es wichtiger, die Konsistenz mit dem bestehenden Sourcecode zu wahren als neuen Regeln zu folgen, die die Namen und das Layout des Codes bestimmen. Mit diesem Vorgedanken möchte ich meinen Artikel beginnen. Diese Regeln stehen heute auf meiner Agenda:

  • NL.1: Don’t say in comments what can be clearly stated in code
  • NL.2: State intent in comments
  • NL.3: Keep comments crisp
  • NL.4: Maintain a consistent indentation style
  • NL.5: Don’t encode type information in names
  • NL.7: Make the length of a name roughly proportional to the length of its scope
  • NL.8: Use a consistent naming style
  • NL.9: Use ALL_CAPS for macro names only
  • NL.10: Avoid CamelCase
  • NL.11: Make literals readable
  • NL.15: Use spaces sparingly
  • NL.16: Use a conventional class member declaration order
  • NL.17: Use K&R-derived layout
  • NL.18: Use C++-style declarator layout
  • NL.19: Avoid names that are easily misread
  • NL.20: Don’t place two statements on the same line
  • NL.21: Declare one name (only) per declaration
  • NL.25: Don’t use void as an argument type
  • NL.26: Use conventional const notation

Ich werde nichts zu den Regeln schreiben, die bereits ausreichend Dokumentation in den Guidelines besitzen. Ich werde nur zu den Regeln schreiben, die zusätzliche Erläuterungen benötigen oder in meinen Seminaren diskutiert werden.

NL.1: Don’t say in comments what can be clearly stated in code

Ehrlich gesagt bin ich kein Freund davon, jedes Stück Code zu dokumentieren. Wenn du das tust, ist das für mich ein Anzeichen auf ein Geschmäckle (code smell), denn dein Code ist zu kompliziert. Ich folge eher der Python-Regel: Explicit is better than implicit. Ich schreibe nur einen Kommentar, wenn ich einen Trick anzuwenden habe, der nicht offensichtlich ist. So neigen zum Beispiel unerfahrene Programmierer dazu, geschweifte Klammer aus dem Code mit genau der Einstellung zu entfernen, mit der sie überflüssige geschweifte Klammern aus arithmetischen Ausdrücken entfernen. Wenn du mir nicht glaubst, besuche meine Schulungen. Geschweifte Klammer sind aber oft essenziell, um RAII-Objekten wie Locks oder Smart-Pointern einen Bereich vorzugeben. Werden die geschweiften Klammern in diesem Fall von einem Lock entfernt, erhältst du im beste Fall ein langsameres Programm und im schlechtesten Fall ein Deadlock. Das führt dazu, dass viele meiner Kunden die folgende Anwendung von geschweiften Klammern dokumentieren:

std::mutex mut;
{ // necessary to manage the lifetime of the lock
std::lock_guard<std::mutex> lock(mut);
...
}

Das Schlechte an Kommentaren ist, dass sie automatisch veralten. Das ist per Definition für den Sourcecode nicht möglich. Dieser ist immer auf dem aktuellen Stand. Als Berufseinsteiger bestand mein Job häufig darin, bestehenden Code zu analysieren oder zu refaktorieren. Ehrlich gesagt hatte ich oft keine Ahnung, welche Intention der Code hatte, und war daher sehr frustriert. Zu meiner Rettung fand ich aber ein paar Kommentare. Leider waren die Kommentare total veraltet. Es dauerte oft einige Zeit, bis mir das bewusst wurde. Du kannst dir wohl denken, wie frustrierend das war. Kommentare müssen mit der gleichen Sorgfalt wie Sourcecode gepflegt werden. Das trifft aber oft nicht zu.

NL.5: Don’t encode type information in names

Wirklich? Muss dies immer noch als Regel postuliert werden? Ich dachte, wir haben die ungarische Notation wie das letzte Jahrtausend hinter uns gelassen. Ich meine die guten alten Zeiten, in den unsere Variablen noch keine Typen besaßen. Ungarische Notation ist dank der Typprüfung des Compilers überflüssig, widerspricht der generischen Programmierung und – dies ist mein Hauptargument – veraltet ähnlich schnell wie Kommentare. Kannst du erraten, für welche Datentypen die folgenden Variablen stehen?

bBusy;
fBusy;
pFoo;
szLastName;
fnFunction;
pszOwner;
rgfpBalances;
lpszBar;
g_nWhells;
m_whells;
_whells;

Falls du es nicht weißt, hier ist die Lösung: Ungarische Notation.

NL.7: Make the length of a name roughly proportional to the length of its scope

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 der 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.

NL.16: Use a conventional class member declaration order

Das ist eine einfache und sehr hilfreiche Regel.

  • Wenn du eine Klasse deklarierst, verwende die folgende Reihenfolge: die Konstruktoren, Zuweisungsoperatoren und Destruktoren vor den Funktionen und die wiederum vor den Daten.
  • Dasselbe gilt für die Zugriffsspezifizierer: public vor protected und dies vor private.
  • Verwende einen Zugriffsspezifizierer nicht mehrmals:
class X {   // bad
public:
void f();
public:
int g();
// ...
};
NL.19: Avoid names that are easily misread

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.

NL.20: Don’t place two statements on the same line

Hier ist 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++17 erhalten wir eine Ausnahme der Regeln: strukturierte Bindung. Strukturierte Bindung erlaubt es in eleganter Weise, mehrere Namen in einer Deklaration zu erklären (Zeile 1):

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

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

Wie geht's weiter?

Fertig! Nach mehr als einhundert Artikel zu den C++ Core Guidelines habe ich zwei gute Nachrichten.

Zuerst einmal werde ich ein Buch zu den C++ Core Guidelines schreiben. In diesem werde ich mein Bestes geben und eine kompakte Geschichte zu dem sehr wertvollen Inhalt zu erzählen. Meine Idee ist es, meine Geschichte auf C++17 zu basieren und die Regeln der C++ Core Guidelines zu verwenden, die notwendig für modernes C++ sind. Klar, modernes C++ steht für C++, das typsicher ist, die Grenzen von Container beachtet und die Lebenszeit von Variablen automatisch verwaltet. Ich werde in den nächsten Tagen beginnen und ab und zu eine Wasserstandsmeldung geben.

Meine Artikelserie zu den C++ Core Guidelines endet und damit beginnt meine neue Artikelserie zu dem hochaktuellen C++20-Standard. Meine Artikel zu C++20 werden mit einer Suche in die Breite starten und mit einer Suche in die Tiefe enden. C++20 ist ähnlich mächtig wie C++11. Daher kannst du annehmen, dass ich einiges zur Zukunft von C++ zu schreiben habe.