C++ Core Guidelines: Die Philosophie

Der Abschnitt "primarily for humans" der C++ Core Guidelines ist der allgemeinste aller Abschnitte und besitzt den Namen Philosophie. Die Regeln sind so allgemeingültig, dass sie sich auf jede Programmiersprache anwenden lassen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 8 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Heute tauche ich tiefer in die C++ Core Guidelines ein. In meinem letzten Artikel habe ich sie bereits vorgestellt. Heute geht es aber um den Abschnitt "primarily for humans". Dieser ist der allgemeinste aller Abschnitte und besitzt den Namen Philosophie. Die Regeln sind so allgemeingültig, dass sie sich auf jede Programmiersprache anwenden lassen.

Nur eine kleine Gedächtnisstütze: Die C++ Core Guidelines bestehen aus 350 Regeln. Diese sind in die folgenden Abschnitte gruppiert.

  • In: Introduction
  • P: Philosophy
  • I: Interfaces
  • F: Functions
  • C: Classes and class hierarchies
  • Enum: Enumerations
  • R: Resource management
  • ES: Expressions and statements
  • E: Error handling
  • Con: Constants and immutability
  • T: Templates and generic programming
  • CP: Concurrency
  • SL: The Standard library
  • SF: Source files
  • CPL: C-style programming
  • Pro: Profiles
  • GSL: Guideline support library
  • FAQ: Answers to frequently asked questions

Die 13 Regeln zur Philosophie liefern die Begründung für die mehr als 350 weiteren Regeln.

Die folgenden 13 Regeln können nicht vollständig geprüft werden.

  • P.1: Express ideas directly in code
  • P.2: Write in ISO Standard C++
  • P.3: Express intent
  • P.4: Ideally, a program should be statically type safe
  • P.5: Prefer compile-time checking to run-time checking
  • P.6: What cannot be checked at compile time should be checkable at run time
  • P.7: Catch run-time errors early
  • P.8: Don’t leak any resources
  • P.9: Don’t waste time or space
  • P.10: Prefer immutable data to mutable data
  • P.11: Encapsulate messy constructs, rather than spreading through the code
  • P.12: Use supporting tools as appropriate
  • P.13: Use support libraries as appropriate

Falls möglich, werde ich Beispiele aus den C++ Core Guidelines zu jeder Regel präsentieren.

Code soll die Ideen direkt ausdrücken. Die zwei Methoden bringen die Idee auf den Punkt.

class Date {
// ...
public:
Month month() const; // do
int month(); // don't
// ...
};

Weder drückt die zweite Methode month() aus, dass sie die Instanz nicht verändern wird, noch, dass sie einen Monat zurückgibt. Dieselbe Argumentation trifft oft für explizite Schleifen versus Algorithmen der Standard Template Library (STL) zu.

int index = -1;                         // bad
for (int i = 0; i < v.size(); ++i) {
if (v == val) {
index = i;
break;
}
}

auto p = find(begin(v), end(v), val); // better

Die übergeordnete Regel ist in diesem Beispiel offensichtlich. Professionelle C++-Entwickler sollten die Algorithmen der STL kennen.

Die Regel hört sich recht einfach an und besitzt daher eine einfache Konsequenz: "Verwende einen aktuellen C++11- oder C++14-Compiler ohne Erweiterungen."

Dein Code soll seine Absicht direkt ausdrücken. Was können wir aus den drei folgenden expliziten und impliziten Schleifen ableiten?

for (const auto& v: vec) { ... }       (1)
for (auto& v: vec){ ... } (2)
for_each(par, vec, [](auto v){ ... }); (3)

In (1) werden die Elemente des Containers [i]vec nicht modifiziert. Das gilt aber nicht für die Range-basierte for-Schleife in (2). Der Algorithmus for_each (2) wird parallel ausgeführt (par). Das bedeutet, dass es irrelevant ist, in welcher Reihenfolge die Elemente prozessiert wird.

Dein Ziel sollte es sein, Programme zu schreiben, die bereits zur Compilezeit auf ihre Typsicherheit geprüft werden. Natürlich ist das nicht immer in C++ möglich. Daher nennen die C++ Core Guidelines diese kritischen Bereiche und bieten Lösungen an.

  • Verwende std::variant (neu mit C++17) anstelle von Unions.
  • Minimiere die Verwendung von Casts; setze statt dessen Templates ein, falls möglich.
  • Verwende gsl::span gegen Array Decay (falls du ein Array an eine Funktion übergibst, wird dieses Array implizit zu einem Zeiger auf sein erstes Element reduziert) und Bereichsfehler
  • Minimiere "narrowing conversion" ("narrowing conversion" oder verengende Konvertierung ist die implizite Konvertierung von Datentypen auf einfachere Datentypen, mit der ein Verlust der Datengenauigkeit eingehen kann; so kann aus einem double ein int werden).

GSL steht für Gudelines Support Library, eine kleine Bibliothek, die hilft, die Regeln der C++ Core Guidelines umzusetzen. Ich werde ihr noch einen separaten Artikel widmen.

Alles, was bereits zum Compilezeit geprüft werden kann, sollte zur Compilezeit geprüft werden. Seit C++11 besitzen wir die Funktion static_assert und die Typ-Traits Bibliothek. Dank static_assert lässt sich ein Ausdruck wie static_assert(sizeof(int) >= 4) zur Compilezeit auswerten. Dank der Type-Traits-Biblitothek können wir mächtige Bedingungen an einen Typ T zur Compilezeit formulieren: static_assert(std::is_integral<T>::value). Es versteht sich von selbst: Falls der static_assert-Ausdruck zu false evaluiert, steigt der Compiler mit einer lesbaren Fehlermeldung aus. Ich schrieb bereits Artikel zu static_assert.

Diese Regel beschäftigt sich mit schwierig zu findenden Fehlern, die es zu vermeiden gilt. Daher beschäftigt sich das Beispiel mit dynamisch allozierten Arrays.

extern void f(int* p);
extern void f2(int* p, int n);
extern void f3(unique_ptr<int[]> uniq, int n);

f(new int[n]); (1)
f2(new int[n], m); (2)
f3(make_unique<int[]>(n), m); (3)

Was ist verbesserungswürdig an diesem Beispiel? Der Aufruf (1) übergibt nicht die Anzahl seiner Elemente. (2) erlaubt es hingegen, die falsche Anzahl an Element anzugeben. Der Aufruf (3) übergibt die Besitzverhältnisse und die seine Anzahl separat. Diese Probleme lassen sich lösen, wenn man eine Referenz oder Views (Teil der GSL) verwendet.

Hier sind die Maßnahmen für diese Regel:

  • Kümmere dich um Zeiger und Arrays: Prüfe deren Bereiche frühzeitig und nicht mehrfach.
  • Kümmere dich um Konvertierungen: Unterbinde oder zu mindestens markiere "narrowing conversions".
  • Kümmere dich um die ungeprüfte Eingaben.

Leckende ("leaking") Ressourcen sind insbesondere für lang laufende Programme kritisch. Ressourcen können Speicher, aber auch Dateihandel oder auch Sockets sein. Die idiomatische Art, diese Anforderung in C++ zu beantworten, ist RAII. RAII steht für Rescource Acquisition Is Initialization. Dieses C++-Idiom besagt, dass eine Ressource im Konstruktor eines lokalen Objektes angefordert und im Destruktor des Objektes wieder freigegeben wird. Damit ist die Lebenszeit der kritischen Ressource an die Lebenszeit eines lokalen Objekts gebunden. Genau um dieses lokale Objekt kümmert sich aber die C++-Laufzeit. Hier gibt es mehr Details zu RAII mit Anmerkungen von Bjarne Stroustrup.

Die Begründung für die Regeln ist sehr einleuchtend: "This is C++." Ein Beispiel zu dieser Regel ist ein Rätsel.

Was wird hier verschwendet?

void lower(string s)
{
for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}

Es gibt viele Gründe, konstante Daten zu verwenden:

  • Es ist einfacher, ein Programm mit Konstanten als mit Variablen zu verifizieren.
  • Konstanten besitzen ein deutlich höheres Optimierungspotenzial.
  • Konstanten erlauben keine Data Races.

Unordentlicher Sourcecode ist sehr empfänglich für Fehler und deutlich schwieriger zu schreiben. Daher solltest du diesen Low-level-Code in Funktionen oder Methoden kapseln und ein gutes Interface dazu anbieten.

Computer sind deutlich besser und zuverlässiger darin, die langweiligen und sich wiederholenden Aufgaben immer und immer wieder zu wiederholen. Das bedeutet, dass du statische Codeanalyse Werkzeuge, Werkzeuge für Nebenläufigkeit und Testwerkzeuge verwenden solltest, um diese verifizierenden Schritte zu automatisieren.

Diese Regel ist einfach zu begründen. Du solltest gut entworfene, dokumentierte und unterstützte Bibliotheken verwenden. Damit erhältst du eine gut getestete, nahezu fehlerfrei Bibliothek und hoch optimierte Algorithmen von den Domänen-Experten. Zwei herausragende Beispiele sind die C++-Standard-Bibliothek und die Guidelines Support Library.

Ein Interface ist ein Vertrag zwischen einem Service-Anbieter und einem Server-Nutzer. Es gibt 30 Regeln zu Interfaces in den C++ Core Guidelines. In nächsten Artikel werde ich einen genaueren Blick darauf werfen. ()