Post

Mein C++ Coding Style mit Erläuterungen und Erfahrungswerten, warum ich das so mache

Mein C++ Coding Style mit Erläuterungen und Erfahrungswerten, warum ich das so mache

Für die C++ Entwicklung im Team lohnt es sich, einen konsistenten Coding-Style für alle zu definieren und einzuhalten. Nachfolgend hab ich mal meine Standards (die sich über Jahre so eingeschliffen haben) aufgeschrieben.

Wozu Code-Stil-Richtlinien?

  • Lesbarkeit für andere Programmierer und einen selbst verbessern
  • Klarheit bei Variablen und Funktionen → weniger fehleranfällig → höhere Programmiergeschwindigkeit
  • Vermeidung von Programmierfehlern (die treten schnell auf, wenn der bestehende Code unklar/schwer verständlich ist)
  • Minimale Änderungen im Git (→ Whitespaces); Diffs besser lesbar!
  • Konsistente Code-Generierung durch KI Assistenten

Verzeichnisstruktur

Zum Coding-Stil gehört auch eine einigermaßen sinnige und konsistente Verzeichnisstruktur, damit sich jeder im Team gleich zurechtfindet.

Repository-Struktur

Das Repository ist wie folgt aufgebaut:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<repo-name>/
├── bin/                        # Ausgabeverzeichnis für kompilierte Binärdateien (generiert)
│   ├── debug/                  # Debug-Mode Binärdateien
│   └── release/                # Release-Mode Binärdateien
├── build/                      # Build-Skripte und Buildverzeichnisse (compiler-generiert)
├── data/                       # Projektdaten
│   ├── DB/                     # Datenbankdateien (wenn vorhanden)
│   ├── tests/                  # Testprojektdateien für automatisierte Tests
│   └── ...                     # weitere Beispiel- und Demoprojekte
├── doc/                        # Dokumentation (Modellbeschreibung, Spezifikation, Tutorial)
├── libs/                       # Alle Bibliotheken (überwiegend Git-Submodule, s.u.)
│   ├── XXXLib/                 # Submodul: typischerweise Bibliotheken
│   └── ...                     # weiterte Submodule
├── XXXSolver/                  # CLI-Anwendung (Einstiegspunkt main.cpp, ArgsParser), könnte auch ein Dienst sein
│   └── src/
├── XXXUI/                      # Qt-GUI-Anwendung (qmake-Projekt)
│   ├── resources/              # Icons, QSS-Stylesheets, Übersetzungen (.qrc)
│   └── src/
├── scripts/                    # Hilfsskripte (Testausführung, Konvertierungen)
├── TinyXMLCodeGenerator/       # Submodul: Codegenerator für XML-Serialisierer
├── .clang-format               # Verbindliche globale Formatierungsregeln (clang-format)
├── .gitmodules                 # Submodul-Konfiguration
├── CMakeLists.txt              # Top-Level-CMake-Build-Datei
└── <repo-name>-Session.pro     # Qt Creator Sitzungsdatei

Submodule: Die meisten Bibliotheken unter libs/ sowie TinyXMLCodeGenerator/ sind Git-Submodule. Änderungen daran müssen zuerst im jeweiligen Submodul-Repository committet und dann im Hauptrepository per Submodul-Update nachgezogen werden.


Struktur einer Bibliothek

Alle eigenen Bibliotheken (z. B. IBK, IntegratorFramework, …) folgen dem gleichen Aufbau:

1
2
3
4
5
6
7
8
LibraryName/
├── src/                        # Alle Quelldateien flach (keine Unterverzeichnisse)
│   ├── <LIB>_ClassName.h       # Header: ein Namespace-Präfix + Klassenname
│   ├── <LIB>_ClassName.cpp     # Implementierung
│   └── cg/                     # (Optional) Für code-generierte Serialisierer (nicht manuell editieren!)
├── doc/                        # Bibliotheksspezifische Dokumentation
├── CMakeLists.txt              # CMake-Build-Definition der Bibliothek
└── LibraryName.pro             # qmake-Projektdatei (für Qt Creator)

Wichtige Regeln:

  • Quelldateien liegen flach in src/ – keine weiteren Unterverzeichnisse.
  • Das Unterverzeichnis cg/ enthält ausschließlich generierte Dateien (durch TinyXMLCodeGenerator). Diese Dateien dürfen nicht manuell bearbeitet werden.

Allgemeine Konventionen für alle (neuen) Projekte

Einrückung und Zeilenlängenbegrenzung

  • Tabs für Einrückung verwenden (vereinfacht Diffs, kleinere Quelltexte)
  • 1 Tab = 4 Spaces

Begründung:

Diese Einstellung bietet auch auf großen Bildschirmen mit hoher Auflösung einen guten Überblick über den Beginn und das Ende eines eingerückten Scopes. Die Verwendung von Tabs ermöglicht die Zusammenarbeit mit anderen Programmierern, die unbedingt 2 oder 8 Zeichen als Tabbreite haben wollen (bei uns nicht, oder?).

  • Angehängte Whitespaces entfernen
  • Die Zeilenlänge ist zwar nicht strikt begrenzt, aber auf heutigen Monitoren/Auflösungen sind 120 Zeichen eine sinnvolle Obergrenze

Zeichenkodierung und Zeilenenden

  • Zeilenenden LF (Unix/Linux) verwenden, siehe auch Git-Repo-Konfiguration
  • UTF-8-Kodierung für Dateien verwenden (vor allem wichtig, wenn Umlaute im Quelltext auftauchen)

Dateinamen

Schema: <namespace>_<Klassenname>.*

Wichtig: Auf Groß- und Kleinschreibung achten (Linux!)

Beispiel:

  • Namespace: IBK
  • Klassenname: ConfigParser

Dateien:

  • IBK_ConfigParser.h
  • IBK_ConfigParser.cpp

Tipp: Jede Klasse in eine eigene Datei auslagern.

Begründung:

  • Vereinfacht Git-Versionierung
  • Vereinfacht Refactoring (Umbenennen von Klassen und dazugehörigen Dateinamen einfach via Skript, da gleich benannt)
  • Reduziert Merge-Konflikte
  • Beschleunigt das Erstellen während der Entwicklung (kleinere Übersetzungseinheiten)

Header Guards

Früher hat man in Header-Dateien sogenannten Header-Guards verwendet. Wir verwenden stattdessen das #pragma once

1
2
3
4
#pragma once

... header code

was von allen gängigen Compilern unterstützt wird, siehe Wikipedia, #pragma once.


Include-Reihenfolge

In Header-Dateien und Implementierungsdateien gilt folgende Reihenfolge der #include-Anweisungen, wobei Gruppen durch eine Leerzeile getrennt werden:

  1. In .cpp-Dateien: der eigene Header immer zuerst (vor allen anderen Includes); dient der Prüfung, ob die Header-Datei selbst alle benötigten Includes selbst einbindet
  2. C++-Standardbibliothek (<vector>, <string>, …)
  3. Falls verwendet alle Qt-Header (<QString>, <QWidget>, …)
  4. IBK-Bibliotheken (<IBK_Path.h>, <IBK_Parameter.h>, …)
  5. Weitere Drittanbieter-Bibliotheken (in < >)
  6. Eigene Header im selben Verzeichnis (in " ")

Innerhalb jeder Gruppe sind die Headers alphabetisch (Groß-/Kleinschreibung ignoriert) sortiert.

Beispiel (.cpp-Datei):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "RECO_Model.h"           // eigener Header immer zuerst

#include <map>
#include <string>
#include <vector>

#include <IBK_assert.h>
#include <IBK_messages.h>
#include <IBK_Path.h>

#include <tinyxml.h>

#include "RECO_Constants.h"
#include "RECO_Project.h"

Spezifische C++-Programmierrichtlinien

Namespaces

Je nach Bibliothek sollte man einen Namespace verwenden (IBK oder BTP), um Funktionen/Typen für größere Projekte zu kapseln.

Niemals in irgendeinem Quelltext using namespace XXX schreiben – nicht einmal für den Namespace std!

Das ist eigentlich eher eine Vorsichtsmaßnahme, aber in größeren Projekten mit vielen Programmierern ist es leicht möglich, dass Namenskonflikte auftreten. Außerdem erhöht dies die Lesbarkeit des Quelltextes, da bereits im Code ersichtlich ist, aus welcher Bibliothek eine bestimmte Klasse/Funktion stammt.

In reinem C-Code ohne Namespaces sind Namespace-Präfixe zu verwenden, also bspw. für die MCD Bibliothek heißen Funktionen dann mcdMyMicroCodeStruct.


Klassen- und Variablenbenennung

  • CamelCase für Variablen-/Typnamen, Beispiel: thisNiceVariable
  • Typ-/Struct-/Klassennamen beginnen mit Großbuchstaben, Beispiel: MyClassType (Zusammen mit Namespaces macht sich das sehr gut bei der Code-Vervollständigung)
  • Membervariablen beginnen mit m_, Beispiel: m_myMemberVariableObject
  • Die Namensgebung erfolgt vorzugsweise auf Englisch, alternativ auf Deutsch. Niemals gemischt.
  • Getter-/Setter-Funktionen entsprechend dem Qt-Stil

Beispiel:

1
2
3
4
std::string m_myStringMember;

const std::string & myStringMember() const;
void setMyStringMember(const std::string & str);

Niemals getXXX schreiben!

Dafür gibt es mehrere Gründe:

  1. In Member-Funktionen sieht man anhand des m_-Präfixes sofort, dass es sich um Membervariablen handelt. Alle anderen sind lokale (oder sehr selten globale) Variablen.
  2. Die Autovervollständigung zeigt beim Tippen von m_ nur die verfügbaren Membervariablen (keine Verwechslung mit lokalen Variablen/Member-Funktionen).
  3. Man braucht sich keine unterschiedlichen Namen für Variablen und Getter-/Setter-Funktionen zu merken.
  4. Effizienz: Dieses Benennungsschema wird direkt von den Qt Creator Refactoring-Funktionen unterstützt.

Ausrichtung von Membervariablen

Membervariablen werden spaltenweise ausgerichtet, um die Deklaration übersichtlich zu gestalten. Bei Zeigervariablen wird das * an den Variablennamen (rechts) geschrieben und zusammen mit dem Variablennamen ausgerichtet – nicht mit dem Typnamen.

1
2
3
std::string                     m_name;                    // XML:E:required
std::vector<std::string>        m_vec;                     // XML:E
SomeClass                       *m_ptrToClass = nullptr;   // XML:E

Die Verwendung des * direkt in der Spalte mit den Variablen deutet sofort und gut sichtbar auf die Zeigervariable hin (da ja bei rohen Zeigen in C/C++ immer besondere Sorgfalt angebracht ist).

Die Anmerkungen // XML:E (Element) und // XML:A (Attribut) für die XML-Serialisierung (siehe TinyXMLCodeGenerator unten) werden ebenfalls vertikal ausgerichtet (alle in derselben Spalte).


Empfehlungen für gut lesbaren Quelltext

Gut lesbarer Quelltext zeichnet sich auch durch eine gewisse Kompaktheit aus – wenn man nur am Scrollen ist, verliert man leicht den Überblick.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Kurze Funktionsdeklarationen dürfen die { in der selben Zeile haben
void someFunction(int t) {
    // Einrückung mit einem Tab-Zeichen
    for (int i=0; i<t; ++i) { // öffnende geschweifte Klammer ebenfalls in dieser Zeile
        // Code
    }

    // Längere for-Klauseln über mehr als eine Zeile sollten die
    // öffnende { in die nächste Zeile setzen, um den Beginn des Scopes zu markieren.
    for (std::vector<double>::iterator it = m_localVec.begin();
        it != m_localVec.end(); ++it)
    {
        // Code
    }

    // Ähnliche Regeln gelten für if und andere Klauseln, zum Beispiel:
    if (value == 15 || takeNextStep ||
        (firstStepCounter > 15 && repeat))
    {
        // Code
    }
}

// Längere Funktionsdeklarationen über zwei oder mehr Zeilen sollten
// die { in die nächste Zeile setzen, um den Beginn des Scopes klar zu markieren.
void someFunctionWithManyArguments(const std::vector<double> & vec1,
    const std::vector<double> & vec2,
    const std::vector<double> & vec3)
{
    // Code
}


// Der folgende Quelltext zeigt typische Einrückungsregeln
void indentationAndOtherRules() {

    // Empfehlung: 'if (' statt 'if( ' verwenden
    if (someCondition) {
        // Code
    }
    // else in eine separate Zeile setzen und Kommentare
    // wie hier vor der else-Klausel platzieren, um zu dokumentieren,
    // was im else-Block gemacht wird
    else {
       //
    }

    // Leerzeichen zwischen den durch ; getrennten Teilen in for-Schleifen setzen
    for (i=0; i<20; ++i) {
    }

    // switch-Klauseln wie im folgenden Beispiel einrücken
    switch (condition) {
         case Well:
             // Code
             // Weiterer Code
         break; // break auf gleicher Ebene wie case

         // case-Klauseln vor dem case dokumentieren
         case Sick:
             // Code
             return "sick";

         // Wenn lokale Variablen innerhalb von switch deklariert werden,
         // einen eigenen Scope öffnen
         case DontKnow: {
             int var1;  // lokale Variable, nur gültig für diese case-Klausel
             // Code
         }
         break;

         // Bei vielen kurzen case-Klauseln können
         // korrekt eingerückte Einzeiler verwendet werden
         case ABitSick        : return "a bit sick";
         case ALittleBitSick  : return "a little bit sick";
         case QuiteWell       : break;

         default: ; // Die default-Klausel nur implementieren, wenn nötig.
                    // Andernfalls erinnert der Compiler an vergessene Klauseln
                    // (was sehr hilfreich sein kann).
    } // switch (condition)
    // In langen verschachtelten Scopes das Ende des Scopes wie oben dokumentieren

    // Ein weiteres Beispiel für dokumentierte verschachtelte Scopes
    for (k=0; k<10; ++k) {
        for (j=k; j<10; ++j) {

            // Viel Code

        } // for (j=k; j<10; ++j)

    } // for (k=0; k<10; ++k)
}

Enumerationen

Enumerationen sollten CamelCase-Namen haben und ein Präfix enthalten – dies macht es bei Nutzung der Code-Vervollständigung einfacher, passende Enum-Werte zu finden.

1
2
3
4
5
6
enum ModelType {
  MT_Standard,
  MT_MoreComplicated,
  MT_ReallyReallyDifficult,
  NUM_MT
};

Der Präfix sollte sich aus den Großbuchstaben des Enumerations-Typs ergeben – so muss man sich den Präfix nicht extra merken. Beispiel: DeviceConnection → DC_ModBus, DC_TCPIP

Der _t-Suffix (MyStruct_t) sollte nur in reinem C-Code verwendet werden.


API-Dokumentation

Doxygen-Stil, im Header am besten wie folgt:

1
2
3
4
5
6
7
8
9
/*! Kurzbeschreibung der Funktion.
    Längere mehrzeilige Dokumentation der Funktion.
    \param arg1 Das erste Argument.
    \param temperature Eine Temperatur in [C]
*/
void setParams(int arg1, double temperature);

/*! Mittlere Temperatur in [K]. */
double m_meanTemperature;

In der cpp möglichst keine /* ...*/ verwenden:

1
2
3
4
5
6
7
8
9
	// Kommentar einzeilig
	if (someCondition) {
	}

	// Kommentar mehrzeilig
	// Zeile 1
	// zeile 2
	if (someCondition1) {
	}

Bei physikalischen Parametern immer die physikalischen Einheiten im Kommentar angeben. Physikalische Variablen zur Verwendung in einer Berechnung sollten immer in der Basis-SI-Einheit gespeichert werden.


C++11/14-Features

Lambdas

Lambdas sollte man eher vermeiden.

Ausnahme wären kleine Sortierlambda-Funktionen, aber größere Funktionen sollten klassich in separate Funktionen ausgelagt werden. Falls die Programmlogik nur lokal in einer CPP gebraucht wird (wie das bei Lambdas quasi impliziert ist), kann man diese Funktionen auch mit einem static markieren. Für komplexere Programmlogik mit Klassenbezug kann man auch eine private Memberfunktion verwenden (ggfs. auch statisch, falls keine Zugriff auf Membervariablen notwendig ist).

Hintergrund ist, dass Lambdas schwer zu debuggen sein können und das Refaktoring mit Hausmitteln (Qt Creator) bei Lambdas gelegentlich fehlschlägt. Außerdem ist die Lesbarkeit des Codes schlechter und bei inkorrekter Verwendung der Lambda-Argument, kann das ein Performanceproblem sein oder unerwünsche Nebeneffekte haben.

C++ Schlüsselworte

  • auto eher vermeiden, außer für Iteratortypen (die ja sehr mühselig zu tippen sind und auch eher länglich sind)
  • override immer verwenden
  • default- und delete-Attribute für Konstruktoren/Memberfunktionen können verwendet werden

Weitere C++ Features

  • Direkte Initialisierung der Membervariablen verwenden (bool m_isValid = false;)
  • Neue for-Schleifen nur verwenden, wenn es sich nicht um komplexe Strukturen oder Klassen handelt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<std::string> stringvec;
stringvec.push_back(...);
...
for (const std::string & s : stringvec) {  // <-- neue Schleifensyntax
    ...
}

// anstelle von

for (std::vector<std::string>::const_iterator it = stringvec.begin();
    it != stringvec.end(); ++it)
{
    ...
}

Werkzeuge und Automatisierung

clang-format

Die Datei .clang-format im Wurzelverzeichnis enthält verbindliche Formatierungsregeln. Qt Creator kann so konfiguriert werden, dass diese Regeln beim Kopieren/Einfügen und Tippen von Text angewendet werden.

Die Einstellung des Qt Creators sollte man aber nicht so setzen, dass die Dateien komplett nach den .clang-format Regeln umformatiert wird. Das klappt häufig nicht, bzw. hängt auch von der installierten CLang-Version ab. Daher besser nur die Einrückungsvorschläge bei der Quelltextbearbeitung (und natürlich die globale Einrückung mit Tabs) konfigurieren.


TinyXMLCodeGenerator für die XML-Serialisierer (Code-Generierung)

Datenmodellklassen können automatisch generierte XML-Serialisierungsfunktion nutzen. Dafür müssen die Quelltextdateien mit Kommentaren annotiert werden. Das Hilfprogramm TinyXMLCodeGenerator parst den Quelltext und diese speziellen Kommentare und generiert XML Lese-/Schreibcode. Die generierten Dateien liegen in libs/<LibName>/src/cg/ und tragen den Präfix cg_.

Dateien in cg/ niemals manuell bearbeiten – Änderungen werden beim nächsten Generierungslauf überschrieben.

Die Ausführung des TinyXMLCodeGenerator ist Teil der CMake build tool chain. Falls man den doch mal manuell ausführen will, TinyXMLCodeGenerator via Script ausführen (sollte im build-Verzeichnis liegen und die entsprechenden Kommandozeilenargumente enthalten):

1
2
cd scripts
./runCodeGenerator.sh

Die Annotationen // XML:E (als XML-Element serialisieren) und // XML:A (als XML-Attribut) sowie // XML:E:required (Pflichtfeld) in den Headerdateien steuern die Codegenerierung.

Klassen-Hilfsmakros (CodeGen_Macros.h)

Die Datei cg/CodeGen_Macros.h stellt Makros bereit, die in den Headerdateien der Datenmodellklassen verwendet werden. Sie deklarieren die Schnittstellen, die der Code-Generator als implementiert voraussetzt.

Makro Bedeutung
CODEGEN_READWRITE Deklariert readXML(const TiXmlElement*) und writeXML(TiXmlElement*) als public. Standardfall: beide Funktionen werden direkt vom Generator erzeugt.
CODEGEN_READWRITE_PRIVATE Deklariert readXMLPrivate / writeXMLPrivate und setzt voraus, dass die Klasse eigene readXML / writeXML-Funktionen hat (für Klassen, die zusätzliche Logik beim Lesen/Schreiben benötigen).
CODEGEN_READWRITE_IFNOTEMPTY(X) Wie CODEGEN_READWRITE, aber writeXML() schreibt das Element nur, wenn das Objekt sich von einem default-konstruierten X() unterscheidet.
CODEGEN_READWRITE_IFNOT_INVALID_ID Wie CODEGEN_READWRITE, aber writeXML() schreibt das Element nur, wenn m_id != INVALID_ID.
CODEGEN_COMP(X) Deklariert operator!= und operator== für die Klasse X. Die Implementierung des operator== wird ebenfalls generiert, die Implementierung des operator!= muss selbst definiert werden.
CODEGEN_COMPARE_WITH_ID Deklariert operator==(unsigned int) – Vergleich der Member-Variable m_id mit einer numerischen ID.
CODEGEN_COMPARE_WITH_NAME Deklariert operator==(const std::string&) – Vergleich der Member-Variable m_name (std::string) mit einem Namen.
CODEGEN_LESS_ID(X) Deklariert operator< basierend auf m_id – nützlich für Sortierung in STL-Containern.

Außerdem werden zwei Typaliase definiert:

  • IDType als unsigned int – signalisiert dem Code-Generator eine spezielle Behandlung (Überprüfung auf INVALID_ID beim Schreiben).
  • QuotedString als std::string – signalisiert dem Code-Generator, dass Zeichenketten mit zusätzlichen Anführungszeichen serialisiert werden sollen.

Einige der Makros gehen davon aus, dass Klassen die Membervariablen m_id (unsigned int) und m_name (std::string) deklarieren, was in vielen Datenmodellen sinnvolle Identifikatoren sind.

Die Definition eines der CODEGEN_READWRITE* Makros ist notwendig, damit der CodeGenerator die Datei überhaupt berücksichtigt.


XML-Annotationen

Membervariablen in Datenmodellklassen werden mit einem Zeilenkommentar annotiert, der dem Code-Generator mitteilt, wie die Variable in XML serialisiert werden soll. Das allgemeine Format ist:

1
// XML:<Typ>[:<Flag>[:<Flag>...]]

Typen:

Annotation Bedeutung
// XML:A Variable als XML-Attribut serialisieren (im öffnenden Tag der Klasse, z. B. name="Wert").
// XML:E Variable als XML-Kindelement serialisieren (eigener XML-Tag, z. B. <Length unit="m">3.5</Length>).
// XML:C Variable (muss std::string sein) wird als XML-Kommentar direkt nach dem öffnenden Tag geschrieben.

Flags (kombinierbar durch :):

Flag Bedeutung
required Das Attribut / Element ist Pflicht. Fehlt es beim Einlesen, wird eine Exception ausgelöst.
write-if-different Das Element / Attribut wird nur geschrieben, wenn es sich vom Wert eines default-konstruierten Objekts unterscheidet. Setzt voraus, dass die Klasse einen Defaultkonstruktor hat.
tag=<TagName> Verwendet <TagName> statt des aus dem Variablennamen abgeleiteten Standardnamens als XML-Tag. Nötig bei Namenskonflikten oder aus Lesbarkeitsgründen (z. B. tag=pH statt Ph).

Beispiele:

1
2
3
4
5
6
std::string          m_name;                             // XML:A:required
double               m_openPorosity;                     // XML:E:required
double               m_closedPorosity = 0;               // XML:E
double               m_pH = -888;                        // XML:E:write-if-different:tag=pH
InitialInventory     m_initialInventoryOpenPore;         // XML:E:tag=InitialInventoryOpenPore:write-if-different
std::string          m_comment;                          // XML:C

Aus dem Variablennamen (m_xyz) leitet der Generator den XML-Tag-Namen ab, indem er das m_-Präfix entfernt und den ersten Buchstaben großschreibt: m_openPorosity → <OpenPorosity>. Mit tag= kann davon abgewichen werden. Bei Attributen (XML:A) beginnt der Name mit einem Kleinbuchstaben.

Vererbte Membervariablen (aus einer Basisklasse) können ebenfalls annotiert werden, indem der Kommentar //:inherited vorangestellt wird:

1
//:inherited std::string m_name; // XML:A:required

Die so geerbten Member-Variablen werden ebenfalls serialisiert.


Keyword-Listen

Enumerationen können mit // Keyword:-Kommentaren versehen werden, damit der Code-Generator eine zentrale Umwandlungstabelle zwischen Enum-Werten und Zeichenketten erzeugt.

Format:

1
EnumerationsWert,  // Keyword: <Schlüsselwort> [<Einheit>] <#RRGGBB> {Standardwert} 'Beschreibung'

Alle Felder außer dem Schlüsselwort sind optional, wobei aber die Reihenfolge strikt eingehalten werden muss.

  • [Einheit] – z. B. [C], [m], [---] – die physikalische Einheit des Parameters (in eckigen Klammern)
  • <#RRGGBB> – eine Farbzuweisung in HTML-Notation (in spitzen Klammern)
  • {Standardwert} – ein numerischer Standardwert in der angegebenen Einheit (in geschweiften Klammern)
  • 'Beschreibung' – ein beschreibender Text (in einfachen Anführungszeichen)

Beispiel:

1
2
3
4
5
enum Parameter {
    P_RelTol,           // Keyword: RelTol         [---] 'Relative tolerance for solver error check.'
    P_MaxTimeStep,      // Keyword: MaxTimeStep    [min] 'Maximum permitted time step for integration.'
    NUM_P
};

Der Sentinel-Wert NUM_<Präfix> markiert das Ende der Enumeration und wird vom Generator erkannt und benötigt. Er darf keinen // Keyword:-Kommentar haben – oder nur einen, wenn er als letzter Eintrag den Enum-Zähler mit abdecken soll.

Der Generator erzeugt die Datei cg_NAMESPACE_KeywordList.cpp mit den Tabellen sowie – falls noch nicht vorhanden – den Header NAMESPACE_KeywordList.h.

Typische Enumerationsnamen sind Parameter, Flags, IntParameter. Der Präfix ergibt sich überlicherweise aus den Initialen der CamelCase-Enumerationsnamen, bspw. PerformanceOptions → PO_xxx.


Die Klasse KeywordList

NAMESPACE::KeywordList ist eine rein statische Hilfsklasse, die Enum-Werte und ihre zugehörigen Schlüsselwörter, Einheiten, Farben und Standardwerte verwaltet. Sie wird hauptsächlich in den generierten Serialisierern verwendet, steht aber auch im gesamten übrigen Code zur Verfügung.

Wichtige statische Methoden:

Methode Bedeutung
Keyword(enumtype, t) Gibt den Schlüsselwort-String für den Enum-Wert t der Kategorie enumtype zurück.
Enumeration(enumtype, kw) Gibt den Enum-Wert (als int) für den Schlüsselwort-String kw zurück. Wirft eine Exception bei ungültigem Schlüsselwort.
Description(enumtype, t) Gibt die Beschreibung des Enum-Werts zurück (oder das Schlüsselwort, falls keine Beschreibung angegeben).
Unit(enumtype, t) Gibt die Standardeinheit als Zeichenkette zurück (leer, wenn nicht angegeben).
Color(enumtype, t) Gibt den Farb-Hash-String zurück (Standard: "#FFFFFF").
DefaultValue(enumtype, t) Gibt den Standardwert als double zurück (NaN wenn nicht angegeben).
KeywordExists(enumtype, kw) Prüft, ob kw in der Enumeration enumtype vorhanden ist.
CategoryExists(enumtype) Prüft, ob die Kategorie enumtype überhaupt bekannt ist.
setParameter(para[], enumtype, n, val) Hilfsmethode zum Befüllen eines IBK::Parameter-Arrays mit Name, Wert und Einheit aus der Keyword-Liste.
setIntPara(para[], enumtype, n, val) Analog zu setParameter, aber für IBK::IntPara-Arrays.

Der Parameter enumtype ist stets der vollständige C++-Typname der Enumeration einschließlich Klassenname, z. B. "SolverParameter::Parameter".

Verwendungsbeispiel:

1
2
3
4
5
6
7
8
9
// Schlüsselwort-String für einen Enum-Wert lesen
const char * kw = NAMESPACE::KeywordList::Keyword("SolverParameter::Parameter", SolverParameter::P_MaxTimeStep);

// Enum-Wert aus einem Schlüsselwort-String (z. B. aus XML) rekonstruieren
SolverParameter::Parameter p = (SolverParameter::Parameter)
    NAMESPACE::KeywordList::Enumeration("SolverParameter::Parameter", "MaxTimeStep");

// IBK::Parameter-Array mit Standardwerten initialisieren
NAMESPACE::KeywordList::setParameter(m_para, "SolverParameter::Parameter", SolverParameter::P_MaxTimeStep, 60.0);

Tipps und Tricks

Zugriff auf nicht initialisierte Variablen beim Debuggen erkennen

Der Zugriff auf nicht initialisierte Membervariablen – oder noch schlimmer: auf Membervariablen, die mit Standardwerten initialisiert wurden (wodurch obligatorische Initialisierungsschritte übersprungen werden) – kann während der Entwicklung/des Debuggings schwer nachzuverfolgen sein.

Daher sollten Variablen, die initialisiert werden müssen, mit leicht erkennbaren Werten initialisiert werden. Mit C++11-Features sollte man Code wie folgt schreiben:

1
2
3
4
5
6
7
8
9
class SomeClass {
    ...

    // nullptr ist gut geeignet, um Zeiger als "nicht initialisiert" zu erkennen
    SomeType    *m_ptrToSomeType = nullptr;

    // Eine unwahrscheinliche "magische Zahl" verwenden, um zu sehen, dass eine Variable (noch) nicht initialisiert ist
    double      m_cachedCalculationValue = 999;
};
This post is licensed under CC BY 4.0 by the author.