In diesem Beitrag untersuchen wir, warum die prozedurale C-Architektur von EDG auf monolithische Dispatcher und miteinander verknüpfte Header-Dateien setzt – und warum ein Refactoring dieser Strukturen Performance und Wartbarkeit tatsächlich verschlechtern würde.
Werfen wir einen Blick auf CppDepends Smart Code City für das EDG-Front-End:
Was die visuellen Muster im EDG-Front-End offenbaren
Betrachtet man die gerenderte Stadtübersicht von EDG, werden mehrere makro-architektonische Merkmale sofort sichtbar:
- Kuppeln zeigen hohe Kopplung in den Kernbezirken: Man beachte die Cluster rot getönter Gebäude mit Kuppeln. In CppDepends 3D City markiert eine Kuppel gezielt eine Funktion mit hoher Kopplung – sie stützt sich also stark auf viele andere Teile der Codebasis oder interagiert mit ihnen. In EDG greifen Kernaufgaben wie Auswertung von Ausdrücken, Typauflösung und Deklarationsverarbeitung ständig auf globale Zustandstabellen und AST-Strukturen zu. Diese hohe gegenseitige Abhängigkeit (efferente Kopplung) lässt Kuppeln in allen primären Codebezirken auftauchen.
- Monolithische Dispatch-Säulen vs. flache Nachbarschaften: Mehrere markante, hohe Monolithen mit breiter Grundfläche ragen über die umliegenden flachen Blöcke hinaus. Sie repräsentieren Funktionen wie
process_expr_work: große prozedurale C-Switch-Schleifen mit Tausenden von Codezeilen und hoher zyklomatischer Komplexität. - Farbclusterung (Rot-/Orange-Dominanz in den zentralen Modulen): Während Hilfsmodule kühlere grün/türkise Oberflächen zeigen, weist der zentrale C++-Front-End-Bezirk (
cpfe) durchgehend warme Farben auf. In Standardsoftware signalisiert weit verbreitetes Rot dringenden Refactoring-Bedarf; in einem industriellen C-Compiler beweist es visuell, wie eng die gesamte Parsing- und Auswertungspipeline integriert sein muss, um komplexe C++-Semantik effizient zu verarbeiten. - Grüne und blaue Begrenzungslinien zeigen starke Modulgesundheit: Man beachte, dass die Umrandungen der Hauptmodulzonen durchgehend grün und blau bleiben. Während einzelne Funktionen im Inneren hochgradig komplex sind, spiegeln CppDepends Begrenzungslinien die architektonische Gesundheit auf oberster Ebene, Regelkonformität und saubere Modulgrenzen wider.
Der monolithische Kern: process_expr_work
In CppDepends Smart Code City sticht die Funktion process_expr_work in src/interpret.c als eine der größten Strukturen der gesamten EDG-Codebasis hervor:
- Lines of Code (LOC): 2.911
- Zyklomatische Komplexität (CC): 1.229
- Efferente Kopplung (EC): 506
Für ein klassisches statisches Analysewerkzeug lösen diese Kennzahlen sofortige Refactoring-Alarme mit höchster Priorität aus. Das Verständnis des prozeduralen C-Designs von EDG zeigt jedoch, warum diese Struktur sowohl beabsichtigt als auch effektiv ist.
Warum process_expr_work so lang ist: Die prozedurale C-Dispatch-Schleife
Da EDG historisch in prozeduralem C entwickelt wurde, verwendet es keine objektorientierten C++-Features wie Klassenvererbung oder virtuelle Funktionstabellen.
Stattdessen basieren die AST-Traversierung (Abstract Syntax Tree) und die Ausdrucksauswertung auf explizitem, tag-basiertem Dispatching:
- Tag-basierte Diskriminatoren: AST-Knoten sind rohe C-Struct-Zeiger, die mit Enum-Kennungen für Ausdrucksarten versehen sind (z. B.
EXPR_BINARY,EXPR_CAST,EXPR_CALL). - Zentralisierter prozeduraler Switch:
process_expr_workfungiert als zentrale Interpreter-Dispatch-Schleife. Sie führt eine einzige, massive C-switch-Anweisung aus, die über Hunderte von AST-Knotenarten verzweigt. - Lokale Registerallokation & null Call-Overhead: Die Auswertungsschritte innerhalb eines einzigen Funktionsscopes zu halten, vermeidet das Aufsetzen von Stack-Frames und indirekte Funktionsaufrufe während rekursiver AST-Traversierungen. Zudem können Standard-C-Compiler die Registerallokation über lokale temporäre Variablen hinweg optimieren.
Wie Clang dasselbe Verhalten implementiert
Clang parst und wertet Ausdrücke für exakt dieselbe C++-Sprachspezifikation aus, verfolgt jedoch einen objektorientierten C++-Ansatz. Der Vergleich zwischen EDGs prozeduralem C-Dispatch und Clang zeigt, wie Sprachparadigmen die Signatur der statischen Analyse verändern, während die zugrunde liegenden architektonischen Anforderungen erhalten bleiben:
| Metrik / Merkmal | EDG (process_expr_work) | Clang (ExprConstant.cpp) |
|---|---|---|
| Sprachparadigma | Prozedurales C | Modernes C++ (objektorientiert) |
| Architekturmuster | Zentralisierte Tagged-switch-Dispatch-Schleife | AST-Visitor-Pattern (ConstStmtVisitor) |
| Code-Struktur | Einzelne monolithische Funktion (2.900+ LOC) | Verteilt über eine Klassenhierarchie (VisitExpr, VisitCast usw.) |
| Signatur der statischen Analyse | Vertikale Komplexität: hohe CC (1.229) in einer Funktion | Horizontale Komplexität: geringere CC pro Methode, aber höhere Klassenzahl |
| Call Stack & Speicher | Geringer Stack-Overhead; optimierte Registerwiederverwendung | Virtuelle/überladene Methodenaufrufe über Klassenhierarchien |
| Kontextübergabe | Direkter Zugriff auf lokalen Funktionsscope & Zustand | Explizite Kontextobjekte (EvalInfo&) werden an Methoden übergeben |
Wohin die Komplexität wandert
In EDG: Die Komplexität ist vertikal und lokalisiert. Sie sammelt sich in process_expr_work und erzeugt eine hohe zyklomatische Komplexität (1.229) in einer einzigen Übersetzungseinheit.
In Clang: Die Komplexität ist horizontal und verteilt. Clang teilt die Auswertungslogik auf einzelne Visit*-Methoden in mehreren Evaluator-Klassen auf (IntExprEvaluator, FloatExprEvaluator, LValueExprEvaluator).
Clang vermeidet zwar 3.000 Zeilen lange Einzelfunktionen, doch die gesamte Systemkomplexität bleibt identisch, da die Auswertung von C++-Ausdrücken – mit allen impliziten Konvertierungen, Operatorüberladungen, Template-Instanziierungen und Dialekt-Sonderfällen – exakt denselben Regelsatz berücksichtigen muss.
Ob als 2.900 Zeilen langer prozeduraler C-Switch in EDG oder als 50 Visitor-Methoden über eine Klassenhierarchie in Clang – die zugrunde liegende Domänenkomplexität bleibt unverändert.
Abhängigkeitszyklen auf Dateiebene: Die Matrix (DSM) lesen
Der Wechsel von der Smart Code City zu CppDepends Dependency Structure Matrix (DSM) offenbart eine weitere wichtige Signatur der statischen Analyse: dichte Abhängigkeitszyklen auf Dateiebene zwischen den zentralen Übersetzungseinheiten.
Betrachtet man das DSM-Raster, bilden Dateien wie src/class_decl.c, src/declarator.c, src/decls.c und src/expr.c eine markante stark zusammenhängende Komponente (Strongly Connected Component, SCC):
- Blaue Zellen: Kennzeichnen unidirektionale, geschichtete Abhängigkeiten (Datei A hängt von Datei B ab, aber nicht umgekehrt).
- Rote Zellen: Markieren bidirektionale Abhängigkeiten (Zyklen), bei denen zwei Dateien direkt oder indirekt auf die Symbole und Definitionen der jeweils anderen angewiesen sind.
- Zellenzahlen: Zeigen das exakte Kopplungsgewicht der Member (z. B. verwendet
declarator.c31 Member ausdecls.hund 5 Member ausdeclarator.h).
Die gegenseitige Abhängigkeit von class_decl.c && declarator.c
In der traditionellen Enterprise-C/C++-Architektur gelten zirkuläre Abhängigkeiten zwischen Übersetzungseinheiten als schwerwiegender Designfehler.
Den Zyklus sezieren: Pragmatische gegenseitige Rekursion vs. zufällige Kopplung
Mit Code Quest in CppDepend können wir die präzisen Funktionsaufrufe untersuchen, die den bidirektionalen Zyklus zwischen src/class_decl.c und src/declarator.c antreiben.
Indem wir Methoden in class_decl.c abfragen, die von declarator.c verwendet werden – und umgekehrt –, decken wir zwei unterschiedliche Kopplungskategorien auf: pragmatische Zyklen, die fundamentale Sprachregeln widerspiegeln, und zufällige Zyklen, die sich leicht refaktorisieren lassen.
Funktionen aus declarator.c, die von class_decl.c verwendet werden:
Funktionen aus class_decl.c, die von declarator.c verwendet werden:
Die Absicht hinter dem Abhängigkeitszyklus class_decl.c ↔ declarator.c
1. Pragmatische gegenseitige Rekursion
(Als bewusstes Design beibehalten)
declarator()scan_lambda_declarator()abstract_class_diagnostic()
2. Zufällige Kopplung / Refactoring-Kandidaten
(Kandidaten für Entkopplung)
f_consume_any_stray_microsoft_rparen()in_cli_property_or_event_definition()in_static_cli_property_or_event_definition()
1. Die pragmatische Seite: Sprachinhärente gegenseitige Rekursion
Beim Parsen von modernem C++ sind Klassendefinitionen und Declarator-Parsing inhärent miteinander verknüpft. Die Code-Quest-Abfragen bringen Kernfunktionen an die Oberfläche, die die Abhängigkeit rechtfertigen:
A. declarator.c → class_decl.c
abstract_class_diagnostic(...): Wird von declarator.c aufgerufen, wenn eine Funktions- oder Variablendeklaration versucht, eine abstrakte Klasse zu instanziieren oder zurückzugeben. Der Declarator-Parser muss Klassenlayout-Metadaten aus class_decl.c abfragen, um rein virtuelle Funktionen auszuwerten und Diagnosen auszugeben.
B. class_decl.c → declarator.c
declarator(...) & scan_lambda_declarator(...): Werden von class_decl.c aufgerufen, sobald eine Klassendefinition auf Member-Funktions-Deklaratoren, Zeiger auf Member oder verschachtelte Lambdas trifft.
delayed_scan_of_exception_spec(...): Exception-Spezifikationen an Member-Funktionen können erst nach dem Parsen der relevanten Klassenkontexte verarbeitet werden.
Der Versuch, diese Kernaufrufe durch übermäßige Abstraktion zu eliminieren, würde künstliche Wrapper-Schichten und spürbare Performance-Einbußen zur Laufzeit einführen. In der Compiler-Technik ist diese gegenseitige Rekursion pragmatisches, domänengetriebenes Design.
2. Die Refactoring-Chancen: Zufällige Utility-Kopplung
Code Quest isoliert jedoch auch Funktionen im Zyklus, die wenig mit der zentralen C++-Parsing-Grammatik zu tun haben und eine zufällige architektonische Kopplung darstellen:
Betrachtet man die 6 zurückgegebenen Methoden in class_decl.c:
- Dialekt-/Parser-Helfer:
f_consume_any_stray_microsoft_rparen() - CLI-/Erweiterungs-Helfer:
in_cli_property_or_event_definition()undin_static_cli_property_or_event_definition()
Warum diese unnötige Zyklen erzeugen
f_consume_any_stray_microsoft_rparen() ist ein Parser-Recovery-Hilfsmittel für MSVC-spezifische Syntax-Eigenheiten. Es in class_decl.c zu platzieren, zwingt declarator.c dazu, von der gesamten Klassendeklarations-Einheit abzuhängen – nur um ein einzelnes Token zu konsumieren!
Wie man den zufälligen Zyklus entfernt
- Utility-Helfer extrahieren: Das Verschieben der Dialekt-Recovery-Routinen (
f_consume_...) und CLI-Zustandsprüfungen in ein dediziertes Utility-Modul (z. B.src/parser_utils.codersrc/lex_helpers.c) durchbricht die künstliche Verbindung. - Ergebnis: Die Kopplungsfläche der 6 Methoden schrumpft, der zufällige Zyklus wird eliminiert – während die notwendigen, hochperformanten C++-Grammatik-Dispatch-Schleifen erhalten bleiben.
Zentrale Erkenntnis für Architekten
Warnungen statischer Analysewerkzeuge sollten niemals pauschal nach der Regel „alle Zyklen beheben“ behandelt werden.
Mit Werkzeugen wie CppDepend und Code Quest können Teams zwischen domänennotwendigen Zyklen (wie declarator() ↔ abstract_class_diagnostic()), die in eine C++-Parser-Engine gehören, und zufällig platzierten Utilities (wie MSVC-Token-Recovery-Helfern) unterscheiden, die sauber herausrefaktorisiert werden können.
Da C++ Inline-Klassendefinitionen, Member-Funktionen, verschachtelte Klassen und nachgestellte Rückgabetypen erlaubt, sind das Parsen von Klassendefinitionen und das Declarator-Parsing fundamental gegenseitig rekursiv.
Warum C-Header-Mechaniken die Zyklen verschärfen
Da EDG in prozeduralem C umgesetzt ist, stützt es sich auf gemeinsam genutzte Header-Definitionen (class_decl.h, declarator.h, decls.h).
- Gemeinsame C-Typen: Datenstrukturen wie
a_type_ptr,a_decl_ptrunda_declaratormüssen in beiden Übersetzungseinheiten sichtbar sein. - Kreuz-Inklusion & Vorwärtsdeklarationen: Während sauberes C++ Interfaces oder Pimpl-Muster zur Entkopplung von Headern nutzen könnte, verlassen sich C-Codebasen auf sich gegenseitig inkludierende Header und vorwärtsdeklarierte rohe Struct-Zeiger. In der DSM zeigt sich das als dichte rote Cluster über
class_decl.c,declarator.cunddecls.c.
Zentrale Erkenntnis für die Matrix-Analyse
In Standard-Softwarearchitekturen bedeutet ein rotes Quadrat in einer DSM: „Brich dies mit Interface-Abstraktion auf.“
In einem industriellen Compiler-Front-End spiegelt ein roter Matrix-Cluster zwischen class_decl, declarator, decls und expr schlicht die Tatsache wider, dass sich die C++-Grammatikregeln nicht sauber in einen reinen gerichteten azyklischen Graphen (DAG) schichten lassen. Die Zyklen sind kein zufälliger architektonischer Verfall – sie spiegeln die gegenseitige Rekursion wider, die in die C++-Sprachspezifikation selbst eingebaut ist.
Das Qualitäts-Dashboard: 23.000+ Low-Severity-Issues entschlüsseln
Bei der Betrachtung von CppDepends übergeordnetem Qualitäts-Dashboard für EDG zeigt sich ein auffälliges statistisches Paradoxon:
Das EDG-Qualitäts-Dashboard auf einen Blick
| Metrik / Kategorie | Wert / Details |
|---|---|
| Gesamte Codezeilen (LOC) | 372.391 |
| Issues gesamt | 23.985 |
| Low-Severity-Issues | 22.620 (~94 % des Gesamtwerts) |
| High-Severity-Issues | 1.344 |
| Verletzte kritische Regeln | 0 |
| Quality-Gate-Status | 5/5 BESTANDEN |
Auf den ersten Blick mag die Zahl von 23.985 Issues alarmierend wirken. Die Aufschlüsselung zeigt jedoch, warum EDG eine Gesamtbewertung von Rating B erreicht und alle 5 Quality Gates sauber besteht – ohne einen einzigen Fail- oder Warn-Status auszulösen.
1. Schweregrad-Aufschlüsselung: Rauschen vs. kritisches Risiko
Das Dashboard klassifiziert Issues in verschiedene Schweregrade:
- Kritische Issues: 0 (keine schweren Speicherlecks, kein undefiniertes Verhalten, keine systemgefährdenden Fehler).
- High-Severity-Issues: 1.344 (hauptsächlich konzentriert in Funktionen mit hoher zyklomatischer Komplexität wie
process_expr_work). - Medium-Severity-Issues: 21
- Low-Severity-Issues: 22.620 (~94 % aller gemeldeten Issues)
Der überwiegende Teil der Gesamtzahl stammt aus Low-Severity-Code-Style-Richtlinien, etwa den statischen MISRA-C/C++-Prüfungen.
2. Der MISRA-C-Effekt: Formatierungsregeln skalieren mit den LOC
Da EDG in prozeduralem C geschrieben ist, erzwingen statische Analyseregeln wie MISRA C strikte syntaktische Richtlinien über alle 372.391 Codezeilen hinweg. Ein klassisches Beispiel:
„Auf ein if(condition)-Konstrukt soll eine zusammengesetzte Anweisung (geschweifte Klammern {}) folgen.“
In klassischem prozeduralem C sind einzeilige if-Anweisungen ohne explizite Klammern üblich:
// Löst bei jedem Vorkommen eine Low-Severity-MISRA-C-Style-Verletzung aus
if (expr == NULL) return;
// MISRA-C-konforme Form
if (expr == NULL) {
return;
}
Wenn eine Codebasis mit über 370.000 LOC optionale Klammern weglässt oder Makro-Stilmuster in Tausenden von switch-Fällen verwendet, markiert CppDepend jedes einzelne Vorkommen als Issue.
Obwohl diese Regeln Zehntausende einzelner Warnungen erzeugen, ist ihr Beitrag zur technischen Schuld pro Instanz minimal – deshalb zeigt Code Implementation 33.280 Issues, während Code Safety 0 Issues zeigt.
3. Hohe Kommentardichte spiegelt starke Dokumentation wider
Eine weitere herausragende Kennzahl auf dem Dashboard ist die Kommentarrate:
- Kommentaranteil: 48,44 %
- Kommentarzeilen: 349.808
Nahezu 50 % der gesamten EDG-Codebasis bestehen aus Kommentaren. Auf jede Zeile ausführbaren prozeduralen C-Codes kommt nahezu eine volle Zeile Dokumentation, die Eigenheiten der C++-Standardkonformität, Compiler-Flags und Dialekt-Sonderfälle erklärt.
Diese außergewöhnliche Kommentardichte erklärt, warum die Codebasis trotz struktureller Komplexität in den AST-Traversierungs-Dispatchern für ihre Domäne hervorragend wartbar bleibt.
Zentrale Erkenntnis
Das Qualitäts-Dashboard zeigt, warum rohe Issue-Zahlen ohne Schweregrad-Klassifizierung irreführend sein können.
EDGs 22.620 Low-Severity-Issues sind kosmetische Stil- und Formatierungsverletzungen (wie MISRA-Klammerkonformität). Da die Engine null Verletzungen kritischer Regeln und eine Kommentardichte von 48 % aufweist, erfüllt sie CppDepends oberste Quality Gates – bei gleichzeitig industrietauglichem C++-Parsing.
Fazit: Statische Metriken mit Domänenarchitektur in Einklang bringen
Die Analyse einer ausgereiften, industrietauglichen Codebasis wie dem EDG-C/C++-Front-End mit CppDepend bietet eine wertvolle Lektion in Softwarearchitektur: Statische Analysemetriken sind diagnostische Indikatoren, keine absoluten Dogmen.
Lektionen aus der EDG-Analyse
| Zentrale Erkenntnis | Architektonische Einsicht |
|---|---|
| 1. Kontext vor Metrik-Dogma | Eine 2.900 Zeilen lange Funktion mit CC 1.229 ist nicht immer technische Schuld – in einem C-basierten AST-Interpreter vermeidet sie Call-Stack-Allokation und Register-Overhead. |
| 2. Zyklustypen unterscheiden | Gegenseitige Rekursion beim Grammatik-Parsing (declarator ↔ class_decl) ist pragmatisch, während verstreute MSVC-Token-Helfer behebbare technische Schuld sind. |
| 3. Makro- vs. Mikro-Gesundheit | Trotz hoher lokaler Funktionskomplexität belegen grüne Begrenzungslinien und eine Kommentardichte von 48 % diszipliniertes Engineering auf Systemebene. |
Zentrale Engineering-Erkenntnisse
- Hohe Komplexität kann bewusstes Design sein: Funktionen wie
process_expr_workerreichen astronomische Werte bei der zyklomatischen Komplexität (1.229), weil sie die Auswertung von C++-AST-Ausdrücken in hochdurchsatzfähigen prozeduralen C-Dispatch-Schleifen zentralisieren. Die Aufspaltung jedes Zweigs in separate Funktionen würde Parameter-Drilling, Stack-Frame-Overhead und Cache-Misses erhöhen, ohne die inhärente Komplexität der C++-Sprachspezifikation zu reduzieren. - Nicht alle Abhängigkeitszyklen sind gleich: Durch DSM-Visualisierung und Code-Quest-Abfragen haben wir gesehen, dass Abhängigkeitszyklen auf Dateiebene oft eine doppelte Natur haben:
- Domänengetriebene Zyklen: Die gegenseitige Rekursion zwischen
class_decl.cunddeclarator.cspiegelt direkt die rekursiven Regeln der C++-Grammatik wider (Klassendefinitionen enthalten Declaratoren; Declaratoren benötigen Klassenkontext). - Zufällige Zyklen: Das Platzieren von Token-Parser-Helfern (wie
f_consume_any_stray_microsoft_rparen()) in zentralen Klassenmodulen erzeugt unnötige dateiübergreifende Kopplung, die sauber refaktorisiert werden kann.
- Domänengetriebene Zyklen: Die gegenseitige Rekursion zwischen
- Schweregrad-Klassifizierung verhindert Metrik-Panik: EDGs Qualitäts-Dashboard verzeichnet knapp 24.000 Issues. Doch mit 0 verletzten kritischen Regeln, 5/5 bestandenen Quality Gates und ~94 % Low-Severity-Warnungen aus strengen Style-Prüfungen (wie MISRA-C-Klammerkonformität) erreicht das Projekt ein solides Rating B. Zusammen mit einer Kommentardichte von 48,44 % demonstriert die Codebasis außergewöhnliche Wartungshygiene – trotz ihrer rohen strukturellen Größe.
Das letzte Wort für Softwarearchitekten
Statische Analysewerkzeuge wie CppDepend sind von unschätzbarem Wert, um strukturelle Heatmaps, architektonische Grenzen und verborgene Kopplung sichtbar zu machen. Das Ziel der statischen Analyse in komplexen Engineering-Domänen ist jedoch nicht, um jeden Preis alle Metriken auf Grün zu zwingen.
Durch die Kombination automatisierter Visualisierung mit einem tiefen Verständnis der Domänenanforderungen – ob beim Bau eines Compiler-Front-Ends, einer Game-Engine oder eines Echtzeit-Betriebssystems – können Architekten zwischen echter, wachsender technischer Schuld und pragmatischen, hochperformanten Designentscheidungen unterscheiden.
