Aus Linux-Magazin 12/2018

Modernes C++ in der Praxis – Folge 43

© Gleb TV, 123RF

Als Appetizer servieren die C++ Core Guidelines ihren Lesern zunächst einige philosophische Erkenntnisse. Deren Einhaltung lässt sich nur schwer über Algorithmen abfragen, C++-Entwicklern verhelfen die Rezeptideen dennoch zu modernem Code aus einem Guss.

Die Herausgeber der C++ Core Guidelines [1] sind für C++-Enthusiasten keine Unbekannten: Es handelt sich um Bjarne Stroustrup, den C++-Erfinder höchstselbst, und um Herb Sutter. Letzterer arbeitet seit vielen Jahren als Vorsitzender des Standardisierungskomitees.

Abbildung 1: Die Core Guidelines ordnen die mehr als hundert Regeln verschiedenen Bereichen zu.

Abbildung 1: Die Core Guidelines ordnen die mehr als hundert Regeln verschiedenen Bereichen zu.

Die C++ Core Guidelines enthalten weit mehr als hundert Regeln, die das Regelwerk in mehrere Hauptabschnitte untergliedert. Die wiederum widmen sich wahlweise den Themenbereichen Interfaces, den Klassen und Klassenhierarchien oder dem Thema Ressourcenmanagement (Abbildung 1).

Am Anfang …

Jeder Hauptabschnitt besteht aus mehreren Regeln, das gilt auch für die Einführung. Laut der ist der imaginierte Zielleser ein C++-Programmierer, der aber auch mit C liebäugelt. Ziel sei es, Entwicklern zu helfen modernes C++zu schreiben, wobei sich modern auf die Versionen 11, 14 und 17 bezieht. Entwickler sollten diese Regeln, die statische Typsicherheit und sicheres Ressourcenmanagement betonen, verstehen, sie aber nicht unreflektiert anwenden.

Einige Ziele schließen die Guidelines-Autoren auch explizit aus. Die Regeln sollen sich nicht einfach nacheinander lesen lassen oder ein Tutorial ersetzen. Auch seien sie weder eine Anleitung, um alten C++-Code in modernes C++ zu portieren, noch sollen sie sehr exakt sein oder eine vereinfachte Teilmenge von C++ beschreiben.

Abbildung 2: Die Regeln folgen ihrerseits wieder einheitlichen Strukturen.

Abbildung 2: Die Regeln folgen ihrerseits wieder einheitlichen Strukturen.

Zu jeder Regel gehört auch ein Abschnitt, der sich ihrer Umsetzung widmet, denn sie soll den Entwicklern helfen, ihren Code zu vereinheitlichen und zu modernisieren. Die Regeln folgen zudem einer einheitlichen Struktur (Abbildung 2). Nicht zufällig erinnert die Systematik an die Design-Pattern-Literatur. Als deren prominentestes Beispiel gilt wohl das Buch “Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software” [2].

Erstes Beispiel

Um die Absicht der Struktur klar auf den Punkt zu bringen, folgt als einfaches Beispiel die Regel R.22, das R steht für Ressourcenmanagement. Die Regel lautet: “Verwende »std::make_shared()«, um »std::shared_ptr« zu erzeugen.” Zur Begründung argumentieren die Autoren damit, dass Entwickler, die ein Objekt erzeugen und an den Konstruktor des »std::shared_ptr« übergeben, mit hoher Wahrscheinlichkeit mehr als eine Speicher-Allokation und später -Deallokation anwenden.

Der Grund: Sie müssen das Objekt und den Referenzzähler separat allozieren. Das ist nicht der Fall, wenn sie »std::make_shared()« einsetzen. Als Beispiel dient dann der in Listing 1 gezeigte Code. Die »std::make_shared()«-Version verwendet »X« nur ein Mal. Daher ist diese Variante im Allgemeinen kürzer und schneller als die Variante mit dem expliziten »new«-Aufruf.

Listing 1

Einsatz von std::make_shared()

01 std::shared_ptr<X> p1 { new X{2} }; // bad
02 auto p = std::make_shared<X>(2);    // good

Bei der Umsetzung raten die Autoren dazu, eine Warnung für den Fall ausgeben zu lassen, dass der Programmierer einen »std::shared_ptr« über ein »new«- und nicht über einen »std:make_shared()«-Aufruf erzeugt.

Metaregeln

Bevor die nächsten Artikel die Rosinen aus den Guidelines picken, will diese Einführung darlegen, welcher Philosophie oder welchen Metaregeln die Richtlinien der Guidelines zugrunde liegen. Die Details zu den sehr allgemein gehaltenen Regeln des Philosophie-Abschnittes lassen sich auch direkt im englischsprachigen Dokument nachlesen [3]. Der vorliegende Artikel handelt sie allerdings eher kurz und bündig ab.

Ideen in Sourcecode gießen

In modernem C++-Code sollen Entwickler ihre Design-Ideen direkt ausdrücken. Die zwei Methoden in Listing 2 bringen die Absicht hinter dieser Forderung auf den Punkt. Weder macht die als Negativbeispiel gezeigte Methode »month()« in Zeile 5 deutlich, dass sie die Instanz und das Date-Objekt nicht verändert. Noch wird an dieser Stelle klar, dass sie einen Monat zurückgibt.

Listing 2

Ideen im Quellcode ausdrücken

01 class Date {
02     // ...
03 public:
04     Month month() const;  // do
05     int month();          // don't
06     // ...
07 };

Dasselbe Argument trifft häufig auf explizite Schleifen wie »for« oder »while« versus Algorithmen der Standard Template Library (STL) zu. Listing 3 tritt den Beweis an.

Listing 3

Schleifen explizit verwenden

01 int index = -1;                             // bad
02 for (int i = 0; i < v.size(); ++i) {
03     if (v == val) {
04       index = i;
05       break;
06     }
07 }
08
09 auto p = std::find(begin(v), end(v), val);  // better

Professionelle C++-Entwickler sollten die Algorithmen der STL also am besten im Schlaf beherrschen. Dadurch vermeiden sie unter anderem den Einsatz von Schleifen. Von denen raten die Richtlinien ab, weil der Algorithmus in Zeile 9 seine Absicht viel direkter ausdrückt.

ISO-C++ schreiben

Die Regel, C++ nach dem ISO-Standard zu verwenden, lässt sich nicht nur einfach merken. Sie zieht auch als sehr einfache Konsequenz das Gebot nach sich, möglichst einen aktuellen C++11-, C++14- oder C++17-Compiler ohne Erweiterungen zu verwenden.

Absicht ausdrücken

Eine weitere Richtlinie lautet, stets die Absicht des Codes im Code selbst auszudrücken. In der Praxis wirft das zum Beispiel die Frage auf, was sich aus den drei expliziten und impliziten Schleifen in Listing 4 ableiten lässt.

Listing 4

Die Absicht ausdrücken

01 for (const auto& v: vec) { ... }
02 for (auto& v: vec){ ... }
03 std::for_each(std::execution::par, vec, [](auto v){ ... });

In Zeile 1 modifiziert das Listing die Elemente des Containers »vec« nicht. Das gilt jedoch nicht für die Range-basierte For-Schleife in Zeile 2. Den Algorithmus »for_each()« in Zeile 3 führt C++ dann parallel aus (»std::execution::par«). Das bedeutet, dass es irrelevant ist, in welcher Reihenfolge der Code die Elemente prozessiert.

Statisch typsicher

Ziel sollte es sein, Programme zu schreiben, die der Compiler bereits beim Übersetzen auf ihre Typsicherheit überprüft. Natürlich ist das in C++ nicht immer möglich. Die C++ Core Guidelines greifen diese kritischen Bereiche auf und bieten Lösungen dafür an:

  • Anstelle von Unions verwenden Entwickler besser »std::variant« (neu mit C++17).
  • Wo möglich, setzen Entwickler statt Casts besser Templates ein.
  • Gegen Array Decay und Indexfehler hilft »gsl::span« aus der Guideline Support Library. Array Decay tritt auf, wenn Entwickler ein Array an eine Funktion übergeben. Die Funktion nimmt dieses Array dann als einen Zeiger auf sein erstes Element an.
  • Verengende Konvertierung (Narrowing Conversion) sollten Entwickler ebenfalls vermeiden. Dabei handelt es sich um die implizite Konvertierung von Datentypen auf einfachere Datentypen. Der Prozess kann mit einem Verlust der Datengenauigkeit einhergehen. So verwandelt sich ein »double«-Wert beim Zuweisen an einen »int«-Wert in einen »int«-Wert.

Die Guidelines Support Library hilft übrigens dabei, die Regeln der C++ Core Guidelines umzusetzen.

Code zur Compilezeit prüfen

Alles was der Compiler zur Übersetzungszeit prüfen kann, sollten Entwickler auch zur Übersetzungszeit prüfen lassen. Seit Version 11 besitzt C++ die Funktion »static_assert()« und die Type-Traits-Bibliothek. Dank »static_assert« lässt sich zum Beispiel ein Ausdruck wie »static_assert(sizeof(int) >= 4)« zur Compilezeit auswerten. Mit Hilfe der Type-Traits-Bibliothek darf der Anwender zudem mächtige Bedingungen an einen Typ »T« zur Compilezeit formulieren: »static_assert(std::is_integral<T>::value)«. Falls der »static_assert«-Ausdruck zu »false« evaluiert, steigt der Compiler mit einer lesbaren Fehlermeldung aus.

… oder zur Laufzeit

Diese Regel adressiert typische Fehlerquellen wie Arrays. Listing 5 stellt drei Varianten vor.

Listing 5

Arrays

01 void f(int* p);
02 void f2(int* p, int n);
03 void f3(std::unique_ptr<int[]> uniq, int n);
04
05 f(new int[n]);
06 f2(new int[n], m);
07 f3(std::make_unique<int[]>(n), m);

Worin unterscheiden sich die drei Funktionsaufrufe ab Zeile 5? Der erste übergibt nicht die Anzahl seiner Elemente. Zeile 6 erlaubt es aber, eine falsche Zahl an Elementen anzugeben. Erst der dritte Aufruf übergibt explizit das »<int>«-Array – in einem »std::unique_ptr« verpackt. Das macht die Funktion zum Besitzer des »int«-Array, sie räumt automatisch am Ende des Funktionskörpers auf.

Laufzeitfehler vermeiden

Verschiedene Maßnahmen gehen auch mit dieser Regel einher. So sollen sich Entwickler gut um Zeiger und Arrays kümmern. Es gilt, ihre Bereiche frühzeitig und nicht mehrfach zu prüfen. Auch Konvertierungen behalten sie besser im Auge. Die sollten sie unterbinden oder zumindest verengende Konvertierungen markieren. Nicht zuletzt will diese Regel darauf hinaus, nicht ungeprüft Eingaben zu übernehmen.

Gegen Ressourcenlecks

Leckende Ressourcen (Leaks) erweisen sich insbesondere für lange laufende Programme als kritisch. Bei den Ressourcen kann es sich um Speicher, aber auch um Dateihandles oder Sockets handeln. Die idiomatische Art, diese Anforderung in C++ zu bewältigen, nennt sich RAII. Die Abkürzung steht für Resource Acquisition Is Initialization. Dies C++-Idiom sieht vor, eine Ressource im Konstruktor eines lokalen Objekts anzufordern und im Destruktor des Objekts wieder freizugeben. Das bindet die Lebenszeit der kritischen Ressource an die Lebenszeit eines lokalen Objekts. Um dies kümmert sich die C++-Laufzeit.

Zeit- und Speicher sparen

Die Begründung für diese Regeln fällt eher sparsam aus: “This is C++.” Immerhin liefert Listing 6 ein passendes Beispiel. Das Rätsel aber lautet: Was wird hier verschwendet? Wer es weiß und eine bessere Lösung kennt, schicke eine Mail an mailto:redaktion@linux-magazin.de. Die Auflösung folgt im nächsten Artikel.

Listing 6

Zeit oder Speicher verschwenden

01 void lower(string s)
02 {
03      for (unsigned int i = 0; i < strlen(s.data()); ++i) s[i] = tolower(s[i]);
04 }

Unveränderliche Daten

Es gibt verschiedene Gründe, unveränderliche Daten zu bevorzugen. Beispielsweise ist es einfacher, ein Programm mit Konstanten als mit Variablen zu verifizieren. Konstanten besitzen zudem ein deutlich höheres Optimierungspotenzial. Zudem führt der Einsatz von Konstanten nicht zu Data Races.

Low-Level-Code kapseln

Low-Level-Quellcode ist sehr empfänglich für Fehler und daher deutlich schwieriger zu schreiben. Entwickler sollten den Code in Funktionen oder Methoden kapseln und zudem ein stabiles Interface dafür anbieten.

Hilfswerkzeuge willkommen

Computer sind deutlich besser und zuverlässiger als Menschen in der Lage, langweilige und repetitive Aufgaben ohne zu Murren endlos zu wiederholen. Für Entwickler bedeutet das, statische Code-Analyse-Werkzeuge, Concurrency-Tools und Testwerkzeuge zu verwenden, um diese immer wiederkehrenden Verifizierungsschritte zu automatisieren.

Hilfsbibliotheken an Bord

Auch diese Regel lässt sich einfach begründen. Entwickler sollten gut entworfene, dokumentierte und unterstützende Bibliotheken verwenden. Dadurch erhalten sie eine gut getestete, nahezu fehlerfreie Bibliothek und hochoptimierte Algorithmen von Domänen-Experten. Zwei herausragende Beispiele sind die C++-Standard-Bibliothek und die Guidelines Support Library.

Wie geht’s weiter?

Wie anfangs versprochen, pickt sich der nächste Artikel die Rosinen aus den Guidelines heraus. Genauer: Er widmet sich den Interfaces.

Der Autor

Rainer Grimm ist Trainer für C++ und Python. Seine zahlreichen C++-Bücher, zuletzt “The C++ Standard Library” und “Concurrency with modern C++”, sind bei O’Reilly und Leanpub erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 3 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben