Einleitung: Warum Legacy-Code in C++ eine Herausforderung für Profis darstellt
Die Arbeit mit vererbtem Code in C++ ist nicht nur eine Aufgabe, sondern eine Kunst. Legacy-Projekte, die vor 10–15 Jahren geschrieben wurden, enthalten oft Raw Pointer, lange Funktionen, Präprozessor-Makros und fehlende Modultests. Alles von Grund auf neu zu schreiben, ist jedoch ein Luxus, den sich Unternehmen selten leisten können. Fortgeschrittenes Refactoring ermöglicht es, den Code schrittweise zu modernisieren, technische Schulden zu reduzieren und die Leistung zu steigern, ohne die Entwicklung zu unterbrechen.
In diesem Artikel betrachten wir fortgeschrittene Refactoring-Techniken für erfahrene C++-Entwickler: von der Beseitigung von Makros bis zur Einführung von RAII und Smart Pointern sowie Strategien zur sicheren Änderung der Architektur.
1. Analyse und Kartierung von Abhängigkeiten
Vor jedem Refactoring muss verstanden werden, wie der aktuelle Code aufgebaut ist. Verwenden Sie dazu statische Analyse (z. B. Clang Static Analyzer oder Cppcheck) und Tools zur Visualisierung von Abhängigkeiten (Doxygen + Graphviz).
1.1. Erstellung eines Aufrufgraphen
Erstellen Sie einen Aufrufgraphen für die Schlüsselmodule. Dies hilft, Folgendes zu identifizieren:
- Zyklische Abhängigkeiten zwischen Modulen;
- „Gottobjekte“ (God classes), die zu viel wissen;
- Toten Code, der nie aufgerufen wird.
// Beispiel: Verwendung von Doxygen zur Generierung eines Graphen// In den Funktionskommentaren geben wir Abhängigkeiten an/** * @brief Verarbeitet eine Benutzeranfrage * @param request - Anfragestruktur * @return true, wenn die Verarbeitung erfolgreich ist * @see UserManager, RequestValidator */bool ProcessRequest(const Request& request);1.2. Erstellung eines Test-Harness
Ohne Tests ist Refactoring wie ein Gang durch ein Minenfeld. Beginnen Sie mit dem Schreiben von Charakterisierungstests (characterization tests), die das aktuelle Verhalten des Codes festhalten. Verwenden Sie Google Test oder Catch2 für C++.
// Beispiel eines Charakterisierungstests für eine alte FunktionTEST(LegacyModuleTest, CalculateDiscount_ReturnsCorrectValue) { // Arrange LegacyCalculator calc; // Act double result = calc.CalculateDiscount(100.0, "VIP"); // Assert — aktuelles Verhalten festhalten, auch wenn es unlogisch erscheint ASSERT_DOUBLE_EQ(result, 85.0);}2. Ersetzen von Makros durch constexpr- und Inline-Funktionen
Makros (#define) sind der größte Feind der Lesbarkeit und Typsicherheit in Legacy-C++. Sie unterliegen nicht den Gültigkeitsbereichsregeln und können schwer zu findende Fehler verursachen.
2.1. Schrittweise Ersetzung
Anstatt alle Makros auf einmal zu entfernen, verwenden Sie die „Strangler-Fig“-Strategie:
- Finden Sie das Makro und identifizieren Sie alle Stellen, an denen es verwendet wird.
- Ersetzen Sie es durch eine
constexpr-Konstante oder eineinline-Funktion. - Kompilieren Sie neu und führen Sie die Tests aus.
- Entfernen Sie das ursprüngliche
#define.
// War (Legacy):#define MAX_BUFFER_SIZE 4096#define SQUARE(x) ((x) * (x))
// Wurde zu (modernes C++):constexpr size_t kMaxBufferSize = 4096;template<typename T>constexpr T Square(T x) noexcept { return x * x;}Wichtig: Für Makros, die sich wie Funktionen verhalten (mit Argumenten), verwenden Sie variadische Templates oder std::invoke, wenn das Makro __VA_ARGS__ verwendet hat.
3. Übergang von Raw Pointern zu Smart Pointern
Raw Pointer mit manueller Speicherverwaltung (new/delete) sind eine Quelle für Speicherlecks und doppelte Löschvorgänge. Fortgeschrittenes Refactoring beinhaltet die Einführung von RAII und Smart Pointern.
3.1. Analyse der Eigentümerschaft
Bevor Sie Pointer ändern, bestimmen Sie das Eigentumsmodell:
- Exklusives Eigentum — verwenden Sie
std::unique_ptr.