Linux, PHP und Git sind bekannte Projekte, die in C entwickelt wurden. OpenOffice, Firefox, Clang und Photoshop hingegen sind in C++ geschrieben. Das zeigt, dass sich beide Sprachen gut für die Entwicklung komplexer Anwendungen eignen. Zu beweisen, dass eine Sprache grundsätzlich besser als die andere ist, dürfte daher wenig sinnvoll sein. Interessanter ist die Frage, welche Gründe für die Wahl der einen oder anderen Sprache sprechen.
Als ich zum ersten Mal dieMeinungvon Linus Torvalds über C++ las, widersprach ich ihm als C++-Entwickler vollständig. Doch es handelt sich um die Sichtweise des leitenden Entwicklers des Linux-Kernels und von Git – sie verdient daher eine genauere Betrachtung.
Nachdem ich seine Stellungnahme erneut gelesen hatte, konnte ich dieser Aussage durchaus zustimmen:
inefficient abstracted programming models where two years down the road you notice that some abstraction wasn't very efficient, but now all your code depends on all the nice object models around it, and you cannot fix it without rewriting your app.C++ bietet zweifellos mehr Möglichkeiten, eleganten und gut strukturierten Code zu schreiben. Das hat jedoch seinen Preis: Änderungen und Refactorings können aufwendig sein. Das bedeutet aber nicht, dass man deshalb eine andere Sprache wählen muss. Jede Sprache und jede Bibliothek bringt Kompromisse mit sich. Entscheidend ist, die Auswirkungen späterer Änderungen auf eine über Jahre gewachsene C++-Codebasis möglichst gering zu halten.
Analysieren wir denGitQuellcode mitCppDepend, betrachten einige Designmerkmale und vergleichen C und C++ in zwei Bereichen:
- Verständlichkeit.
- Umgang mit Änderungen.
Modularität: physisch vs. logisch
Modularität ist eine Technik des Softwaredesigns, bei der Software in klar getrennte Teile gegliedert wird. Modularer Code lässt sich leichter verwalten und warten.
Ein Projekt lässt sich auf zwei Arten modularisieren:
- Physisch: mithilfe von Verzeichnissen und Dateien. Diese Form der Modularität wird vom Betriebssystem bereitgestellt und kann unabhängig von der Programmiersprache eingesetzt werden.
- Logisch: mithilfe von Namespaces, Komponenten, Klassen und Structs. Diese Technik hängt von den Möglichkeiten der jeweiligen Sprache ab.
Bei der Entwicklung in C wird der Code hauptsächlich physisch strukturiert. Verzeichnisse dienen dazu, Module voneinander zu trennen. Hier sehen Sie den Abhängigkeitsgraphen zwischen einigen Git-Verzeichnissen:

In C++ können wir dagegen Namespaces verwenden, um die Codebasis zu modularisieren. Diese Konstrukte werden von der Sprache selbst bereitgestellt. Im vorherigen Graphen könnten die dargestellten Bereiche also Namespaces statt Verzeichnisse repräsentieren.
Die Wahl zwischen diesen beiden Ansätzen hat folgende Auswirkungen:Verständlichkeit: Der logische Ansatz ist verständlicher, weil die Modularität direkt durch Sprachkonstrukte definiert wird. Schon beim Lesen des Codes lässt sich erkennen, zu welchem Modul ein Codeelement gehört.
Umgang mit Änderungen: Gutes Design entsteht meist in mehreren Iterationen. Beim physischen Ansatz können die Auswirkungen von Designänderungen deutlich begrenzter sein als beim logischen Ansatz. Häufig genügt es, eine Funktion oder Variable in eine andere Datei oder eine Datei in ein anderes Verzeichnis zu verschieben.
In C++ kann eine solche Änderung dagegen viele Codestellen betreffen, weil die logische Modularität über Sprachkonstrukte umgesetzt wird und deshalb Änderungen am Code selbst erfordert.
Kapselung: Klasse vs. Datei
In C++ bezeichnet Kapselung das Zusammenfassen von Daten und Funktionen in einer Einheit, der Klasse. Die Daten sind dabei nicht direkt zugänglich, sondern werden über die in der Klasse definierten Funktionen angesprochen.
Auch in C lässt sich Kapselung erreichen, allerdings eher auf physischem Weg: Eine Datei kann Funktionen und die von ihnen verwendeten Daten enthalten, während sich die Sichtbarkeit von Funktionen und Variablen mit dem Schlüsselwort „static“ einschränken lässt.
Git nutzt diese Technik, um Funktionen und Variablen zu verbergen. Prüfen wir das, indem wir nach statischen Funktionen suchen:
from m in Methods where m.IsStatic select m
Die Treemap eignet sich sehr gut, um einen Überblick über die von einer CQLinq-Abfrage gefundenen Codeelemente zu erhalten; die blauen Rechtecke stellen die Ergebnisse dar.

Fast alle Funktionen sind als static deklariert und damit nur innerhalb der jeweiligen Übersetzungseinheit sichtbar. Dasselbe gilt für die Variablen.
from f in Fields where f.IsStatic select f
Verständlichkeit:Die Kapselungsmechanismen von C++ verbessern Lesbarkeit und Struktur des Codes. C arbeitet auf einer niedrigeren Ebene und setzt stärker auf physische als auf logische Strukturierung.
Umgang mit Änderungen: Muss geändert werden, wo eine Variable oder Funktion gekapselt ist, kann dies in C sehr einfach sein; in C++ kann die Änderung dagegen zahlreiche Codestellen betreffen.
Polymorphie vs. Selection Idiom
Polymorphie bedeutet, dass sich Code, Operationen oder Objekte abhängig vom jeweiligen Kontext unterschiedlich verhalten.
Diese Technik ist in C++-Projekten weit verbreitet. Doch wie sieht es in C aus?
In prozeduralen Sprachen wird die Auswahl typischerweise mit Schlüsselwörtern wie „switch“, „if“ oder sogar „goto“ umgesetzt. Dieser Ansatz erhöht jedoch tendenziell die zyklomatische Komplexität des Codes.
Suchen wir im Git-Quellcode nach komplexen Funktionen.

Obwohl Git gut konzipiert ist, können viele Funktionen als komplex gelten. Das liegt teilweise am intensiven Einsatz von Kontrollflussanweisungen wie „if“, „switch“ und „goto“. In C++ lässt sich die Codekomplexität dagegen mithilfe von Polymorphie reduzieren.
Verständlichkeit: Polymorphie ermöglicht es, ein bestimmtes Verhalten in einer Klasse zu isolieren. Das verbessert Lesbarkeit und Kohäsion des Codes.
Umgang mit Änderungen: Wird bei Polymorphie ein weiteres Verhalten ergänzt, kann dafür eine zusätzliche Klasse erforderlich sein. Beim Selection Idiom genügt dagegen häufig ein weiterer case-Zweig in der switch-Anweisung.
Vererbung vs. Komposition
Git verwendet hauptsächlich Structs, um die von Funktionen verarbeiteten Daten zu definieren. Suchen wir nach allen verwendeten Structs:
from t in Types where t.IsStructure select t

Interessanterweise befinden sich nahezu alle Daten in Structs. Zur Überprüfung können wir nach allen öffentlichen, nicht konstanten Variablen primitiver Typen suchen, die sich nicht innerhalb eines Structs befinden:
from f in Fields where f.IsPublic && f.IsPrimitiveType
&& !f.IsStatic && !f.IsConst
select f

Nur wenige Variablen erfüllen diese Abfrage – ein positiver Aspekt des Git-Designs.
Wie lässt sich nun ein Struct erweitern? In C können wir Komposition verwenden, etwa beim Struct „remote“, auf das zahlreiche andere Structs verweisen.

In C++ können Structs zusätzlich durch Vererbung erweitert werden; beispielsweise könnte known_remote von remote erben.
Verständlichkeit: Vererbung kann Beziehungen zwischen Daten verständlicher machen, sollte jedoch mit Bedacht und nur für echte „ist-ein“-Beziehungen eingesetzt werden.
Umgang mit Änderungen: Vererbung führt zu engerer Kopplung, sodass Änderungen viele Codestellen betreffen können.
Fazit:
C++ bietet mehr Möglichkeiten, eleganten und gut strukturierten Code zu schreiben. Das hat jedoch seinen Preis: Änderungen und Refactorings können schwieriger werden.
Refactoring setzt voraus, den bestehenden Code vor einer Änderung zu verstehen. C-Programme können schwerer zu verstehen, dafür aber leichter zu verändern sein. Ein C++-Projekt kann stärker strukturiert sein als ein C-Projekt, Änderungen können jedoch mehr Aufwand verursachen.
Wie lässt sich die Auswirkung von Änderungen in C++ begrenzen?
Ein guter Weg, die Auswirkungen von Änderungen bei einem OOP-Ansatz zu begrenzen, besteht darin, Design Patterns anzuwenden – insbesondere die Prinzipien loser Kopplung und hoher Kohäsion –, damit Änderungen auf klar abgegrenzte Bereiche beschränkt bleiben.
Ein weiterer wirkungsvoller Ansatz ist der Einsatz generischer Programmierung und moderner C++-Praktiken. Generische Programmierung kann flexibler als OOP sein und dabei helfen, die Auswirkungen von Änderungen am C++-Code zu begrenzen.
