Aus Linux-Magazin 02/2018

Modernes C++ in der Praxis – Folge 38

Wer seinen Quellcode auf Lesbarkeit optimiert, ist nicht automatisch ein Ästhet. Schöner Code spart auch Geld, weiß C++-Autor Rainer Grimm und gibt Tipps, um die Lesbarkeit von Code zu verbessern.

Sourcecode wird deutlich häufiger gelesen als geschrieben, Schätzungen gehen von einem Verhältnis von mindestens 10:1 aus. Daher sollten Entwickler den Quellcode auf Lesbarkeit hin optimieren. Doch wer im Folgenden feinsinnige Betrachtungen zu schönem und hässlichem Code erwartet, dürfte enttäuscht werden: Wie fast immer geht es am Ende um den schnöden Mammon.

Abbildung 1: Der mittlerweile siebte Tipp dreht sich nur scheinbar um ästhetische Aspekte der C++-Programmierung. Schöner Code bietet auch ganz handfeste Vorteile.

Abbildung 1: Der mittlerweile siebte Tipp dreht sich nur scheinbar um ästhetische Aspekte der C++-Programmierung. Schöner Code bietet auch ganz handfeste Vorteile.

Bevor sich der Artikel aber der Code-Ästhetik zuwendet, hilft der Blick aufs große Ganze, das Ziel vor Augen zu behalten. “Achte auf die Lesbarkeit des Codes” lautet die siebte von zehn Regeln, um modernen C++-Code zu schreiben. Das Gesamtbild liefert Abbildung 1.

Schönheit lohnt sich

Gute Lesbarkeit des Sourcecodes hat viele Vorteile. Gut lesbarer Code ist einfacher zu verstehen. Dem Entwickler erschließt sich nicht nur wesentlich besser, was der Code tut, er macht so auch weniger Fehler.

Eine weitere Konsequenz besteht darin, dass Dritte weniger Zeit brauchen, um den Code zu verstehen. Sie finden Fehler vorheriger Programmierer schneller und schleusen seltener neue Fehler ein. Letztendlich entlastet dies die kritischste Ressource in der Software-Entwicklung: die kostbare Zeit des Entwicklers.

Schwer verdaulich

Auch wenn der Code in Listing 1 auf C++11-Funktionen setzt, garantiert dies noch lange nicht automatisch leicht verdauliche Kost. In Zeile 13 erklärt das Programm einen Vektor der Länge 20. Alle Elemente des Vektors besitzen in diesem Fall ihren Defaultwert 0. Der anschließende Ausdruck »std::iota(myVec.begin(), myVec.end(), 0)« setzt die Elemente sukzessive auf die Werte 0 bis 19. Genau dies zeigt die erste Zeile der Ausgabe des Programms in Abbildung 2. Erzeugt wird diese Zeile mit Hilfe einer klassischen For-Schleife, die in den Zeilen 18 bis 20 steht.

Listing 1

Vektor per Funktionsobjekt modifizieren

01 #include <algorithm>
02 #include <functional>
03 #include <iostream>
04 #include <iterator>
05 #include <numeric>
06 #include <string>
07 #include <vector>
08
09 int main(){
10
11   std::cout << std::endl;
12
13   std::vector<int> myVec(20);
14   std::iota(myVec.begin(), myVec.end(), 0);
15
16   std::cout << "myVec: ";
17   std::vector<int>::iterator vecIt;
18   for (vecIt= myVec.begin(); vecIt != myVec.end(); ++vecIt){
19     std::cout << *vecIt << " ";
20   }
21
22   std::cout << std::endl;
23
24   std::function<bool(int)> myPred= std::bind(std::logical_and<bool>(),
25                            std::bind(std::greater <int>(), std::placeholders::_1, 9),
26                            std::bind(std::less <int>(), std::placeholders::_1, 16 ));
27
28
29   myVec.erase(std::remove_if(myVec.begin(), myVec.end(), myPred),
30               myVec.end());
31
32   std::cout << "myVec: ";
33   std::copy(myVec.begin(), myVec.end(),
34             std::ostream_iterator<int> (std::cout, " "));
35
36   std::cout << "\n\n";
37
38 }

Schöner geht es nicht, oder? Die Zeilen 24 bis 26 definieren ein Prädikat. Dabei handelt es sich um eine Funktion, die für ihre Argumente immer einen Wahrheitswert liefert. »myPred()« steht in diesem Fall für eine Funktion, die ein »int« annimmt und ein »bool« zurückgibt.

Abbildung 2: Das Pr&auml;dikat taucht hier als Funktionsobjekt auf.

Abbildung 2: Das Prädikat taucht hier als Funktionsobjekt auf.

Das äußerste »std::bind« (Zeile 24) erklärt ein Funktionsobjekt, das zwei innere Funktionsobjekte durch ein logisches Und sowie »std::logical<bool>« bindet. Dabei steht »std::bind(std::greater<int>(), std::placeholders::_1, 9)« für ein Funktionsobjekt, das für ein Argument angibt, ob es größer als 9 ist.

In der Zeile 25 von Listing 1 findet sich mit »std::placeholder::_1« ein so genannter Platzhalter. Der erzeugt hier aus einem Funktionsobjekt, das eigentlich zwei Argumente erwartet (»std::greater <int>()«), ein einzelnes Funktionsobjekt, das entsprechend nur noch ein Argument benötigt. Die »9« bindet das zweite Argument bereits.

Die Argumentation gilt auch für das zweite, das innere Funktionsobjekt »std::bind(std:: less <int>(), std::placeholders::_1, 16 )«. Es verrät, ob sein Argument kleiner als 16 ist. Zugleich gibt »std::remove_if()« (Zeilen 29 und 30) nur das logische Ende des veränderten Vektors zurück. Daher muss der Code die Elemente noch per Erase-Remove-Idiom [1] entfernen.

Die Zeilen 33 und 34 schieben den Vektorinhalt trickreich auf einen Ausgabestream, der seine durch ein Leerzeichen getrennten Daten auf »std::cout« schreibt.

Kurztrip

Range-basierte For-Schleifen und Lambda-Funktionen verkürzen das Programm in Listing 1 um gut zehn Zeilen. Listing 2 fällt aber nicht nur kürzer aus, sondern ist vor allem auch deutlich einfacher zu verstehen. In den Zeilen 14 und 22 kommt die Range-basierte For-Schleife zum Einsatz. Die Lambda-Funktion in Zeile 18 lässt sich deutlich einfacher verdauen als das Funktionsobjekt in Listing 1 (Zeilen 24 bis 26). Die Lambda-Funktion »[](auto v){ return (v > 9) && (v < 16); }« fordert von ihrem Argument »v«, dass es größer als 9 und kleiner als 16 sein soll.

Listing 2

Vektor mit einer Lambda-Funktion ändern

01 #include <algorithm>
02 #include <iostream>
03 #include <numeric>
04 #include <vector>
05
06 int main(){
07
08   std::cout << std::endl;
09
10   std::vector<int> myVec(20);
11   std::iota(myVec.begin(), myVec.end(), 0);
12
13   std::cout << "myVec: ";
14   for (auto v: myVec) std::cout << v << " ";
15
16   std::cout << std::endl;
17
18   myVec.erase(std::remove_if(myVec.begin(), myVec.end(), [](auto v){ return (v > 9) && (v < 16); }),
19               myVec.end());
20
21   std::cout << "myVec: ";
22   for (auto v: myVec) std::cout << v << " ";
23
24   std::cout << "\n\n";
25
26 }

Daneben besitzt sie noch einen weiteren Vorteil, den die C++-Community gern als Killerargument anführt. Dank ihr kann der Optimierer besseren Code erzeugen, da alle notwendigen Informationen zum Prädikat genau dort auf ihn warten, wo es seinen Auftritt hat.

Klein, aber sehr fein

Seit C++11 lassen sich die Attribute einer Klasse direkt im Klassenkörper initialisieren. Zugegeben, die C++-Community hat das kleine Feature zu lange stiefmütterlich behandelt. Dabei sind es gerade solche kleinen und einfachen Funktionen, welche die Lesbarkeit und damit die Wartbarkeit des Sourcecodes entscheidend verbessern.

Abbildung 3: Gleich drei verschiedene Widgets erzeugt der Code aus <a href="#artRef-l3">Listing 3</a>.

Abbildung 3: Gleich drei verschiedene Widgets erzeugt der Code aus Listing 3.

Listing 3 stellt eine typische Klasse vor, die drei Konstruktoren anbietet, um ein VGA-, SVGA- und HD-Widget in den Zeilen 28 bis 30 zu initialisieren. Da es einige Abhängigkeiten zwischen der Höhe und Breite eines Widget gibt und jedes Widget per Default einen Rahmen besitzen und sichtbar sein soll, benötigen die Konstruktoren in den Zeilen 6 bis 10 noch einige Defaultwerte. Das Programm sollte kein größeres Überraschungspotenzial mehr bergen, Abbildung 3 zeigt es in Anwendung.

Listing 3

Die Klasse Widget

01 #include <iostream>
02 #include <string>
03
04 class Widget{
05   public:
06     Widget(): width(640), height(480), frame(false), visible(true) {}
07
08     Widget(int w): width(w), height(getHeight(w)), frame(false), visible(true){}
09
10     Widget(int w, int h): width(w), height(h), frame(false), visible(true){}
11
12     void show(){ std::cout << std::boolalpha << width << "x" << height
13                            << ", frame: " << frame << ", visible: " << visible
14                            << std::endl;
15      }
16   private:
17     int getHeight(int w){ return w*3/4; }
18     int width;
19     int height;
20     bool frame;
21     bool visible;
22 };
23
24 int main(){
25
26   std::cout << std::endl;
27
28   Widget wVGA;
29   Widget wSVGA(800);
30   Widget wHD(1280, 720);
31
32   wVGA.show();
33   wSVGA.show();
34   wHD.show();
35
36   std::cout << std::endl;
37
38 }

Klassenkörper

Das direkte Initialisieren der Klassenattribute im Klassenkörper in Listing 4 reduziert die Komplexität der Konstruktoren deutlich. Die Zeilen 17 bis 20 setzen die Attribute direkt. Verwendet ein Konstruktor ein Attribut »Widget(int w)«, erhält dieser Wert eine höhere Priorität.

Warum handelt es sich hierbei nun um ein so großartiges Feature? Zum einen reduziert es die Komplexität der Konstruktoren deutlich. Zum anderen erschwert es die Möglichkeit, unvollständig initialisierte Objekte zu erzeugen.

Listing 4

Klassenattribute im Klassenkörper initialisieren

01 #include <iostream>
02 #include <string>
03
04 class Widget{
05   public:
06     Widget() = default;
07     Widget(int w): width(w), height(getHeight(w)){}
08     Widget(int w, int h): width(w), height(h){}
09
10     void show(){ std::cout << std::boolalpha << width << "x" << height
11                            << ", frame: " << frame << ", visible: " << visible
12                            << std::endl;
13     }
14
15   private:
16     int getHeight(int w){ return w*3/4; }
17     int width = 640;
18     int height = 480;
19     bool frame = false;
20     bool visible = true;
21 };
22
23
24 int main(){
25
26   std::cout << std::endl;
27
28   Widget wVGA;
29   Widget wSVGA(800);
30   Widget wHD(1280,720);
31
32   wVGA.show();
33   wSVGA.show();
34   wHD.show();
35
36   std::cout << std::endl;
37
38 }

Konzeptionell setzt C++ über die Attribute das Standardverhalten eines Objekts. Das passt der Entwickler an, indem er die Attribute anschließend mit Hilfe von Konstruktoren überschreibt. Bei dem direkten Initialisieren der Klassenattribute beruht der große Mehrwert also vor allem auf dieser konzeptionellen Trennung von Standard- und angepasstem Verhalten.

Wie geht’s weiter?

C++ ist eine statisch typisierte Programmiersprache. Das heißt, beim Übersetzen des Programms prüft der Compiler die Richtigkeit des Sourcecodes. Diese wertvolle Hilfe lässt sich laut der achten Regel (“Lasse dir helfen”) explizit einfordern. Aber wie? Das verrät der kommende Artikel.

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
Nach oben