C++ Core Guidelines: Unterstützende Abschnitte

Modernes C++  –  0 Kommentare

In Summe habe ich in den letzten zwei Jahren rund 100 Artikel zu den C++ Core Guidelines verfasst. Warum? Weil: "This document is a set of guidelines for using C++ well. The aim of this document is to help people to use modern C++ effectively." Hier hört meine Geschichte aber noch nicht auf. Die C++ Core Guidelines besitzen unterstützende Abschnitte (supporting sections).

100 Artikel sind eine ganze Menge. Bevor ich daher in die unterstützenden Abschnitte der Guidelines eintauche, stelle ich zwei Hilfen vor, damit du dich in den bereits existierenden Artikeln zu den Guidelines zurechtfindest.

  1. Die Kategorie C++ Core Guidelines auf meinem Blog enthält alle Artikel zu den Guidelines.
  2. Den genauen Überblick liefert der Link zu dem TOC >>Hier Starten<< aller Artikel. Hier ist auch ein Abschnitt zu den C++ Core Guidelines.

Jetzt geht es aber los. Dies sind alle unterstützenden Abschnitte:

  • A: Architectural ideas
  • NR: Non-Rules and myths
  • RF: References
  • Pro: Profiles
  • GSL: Guidelines support library
  • NL: Naming and layout rules
  • FAQ: Answers to frequently asked questions
  • Appendix A: Libraries
  • Appendix B: Modernizing code
  • Appendix C: Discussion
  • Appendix D: Supporting tools
  • Glossary
  • To-do: Unclassified proto-rules

A: Architectural ideas

Der erste Abschnitt ist sehr kurz. Er besteht nur aus drei Regeln mit ein paar Zeilen Erläuterungen. Der Fokus dieser Regel lässt sich auf alle Programmiersprachen anwenden.

A.1: Separate stable code from less stable code

Dies ist die Begründung der Regel: "Isolating less stable code facilitates its unit testing, interface improvement, refactoring, and eventual deprecation." Was bedeutet das?

Dank eines Interfaces wird der stabile vom weniger stabilen Code separiert. Dank des Interfaces wird der weniger stabile Code zum Subsystem, das sich nun in Isolation testen oder refaktorieren lässt. Nicht nur das Subsystem, sondern auch die Integration des Subsystems in die Applikation lässt sich jetzt testen. Die erste Art von Test wird typischerweise Unit-Test, die zweite Art Systemintegrationstest genannt.

Das Subsystem besitzt zwei Kanäle zur Applikation: den funktionalen und den nichtfunktionalen. Beide müssen getestet werden. Der funktionale Kanal steht für die Funktionalität der Subsysteme und der nichtfunktionale Kanal für die Ausnahmen, die passieren können und auf die die Applikation gegebenenfalls reagieren muss. Dank des Interfaces ist das Subsystem eine Implementierung des Interfaces und kann daher relativ einfach durch eine andere, stabilere Implementierung ersetzt werden.

A.2: Express potentially reusable parts as a library

Das ist einleuchtend, aber es gibt in diesem Zusammenhang deutlich schwierigere Fragen zu beantworten.

  1. Wann lassen sich Teile der Software potenziell wiederverwenden?
  2. Wann zahlen die Kosten für die Implementierung eine Bibliothek aus?
  3. Welcher ist der richtige Grad der Abstraktion?

Die drei Fragen sind recht unscharf und daher relativ schwierig zu beantworten. Dies gilt insbesondere für die dritte Frage. Trotzdem versuche ich mich.

Zuerst einmal investiere nicht allzu viel Aufwand in deinen Code, um ihn als Bibliothek wiederzuverwenden, denn es gilt allzu oft: "You aren't gonna need it" (YAGNI). Schreibe deinen Code hingegen so, dass er wiederverwendet werden könnte. Dies bedeutet, dass dein Code einfachen Charakteristiken wie Testbarkeit, Pflegbarkeit, Verständlichkeit etc. genügen sollte, denn es ist sehr wahrscheinlich, dass du oder andere Programmierer in der Zukunft an den Code Hand anlegen müssen. Oder um es mit den Worten von Philip Wadler zu sagen: "Make your code readable. Pretend the next person who looks at your code is a psychopath and he knows where you live."

"Don't repeat yourself" (DRY), wenn du dieselbe oder ähnliche Funktionalität nochmals benötigst. Nun solltest du spätestens über Abstraktion nachdenken. Wenn ich zwei ähnliche Funktionen habe, schreibe ich eine dritte Funktion, die für die Implementierung steht. Die zwei ursprünglichen Funktionen dienen mir dann nur noch als Aufruf-Wrapper für die Implementierung. Hier ist meine Idee, in Code gegossen.

std::vector<void*> myAlloc;

void* newImpl(std::size_t sz,char const* file, int line){ // (3)
static int counter{};
void* ptr= std::malloc(sz);
std::cerr << file << ": " << line << " " << ptr << std::endl;
myAlloc.push_back(ptr);
return ptr;
}

void* operator new(std::size_t sz,char const* file, int line){ // (1)
return newImpl(sz,file,line);
}

void* operator new [](std::size_t sz,char const* file, int line){ // (2)
return newImpl(sz,file,line);
}

Die überladenen new-Operatoren in der einfachen Form (Zeile 1) und in der Form für Arrays (Zeile 2) rufen die Implementierung in der Zeile 3 auf.

Ich kann die Frage 3 nicht beantworten, denn die Antwort hängt von vielen Faktoren ab. Zum Beispiel von der Domäne der Software. Soll die Software zum Beispiel auf einem Desktop, Embedded-Gerät oder Trading-Server ausgeführt werden. Die Antwort hängt von Faktoren wie der Pflegbarkeit, Testbarkeit, Skalierbarkeit oder von der Performanz ab. Sie hängt vom Kenntnisstand der Entwickler. Eventuell stellt deine Bibliothek Infrastruktur dar, auf der höhere Abstraktionen entstehen sollen, oder sie ist für Kunden geschrieben.

Wiederverwendbare Software in der Form einer Bibliothek ist um den Faktor 3 bis 4 aufwendiger als Software, die nur einmal eingesetzt wird. Hier ist meine Daumenregel: Du solltest über eine Bibliothek nachdenken, denn du weißt, dass die Funktionalität wiederverwendet wird. Du solltest eine Bibliothek implementieren, wenn du die Funktionalität mindestens zweimal wiederverwendest.

A.4: There should be no cycles among libraries

Durch Zyklen zwischen Bibliotheken wird deine Software komplizierter. Zuerst werden die Bibliotheken schwieriger zu testen und lassen sich nicht wiederverwenden. Darüber hinaus werden die Bibliotheken deutlich schwieriger zu verstehen und zu erweitern. Wenn du daher einen Zyklus entdeckst, solltest du diesen brechen. John Lakos beschreibt diese auf der Seite 185 seines Buchs "Large Scale C++ Software Design":

  • Repackage c1 and c2 so they are no longer mutually dependent.
  • Pysically combine c1 and c2 into a single component, c12.
  • Think of c1 and c2 as if they were a single component, c12.

Wie geht's weiter?

Der nächste unterstützende Abschnitt zu den Non-Rules und Mythen besitzt deutlich mehr Inhalt. Ich vermute, du kennst bereits die meisten der Non-Rules als Mythen. Lasse sie mich im nächsten Artikel entmystifizieren.