Open Source im professionellen Einsatz
Linux-Magazin 12/2016

Modernes C++ in der Praxis – Folge 31

Von der Theorie zur Praxis

Gut 30 Artikel und fünf Jahre ist diese Serie zum modernen C++ nun alt. Jetzt ist es Zeit für einen Fokuswechsel. Drehte es sich bisher um die Frage "Welche Features bietet modernes C++?", so wird es in Zukunft um die Frage gehen: "Wie setzt der Programmierer die Features von modernem C++ richtig ein?"

1248

Die Reise zur Beantwortung der Frage, wie Programmierer die Features von modernem C++ richtig einsetzen, startet mit einfachen Merksätzen, beschäftigt sich genauer mit den C++ Core Guidelines [1] und nimmt auch Werkzeuge für die statische Codeanalyse ins Visier. Modernes C++ steht dabei für die Art und Weise, wie C++ mit einem modernen Compiler effektiv verwendet wird.

In der Natur einer lange Reise liegt es, dass sich die Zwischenstationen ändern und manchmal ist sogar das Ziel der Reise anzupassen. Daher bin ich auf jede Diskussion zu den Zwischenstationen gespannt und werde versuchen, ihre Ergebnisse in die nächsten Folgen aufzunehmen [2].

Ein erster Umweg

Los geht es bereits mit dem ersten Umweg. In meiner Rolle als Python-Seminarleiter antworte ich auf die Frage, wie Python richtig einzusetzen sei, mit den 20 Aphorismen von Tim Peters. Ein schlichtes »import this« in der Python-Interpretershell zeigt die 20 Merksätze in Abbildung 1, die unter dem Namen "The Zen of Python" bekannt sind [3]. Eigentlich sind es nur 19 Merksätze – der zwanzigste ist noch nicht aufgeschrieben.

Abbildung 1: Die Merksätze unter dem Titel "The Zen of Python".

Zehn Stationen

In ähnlicher Manier gibt es zehn Leitsätze für C++, die Abbildung 2 zeigt. Diese zehn Merksätze besitzen allerdings keinen großen Mehrwert, wenn sie nicht mit konkreten Beispielen hinterlegt sind. Genau diesen Mehrwert liefern die nächsten Artikel der Serie zu modernem C++. Zu Anfang der Reise aber ist es sehr hilfreich, die zehn Stationen im Schnelldurchlauf vorzustellen.

Abbildung 2: Die zehn Merksätze für den C++-Entwickler.

Vermeide implizite Typkonvertierungen. Erfahrungsgemäß sind implizite Typkonvertierungen ein häufiger Grund für undefiniertes Programmverhalten, das sich dadurch auszeichnet, dass es nicht mehr möglich ist, eine Aussage über das Ergebnis des Programms zu treffen. Das Programm kann abstürzen, ein falsches Ergebnis oder – im schlimmsten Fall – das richtige Ergebnis liefern. Im Englischen hat sich der Ausdruck "Catch Fire Semantic" für dieses Programmverhalten etabliert.

Doch wann treten implizite oder heimliche Typkonvertierungen auf? Das ist einfach, leider zu einfach in C++. So lässt sich eine Int-Variable mit einem Double- Wert initialisieren, was die Nachkomma-Komponente von »3.14« abschneidet: int »i(3.14)« . Weitere Beispiele sind die Nullpointer-Konstanten »0« oder »NULL« . Beide lassen sich in eine natürliche Zahl konvertieren. Das gleich trifft für eine Aufzählung zu: »enum Color{red, blue, green}« . Der Aufzähler »red« konvertiert heimlich in die ganze Zahl 0.

Programmiere deklarativ. In C++11 ist es deutlich einfacher, deklarativ zu programmieren. Dabei steht deklaratives Programmieren für einen Programmierstil, bei dem der Programmierer lediglich seine Absicht ausdrückt und die C++-Laufzeitumgebung diese umsetzt. Deklaratives Programmieren entspricht einem Vertrag. Der Programmierer spezifiziert das Verhalten seines Systems, die C++-Laufzeit setzt diesen Vertrag um. Die neuen Schlüsselwörter »default« und »delete« erlauben es, Methoden vom Compiler anzufordern oder sie zu unterdrücken. In ähnlicher Weise stellt das Schlüsselwort »override« sicher, dass diese Methode eine virtuelle Methode einer Basisklasse überschreibt. Das Gegenteil bewirkt »final« . Dank »final« lässt sich eine virtuelle Methode nicht überschreiben.

Unterstütze automatische Optimierungen. Mit der Move-Semantik und konstanten Ausdrücken besitzt modernes C++ eine mächtige Stellschraube, um die Performance einer C++-Anwendung günstig zu beeinflussen. Während die Move-Semantik es erlaubt, billig zu verschieben statt teuer zu kopieren, lassen sich als »constexpr« deklarierte Funktionen bereits zur Compile-Zeit ausführen. Dementsprechend besteht die Aufgabe des Implementers einer Bibliothek nun darin, die Algorithmen und Funktionen so umzusetzen, dass der Compiler automatisch die schnelle Move-Semantik auswählt oder der Anwender den konstanten Ausdruck bereits zur Compile-Zeit berechnen kann.

Sei nicht schlauer als der Compiler. Es ist eine sehr verbreitete Krankheit unter C++-Entwicklern, schlauer als der Compiler sein zu wollen. Das trifft auf die explizite Typisierung der Variablen genauso zu wie auf die Kosten einer Abstraktion, etwa mit Smart Pointern.

Leider basiert das Halbwissen vieler Programmierer auf jahrzehntelanger Erfahrung, ist aber nach einem Upgrade des C++-Standards oder auch des C++-Compilers nicht mehr gültig. Die modernen Optimierer leisten großartige Arbeit. Daher hat der Merksatz von Donald Knuth "Premature optimization is the root of all evil (or at least most of it) in programming" [4] nichts von seiner Aktualität verloren.

Behalte das große Bild im Auge. "Because we can": Das Zitat von Herb Sutter [5] auf der "C++ CppCon 2015" bringt die Mentalität vieler C++-Entwickler auf den Punkt. Ein sehr anspruchsvolles Feature in C++ setzt der Programmierer gerne nur deshalb ein, weil es vorhanden ist. In diesem Zusammenhang sind aber wichtigere Fragen zu klären:

  • Rechtfertigt der vermeintliche Mehrwert des Feature den Aufwand, es zu verwenden?
  • Wie stark leidet die Verständlichkeit des Codes unter dem Feature?
  • Lässt sich das Feature sicher einsetzen?

Wem das zu abstrakt war, den verweise ich gerne auf atomare Operationen in C++ und das Speichermodell [6]. Durch Optimierung im Mikrobereich lässt sich das letzte Performance-Quäntchen durch den Bruch der sequenziellen Konsistenz aus einem Code-Abschnitt pressen. Dabei geht der Blick auf das große Bild und insbesondere auf die Performance im Ganzen aber allzu leicht verloren.

Vermeide undefiniertes Verhalten. Wann wird eine Variable in C++ initialisiert? Eine einfache Frage, auf die es keine einfache Antwort gibt. Sehr hinterhältig lauert undefiniertes Verhalten dann, wenn ein Thread »t« mittels des Aufrufs »t.detach« im Hintergrund läuft. Hier gilt äußerste Vorsicht.

Achte auf die Lesbarkeit des Codes. Den Sourcecode liest man deutlich öfter, als man ihn schreibt. Daher muss er für den Leser optimiert sein. Mit Lambda-Funktionen, der Range-basierten For-Schleife oder auch der automatischen Typableitung mit »auto« lässt sich modernes C++ viel leichter verdauen.

Lass dir helfen. Der Werkzeugkasten, der helfen kann ein wohldefiniertes Programm zu schreiben, ist reichlich gefüllt. Damit ist es möglich, zur Compile-Zeit den Code mit Hilfe des Schlüsselworts »static_assert« und der Funktionen der Type-Traits-Bibliothek auf seine Richtigkeit zu prüfen. So stellt die automatische Typableitung mit »auto« zur Laufzeit des Programms sicher, dass der richtige Typ zur Verfügung steht. Hier endet die Geschichte aber bei Weitem noch nicht. Statische Code-Analysetools wie »clang-tidy« [7] oder lediglich das Übersetzen des Sourcecodes mit einem anderen Compiler geben ohne großen Aufwand wertvolle Einsichten.

Kenne deine Bibliotheken. In C++ gilt mittlerweile diese Annahme: Wer eine explizite For-Schleife schreibt, um die Elemente seines Containers zu modifizieren, der kennt die gut 100 Algorithmen der Standard Template Library (STL) nicht. Positiv ausgedrückt: Die Kenntnis der Algorithmen der Standard Template Library, die sich mit Lambda-Funktionen an die eigenen Bedürfnisse anpassen lassen, ist eine Schlüsselfähigkeit jedes professionellen Software-Entwicklers. Die Aussagekraft dieses Satzes steigt mit C++17 deutlich, denn mit dieser Sprachvariante lassen sich die Algorithmen der STL mit Hilfe der Execution Policy sequenziell, parallel oder vektorisiert ausführen. Ein Mehrwert, den eine hart kodierte For-Schleife nicht besitzt.

Strebe nach Einfachheit. Die Liste der Features ist lang, mit deren Hilfe C++ einfacher einzusetzen ist. Neben der automatischen Typableitung mit »auto« und der Range-basierten For-Schleife, die auf jeden Container der STL einschließlich des String anzuwenden ist, darf an dieser Stelle auf keinen Fall die vereinheitlichte Initialisierung mit geschweiften Klammern fehlen. Die einfache Regel lautet: Alles, was es zu initialisieren gilt, ist mit geschweiften Klammern zu initialisieren. Daher lassen sich einzelne Variablen oder auch ganze Container in einem Rutsch vorbelegen.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 3 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • C++

    Verantwortung abgeben fällt C++-Entwicklern nicht immer leicht. Dabei trifft der Compiler fast immer die besseren Entscheidungen, wie der vorliegende Artikel zeigt.

  • C++

    Implizite Typkonvertierungen sind eine der Ursachen für undefiniertes Verhalten in C- und C++-Programmen. In Kurzform bedeutet dies, dass solche Programme alles Mögliche tun dürfen und unvorhersehbar reagieren. C++-Routinier Rainer Grimm zeigt, wie C++-Entwickler diese Falle umgehen.

  • Clang beherrscht vollständig C++11

    Das C++-Frontend Clang für das Compiler-System LLVM unterstützt jetzt den kompletten Sprachumfang von C++11. Darüber hinaus hat die Umsetzung erster Teile des kommenden C++1y-Standards (alias C++14) begonnen.

  • Bücher

    Die Bücherseite stellt dieses Mal Bücher für fortgeschrittene Programmierer vor. Der erste Titel beschäftigt sich mit der Python-Praxis, der zweite mit Parallelprogrammierung in der Sprache Haskell.

  • C++

    Der neue Standard C++17 kündigt sich für 2017 an. Er wird zwar einige großartige Features wie eine Bibliothek für das Dateisystem mitbringen, doch andere lang ersehnte Funktionen fehlen wohl weiterhin.

comments powered by Disqus

Ausgabe 10/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.