Ich sehe viel zu viele C-Programmierer, die C ++ hassen. Ich habe einige Zeit (Jahre) gebraucht, um langsam zu verstehen, was gut und was schlecht ist. Ich denke, der beste Weg, es auszudrücken, ist dieser:
Weniger Code, kein Laufzeitaufwand, mehr Sicherheit.
Je weniger Code wir schreiben, desto besser. Dies zeigt sich schnell bei allen Ingenieuren, die nach Spitzenleistungen streben. Man behebt einen Fehler an einer Stelle, nicht an vielen - man drückt einen Algorithmus einmal aus und verwendet ihn an vielen Stellen wieder usw. Die Griechen haben sogar ein Sprichwort, das auf die alten Spartaner zurückgeht: "Etwas mit weniger Worten zu sagen, bedeutet dass du darüber weise bist ". Tatsache ist, dass Sie sich mit C ++ bei richtiger Verwendung in weitaus weniger Code als mit C ausdrücken können, ohne die Laufzeit zu beeinträchtigen, und dabei sicherer sind (dh mehr Fehler zur Kompilierungszeit abfangen) als mit C.
Hier ist ein vereinfachtes Beispiel aus meinem Renderer : Beim Interpolieren von Pixelwerten über die Scanlinie eines Dreiecks. Ich muss von einer X-Koordinate x1 ausgehen und eine X-Koordinate x2 erreichen (von der linken zur rechten Seite eines Dreiecks). Und über jeden Schritt, über jedes Pixel, über das ich gehe, muss ich Werte interpolieren.
Wenn ich das Umgebungslicht interpoliere, das das Pixel erreicht:
typedef struct tagPixelDataAmbient {
int x;
float ambientLight;
} PixelDataAmbient;
...
// inner loop
currentPixel.ambientLight += dv;
Wenn ich die Farbe interpoliere ("Gouraud" -Schattierung genannt, wobei die Felder "Rot", "Grün" und "Blau" durch einen Schrittwert bei jedem Pixel interpoliert werden):
typedef struct tagPixelDataGouraud {
int x;
float red;
float green;
float blue; // The RGB color interpolated per pixel
} PixelDataGouraud;
...
// inner loop
currentPixel.red += dred;
currentPixel.green += dgreen;
currentPixel.blue += dblue;
Wenn ich in "Phong" -Schattierung rendere, interpoliere ich keine Intensität (ambientLight) oder Farbe (rot / grün / blau) mehr - ich interpoliere einen normalen Vektor (nx, ny, nz) und muss bei jedem Schritt neu einstellen -Berechnen Sie die Beleuchtungsgleichung basierend auf dem interpolierten Normalenvektor:
typedef struct tagPixelDataPhong {
int x;
float nX;
float nY;
float nZ; // The normal vector interpolated per pixel
} PixelDataPhong;
...
// inner loop
currentPixel.nX += dx;
currentPixel.nY += dy;
currentPixel.nZ += dz;
Jetzt wäre der erste Instinkt von C-Programmierern "Verdammt, schreibe drei Funktionen, die die Werte interpolieren, und rufe sie in Abhängigkeit vom eingestellten Modus auf". Dies bedeutet zunächst einmal, dass ich ein Typproblem habe - womit arbeite ich? Sind meine Pixel PixelDataAmbient? PixelDataGouraud? PixelDataPhong? Oh, warte, der effiziente C-Programmierer sagt, benutze eine Vereinigung!
typedef union tagSuperPixel {
PixelDataAmbient a;
PixelDataGouraud g;
PixelDataPhong p;
} SuperPixel;
..und dann haben Sie eine Funktion ...
RasterizeTriangleScanline(
enum mode, // { ambient, gouraud, phong }
SuperPixel left,
SuperPixel right)
{
int i,j;
if (mode == ambient) {
// handle pixels as ambient...
int steps = right.a.x - left.a.x;
float dv = (right.a.ambientLight - left.a.ambientLight)/steps;
float currentIntensity = left.a.ambientLight;
for (i=left.a.x; i<right.a.x; i++) {
WorkOnPixelAmbient(i, dv);
currentIntensity+=dv;
}
} else if (mode == gouraud) {
// handle pixels as gouraud...
int steps = right.g.x - left.g.x;
float dred = (right.g.red - left.g.red)/steps;
float dgreen = (right.g.green - left.a.green)/steps;
float dblue = (right.g.blue - left.g.blue)/steps;
float currentRed = left.g.red;
float currentGreen = left.g.green;
float currentBlue = left.g.blue;
for (j=left.g.x; i<right.g.x; j++) {
WorkOnPixelGouraud(j, currentRed, currentBlue, currentGreen);
currentRed+=dred;
currentGreen+=dgreen;
currentBlue+=dblue;
}
...
Spüren Sie das Chaos?
Zunächst genügt ein Tippfehler, um meinen Code zum Absturz zu bringen, da der Compiler mich im Abschnitt "Gouraud" der Funktion niemals anhält, um tatsächlich auf ".a" zuzugreifen. (Umgebungs-) Werte. Ein Fehler, der nicht vom C-Typ-System (dh während der Kompilierung) abgefangen wird, bedeutet einen Fehler, der zur Laufzeit auftritt und das Debuggen erfordert. Haben Sie bemerkt, dass ich left.a.green
bei der Berechnung von "dgreen" zugreife? Der Compiler hat es Ihnen sicherlich nicht gesagt.
Dann gibt es überall Wiederholungen - die for
Schleife gibt es so oft, wie es Render-Modi gibt, wir machen weiter "rechts minus links geteilt durch Schritte". Hässlich und fehleranfällig. Haben Sie bemerkt, dass ich "i" in der Gouraud-Schleife vergleiche, wenn ich "j" hätte verwenden sollen? Der Compiler ist wieder still.
Was ist mit dem if / else / ladder für die Modi? Was ist, wenn ich in drei Wochen einen neuen Rendermodus hinzufüge? Werde ich daran denken, den neuen Modus in allen "if mode ==" in meinem gesamten Code zu behandeln?
Vergleichen Sie nun die obige Hässlichkeit mit dieser Menge von C ++ - Strukturen und einer Template-Funktion:
struct CommonPixelData {
int x;
};
struct AmbientPixelData : CommonPixelData {
float ambientLight;
};
struct GouraudPixelData : CommonPixelData {
float red;
float green;
float blue; // The RGB color interpolated per pixel
};
struct PhongPixelData : CommonPixelData {
float nX;
float nY;
float nZ; // The normal vector interpolated per pixel
};
template <class PixelData>
RasterizeTriangleScanline(
PixelData left,
PixelData right)
{
PixelData interpolated = left;
PixelData step = right;
step -= left;
step /= int(right.x - left.x); // divide by pixel span
for(int i=left.x; i<right.x; i++) {
WorkOnPixel<PixelData>(interpolated);
interpolated += step;
}
}
Nun sieh dir das an. Wir machen keine Union-Typ-Suppe mehr: Wir haben spezifische Typen für jeden Modus. Sie verwenden ihre gemeinsamen Daten (das "x" -Feld) erneut, indem sie von einer Basisklasse ( CommonPixelData
) erben . Und das Template lässt den Compiler CREATE (das heißt, Code generieren) die drei verschiedenen Funktionen, die wir selbst in C geschrieben hätten, aber gleichzeitig sehr streng in Bezug auf die Typen sein!
Unsere Schleife in der Vorlage kann nicht fälschen und auf ungültige Felder zugreifen - der Compiler wird bellen, wenn wir dies tun.
Die Vorlage führt die übliche Arbeit aus (die Schleife wird jedes Mal um "step" erhöht) und kann dies auf eine Weise tun, die einfach KEINE Laufzeitfehler verursachen kann. Die Interpolation pro Typ ( AmbientPixelData
, GouraudPixelData
, PhongPixelData
) erfolgt mit der , operator+=()
die wir in den structs hinzufügen wird - das ist im Grunde diktieren , wie jede Art interpoliert wird.
Und sehen Sie, was wir mit WorkOnPixel <T> gemacht haben? Wir möchten unterschiedliche Arbeiten pro Typ ausführen? Wir nennen einfach eine Template-Spezialisierung:
void WorkOnPixel<AmbientPixelData>(AmbientPixelData& p)
{
// use the p.ambientLight field
}
void WorkOnPixel<GouraudPixelData>(GouraudPixelData& p)
{
// use the p.red/green/blue fields
}
Das heißt, die aufzurufende Funktion wird basierend auf dem Typ festgelegt. Zur Kompilierzeit!
So formulieren Sie es erneut:
- Wir minimieren den Code (über die Vorlage), indem wir gemeinsame Teile wiederverwenden.
- Wir verwenden keine hässlichen Hacks, wir halten ein striktes Typensystem ein, damit der Compiler uns jederzeit überprüfen kann.
- und das Beste von allem: Nichts von dem, was wir getan haben, hat eine Auswirkung auf die Laufzeit. Dieser Code wird NUR so schnell ausgeführt wie der entsprechende C-Code. Wenn der C-Code zum Aufrufen der verschiedenen
WorkOnPixel
Versionen Funktionszeiger verwendet, ist der C ++ - Code SCHNELLER als C, da der Compiler die typspezifische WorkOnPixel
Schablonenspezialisierung einfügt Anruf!
Weniger Code, kein Laufzeitaufwand, mehr Sicherheit.
Bedeutet dies, dass C ++ das A und O aller Sprachen ist? Natürlich nicht. Sie müssen noch die Kompromisse messen. Ignorante Leute verwenden C ++, wenn sie ein Bash / Perl / Python-Skript hätten schreiben sollen. Trigger-fröhliche C ++ - Neulinge erstellen tief verschachtelte Klassen mit virtueller Mehrfachvererbung, bevor Sie sie stoppen und packen können. Sie werden komplexe Boost -Metaprogramme verwenden, bevor sie erkennen, dass dies nicht erforderlich ist. Sie werden immer noch verwendet werden char*
, strcmp
und Makros, statt std::string
und Vorlagen.
Aber das sagt nichts mehr als ... schau mit wem du arbeitest. Es gibt keine Sprache, die Sie vor inkompetenten Benutzern schützt (nein, nicht einmal Java).
Lernen Sie weiter und verwenden Sie C ++ - Überdesignen Sie es einfach nicht.