Modernisierung von C++ Projekten
Verglichen mit vielen anderen Sprachen entwickelt sich C++ eher langsam, vor allem durch seinen Status als internationaler Standard und einen starken Fokus auf Rückwärtskompatibiltät.
Dennoch gibt es immer wieder neue Features, welche bisher verfügbare Konstrukte, Muster, oder externe Bibliotheken durch bessere Alternativen ablösen bzw. ersetzen können.
So brachte uns C++-11 range-based `for`, eine `std::to_string`-Funktion, und Lambda-Funktionen. Im momentan aktuellen C++-23 Standard wurde `std::print` hinzugefügt, eine moderne Alternative zu `printf` welche typsicher und somit wesentlich weniger fehleränfallig ist, gleichzeitig aber auch lesbarer und kompakter als die I/O-Streams Bibliothek (`std::cout` usw.). In all diesen Fällen helfen die neuen Features, Code lesbarer und leichter wartbar zu machen, indem Expressivität und Abstraktionslevel erhöht werden, sowie bestimmte Programmierfehler ausgeschlossen werden.
Bei Legacy-Projekten, welche über viele Jahre oder Jahrzehnte gewachsen sind und vielleicht ursprünglich noch aus der Zeit vor C++-11 stammen, oder sogar aus Vor-Standardisierungs-Zeiten, kann ein Modernisierungs-Durchgang mithilfe ausgewählter neuer Sprachfeatures ein guter erster Schritt sein, um den Code zugänglicher zu machen. Das "Grundrauschen" an Komplexität und Fehleranfälligkeit wird verringert, und das wiederum macht es dann leichter, weitergehende Verbesserungen und Aufräumarbeiten anzugehen, z.B. auf Architektur-Ebene.
Gleichzeitig sind es gerade diese Projekte, bei denen bereits eine solche Grundmodernisierung schon eine große Herausforderung darstellt: Die Menge an Code ist groß, oft mehrere hunderttausend oder sogar millionen Zeilen, und die Abdeckung durch automatisierte Tests nur sehr spärlich, wenn überhaupt vorhanden.
Hier kann uns Clang-Tidy helfen.
Das Clang-Tidy-Werkzeug
Clang-Tidy ist Teil der LLVM-Compiler-Suite, und kann damit auf ein robustes System zum Parsen von C++-Code zurückgreifen. Das Werkzeug kann aber problemlos auch zusammen mit anderen Compilern verwendet werden.
Clang-Tidy ist zunächst einmal ein Linter, also ein Werkzeug, welches Code statisch analysiert, um Programmierfehler und suboptimale Konstrukte zu finden. Das passiert mittels einer Reihe von "Checks", die jeweils für ein ganz bestimmtes Muster oder Problem ausgelegt sind, z.B. gibt es einen Check, der Assertions mit Seiteneffekten findet.
Was das Werkzeug besonders macht ist die Fähigkeit, viele Verbesserungen und Fixes automatisch durchzuführen, wodurch der Arbeitsaufwand deutlich verringert wird. Auch ist es erweiterbar mit eigenen Checks.
Die Checks sind in verschiedene Kategorien eingeteilt, wie z.B. `bugprone` (fehleranfällige Konstrukte/Muster), `performance` (Konstrukte mit Einfluss auf die Ausführungsgeschwindigkeit), `readability` (schwer lesbare Konstrukte/Muster), usw.
Für CodeKeepers ist besonders die `modernize`-Kategorie interessant, welche die automatische Umwandlung von älteren Konstrukten/Mustern in durch neuere C++-Standards bereitgestellte Alternativen behandelt.
Alle oben genannten Beispiele können von Clang-Tidy abgedeckt werden:
herkömmliche Schleifen können in range-based `for` umgewandelt werden,
`boost::lexical_cast` durch `std::to_string` ersetzt werden, usw.
Auch für nach C++-11 eingeführte Features gibt es entsprechende Checks, da das Werkzeug mit der Weiterentwicklung der Sprache ebenfalls kontinuierlich erweitert wird.
So gibt es z.B. `modernize-use-starts-ends-with` für die neuen String-Methoden in C++-20, `modernize-use-std-print` für die oben bereits erwähnte `std::print`-Funktion aus C++-23, usw. Einige Checks haben zusätzliche Konfigurationseinstellungen, die verschiedene Aspekte der automatisch durchgeführten Änderungen steuern.
Clang-Tidy ausführen
Clang-Tidy kann entweder als Standalone-Befehlszeilenwerkzeug aufgerufen werden, oder über Integration in IDEs und Text-Editoren, falls verfügbar. Um den Code erfolgreich analysieren zu können, muss das Werkzeug die Compiler-Konfiguration kennen (Compiler-Flags, Pfade zu Include-Verzeichnissen, usw).
Beim Verwenden einer IDE-Integration wird das automatisch abgedeckt, für den manuellen Aufruf müssen wir es selbst angeben (dazu unten mehr). Außerdem muss eine Liste der durchzuführenden Checks spezifiziert werden, und es gibt noch weitere Optionen um z.B. bestimmte Dateien von der Analyse auszuschließen.
Nutzung einer IDE-Integration
Die Nutzung einer IDE-Integration ist wahrscheinlich am komfortabelsten, um einen ersten Eindruck des Werkzeugs zu bekommen, da kaum Konfiguration notwendig ist (im wesentlichen nur eine Auswahl der auszuführenden Checks) und gefundene Probleme bzw. mögliche Modernisierungen und Fixes direkt im Quelltext angezeigt und von dort aus auch selektiv angewendet werden können.
Hier ein Beispiel aus der Qt Creator IDE:
Ich habe eine "klassische" `for`-Schleife verwendet, und dann Clang-Tidy laufen lassen. Der Code wird markiert und ein Verbesserungsvorschlag angezeigt. Mit einem Klick auf das Glühbirnensymbol kann ich die automatische Umwandlung in einen range-based `for`-Loop direkt ausführen. Ähnliche Integrationen sind auch für andere IDEs und Editoren verfügbar, beispielsweise für CLion, VS Code, Visual Studio, usw.
Aufruf über die Befehlszeile
Möchte man das Werkzeug lieber händisch oder per Skript aufrufen, lohnt es sich, zunächst eine sogenannte Compilation Database erstellen zu lassen. Dabei handelt es sich um ein JSON-basiertes Standard-Format für die Aufzeichnung von Compiler-Aufruf-Befehlszeilen im Rahmen eines Build-Systems, mit einer Auflistung der individuellen Parameter pro Translation Unit (in der Regel eine C++-Quellcodedatei). Es kann von den meisten gängigen Build-Systemen erstellt werden.
Bei CMake z.B. geht das mit der Option `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`.
Clang-Tidy sucht nach der Datenbank unter dem Dateinamen `compile_commands.json` im Verzeichnis, von dem aus das Werkzeug ausgeführt wird. Angenommen, wir haben eine Datei `test.cpp` mit der folgenden "klassischen" Schleife:
for (
std::vector<int>::iterator it = nums.begin();
it != nums.end();
++it)
{
std::cout << "Test" << *it << '\n';
}
Um den o.g. `modernize-loop-convert`-Check darauf anzuwenden, können wir Clang-Tidy wie folgt aufrufen:
clang-tidy -checks='-*,modernize-loop-convert' test.cpp
Wir geben die zu überprüfende Datei an, sowie die Liste an gewünschten Checks.
Mit der Angabe `-*` deaktivieren wir zunächst alle standardmäßig aktivierten Checks, danach aktivieren wir dann den gewünschten `modernize-loop-convert` Check. Wir können hier beliebig viele Checks auflisten, oder auch ganze Gruppen inkludieren, z.B. alle "modernize"-Checks mittels `modernize-*`.
Daraufhin erhalten wir die folgende Ausgabe:
nwuttke@LaptopNW:~$ clang-tidy -checks='-*,modernize-loop-convert' test.cpp
1 warning generated.
/home/nwuttke/test.cpp:10:3: warning: use range-based for loop instead [modernize-loop-convert]
10 | for (std::vector<int>::iterator it = nums.begin();
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| (int & num : nums)
11 | it != nums.end();
| ~~~~~~~~~~~~~~~~~
12 | ++it)
| ~~~~~
13 | {
14 | std::cout << "Test" << *it << '\n';
| ~~~
| num
Ein Aufruf ohne automatische Anpassung kann hilfreich sein, um die Möglichkeiten des Werkzeugs zu explorieren. Denkbar ist auch eine Integration in die Continuous Integration Pipeline, um z.B. die Verwendung bestimmter veralteter Konstrukte gänzlich zu verbieten, nachdem das System einmal überarbeitet wurde.
Um den Code automatisch anzupassen, müssen wir nur die `-fix`-Option angeben:
nwuttke@LaptopNW:~$ clang-tidy -checks='-*,modernize-loop-convert' -fix test.cpp
1 warning generated.
/home/nwuttke/test.cpp:10:3: warning: use range-based for loop instead [modernize-loop-convert]
10 | for (std::vector<int>::iterator it = nums.begin();
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| (int & num : nums)
11 | it != nums.end();
| ~~~~~~~~~~~~~~~~~
12 | ++it)
| ~~~~~
13 | {
14 | std::cout << "Test" << *it << '\n';
| ~~~
| num
/home/nwuttke/test.cpp:10:7: note: FIX-IT applied suggested code changes
10 | for (std::vector<int>::iterator it = nums.begin();
| ^
/home/nwuttke/test.cpp:14:28: note: FIX-IT applied suggested code changes
14 | std::cout << "Test" << *it << "\n";
| ^
clang-tidy applied 2 of 2 suggested fixes.
Um die manuelle Ausführung weiter zu vereinfachen, gibt es noch die Möglichkeit, die Liste der Checks sowie andere Einstellungen über eine Konfigurationsdatei namens `.clang-tidy` zu spezifizieren.
Außerdem bietet das Projekt ein Python-Skript namens `run-clang-tidy.py`. Dieses lässt Clang-Tidy automatisch über alle in der Compilation Database aufgeführten Dateien laufen, und parallelisiert dabei sogar die Ausführung über mehrere Prozesse, um den Vorgang zu beschleunigen.
Für eine vollständige Beschreibung aller Optionen und Möglichkeiten empfehle ich einen Blick in die offizielle Dokumentation des Werkzeugs.
Fazit
Clang-Tidy ist ein vielseitiges Werkzeug zur statischen Analyse und automatischen Anpassung von C++-Code. Es bietet unter anderem eine Reihe an Möglichkeiten zur Ersetzung veralteter Muster und Konstrukte mit moderneren Alternativen, welche in neuen C++-Standards eingeführt wurden. Durch die automatische Anpassung kann dies einiges an Aufwand und Zeit bei der Modernisierung von Legacy-Code sparen.