Art | Beispiel | Erläuterung |
---|---|---|
lokale Variablen | mylocalvar, my_local_var | alles klein |
Instanzvariablen | m_var | m = "member" |
Klassennamen | MyCoolClass | groß anfangen, InCaps |
Exception-Klassen | XThrewUp | wie Klassen, mit X beginnend |
Klassenvariablen | M_MyClassVar | Kombination von Klassennamen und Instanzvariablen |
Konstanten | const int MaxNum = 10; | ähnlich wie Klassenvariablen |
Methoden | calcSomeCoolValue() | inCaps, klein fangen (ausführlicher) |
Template-Parameter | <MyTypeT> | wie Klassennamen mit T am Ende |
abfragende Funktion | getVar(..) const | fragt Variable _var ab |
modifizierende Funktion | setVar(..) | |
Eigenschaft | isProperty()/hasProperty() const | |
typedef | SomeThingT | analog zu Klassennamen, mit 'T' |
Defines | #define DEBUG_ON | all-caps mit Underscores |
enum-Typen | myEnumTypeE | Suffix E; mehr zu enums & Co. |
enum-Member | COL_TRUE, COL_FALSE | wie Defines |
Pointer- oder Reference-Deklaration |
String *str; | * steht bei der Variable, nicht beim Typ |
| |
b) | "Nomen sit omen". |
|
Jeder File im Projekt muß einen eindeutigen Namen haben.
Nur wenige Klassen pro File. Falls mehrere Klassen in einem File stehen, sollen diese unmittelbar miteinander zu tun haben bzw. zu einer grözeren Einheit zusammen gehören. (Bsp: kleine Helper-Klassen, die man sonst als Klasse-in-Klasse implementieren würde.)
Klammerung wird vertikal ausgerichtet. Beispiel:
int myFunction( .. ) { if ( .. ) { for ( .. ) { .. } } else { .. } }Die Einrücktiefe pro Stufe sind 4 Spaces!
Teile größere Sinnzusammenhänge des Codes (oder auch des Headers) durch eine Leerzeile ab (quasi ein "Absatz").
Initialisiere im Konstruktor immer. Verwende Initialisierung statt Zuweisung im Konstruktor (wann immer es geht). Grund: Performanz.
Rufe im Konstruktor immer den Konstruktor der Basisklasse auf (natürlich in der Initialisierungsliste).
Deklariere immer einen Copy-Konstruktor und einen Zuweisungs-Operator. Falls die Klasse diese nicht braucht, mache sie private (Grund). Implementiere den Copy-Konstruktor / die Konvertierungskonstruktoren immer so:
A::A( const A/B &source ) { *this = source; }
Konstruktoren mit nur einem Parameter (heißen Conversion-Constructor)
müssen explicit gemacht
werden (Grund).
Außer dem Copy-Constructor! (Grund: sonst geht return-by-value und
pass-by-value nicht mehr; der Compiler darf zwar die Kopie weg-optimieren, tut
das i.A. auch, aber der Standard schreibt es so vor -- vermutlich, damit das
Programm auch auf Compilern übersetzbar ist,die diese Optimierung nicht
beherrschen.I)
Dasselbe gilt für Konstruktoren, bei denen alle Parameter bis auf
einen Default-Argumente haben.
Verwende const bzw. einfache Funktionen statt define.
Bevorzuge new anstelle von malloc (bzgl. Performanz siehe alloca).
Keine public Instanz- oder Klassenvariablen. (Außer, sie sind const)
Falls die Klasse A nur per Pointer oder Referenz im Header-File der Klasse B benutzt wird, dann verwende eine Vorwärts-Deklaration; includiere nicht den Header-File der Klasse B. Beispiel:
class A; class B { private: class A *a; }
Verändere nicht die "Bedeutung" eines Operators und beachte immer auch das semantische Gegenstück. (Beispiele)
Implementiere binäre Operatoren global (nicht als Methoden). Evtl. müssen sie friend sein. (Grund: siehe "Effective C++", Kapitel 19).
Reference-Parameter werden immer mit const deklariert, ansonsten als Pointer (d.h., sie können verändert werden) (Grund).
Benutze Namespaces.
Schreibe niemals using namespaceblub;
in einem Header-File!
Wenn Methoden überladen werden, müssen sie virtual sein.
(ARVIKA 15.1 -- weiß jemand wieso?)
Virtuelle Methoden müssen auch in Unterklassen als virtual
deklariert werden (wenn sie überladen werden)
(Grund).
Überschreibe niemals einen geerbten Default-Parameter.
Private Methoden dürfen in einer Unterklasse nicht public gemacht werden.
Vermeide Casts. Wenn doch, dann verwende die neuen C++-Casts (Grund und Beispiele).
Vorsicht bei der Definition von Cast-Operatoren! Auch hier können unbemerkt ungewollte Dinge geschehen. (Beispiel)
Mache Downcasts nur mit dynamic_cast (Grund).
Mache kein "händisches" Inlining. (Compiler-Optionen, Konstruktoren)
Header-Files mit Template-Klassen sollen keinen weiteren Code enthalten. Bei Templates steht der "Code" in einem extra File mit Suffix .hh.
Verwende die C++-Features RTTI (typeid und dynamic_cast) und Exceptions.
Vermeide geschachtelte Klassen.
Vermeide Mehrfachvererbung.
Eine Variable, die innerhalb eines for()-Konstruktes deklariert wird, gilt nur innerhalb der Schleife:
for (int i=0; i<10; i ++ ) { // i ist gültig } // i ist nicht mehr gültigGib beim Compilieren auf SGI die Option -LANG:ansi-for-init-scope an.
Schalte die Warnings des Compilers an und schreibe Warning-freien Code.
Keine temporären Objekte in Funktionsaufrufen (Grund).
Instanzvariablen, für die es keine get-Methode gibt, dürfen protected gemacht werden.
Tue keine "richtige" Arbeit in Konstruktoren oder verwende Exceptions (Grund).
Dokumentiere Null-Statements im Source (Grund).
Schreibe const-korrekten Code. (Das macht am Anfang Mühe ;-) ) Achte von Anfang an auf const-correctness! (Im Nachhinein ist es praktisch unmöglich.) Vergiss auch das const auf der "rechten" Seite von Funktionen nicht.
Verwende das assert()-Makro) freizügig. (Setze unbedingt -DNDEBUG im Release!)
Verwende nicht malloc direkt, sondern den malloc-Wrapper aus defs.h.
Verwende wirklich unsigned int, wenn Du den negativen Wertebereich n bei gcc).
icht brauchst. Das ist meistens in Schleifen der Fall. Überlege Dir
das auch bei jedem Prototypen, der int bekommt.
Schalte die entsprechende Warning an (-Wsign-compare bei gcc).
Verlasse Dich niemals auf die Reihenfolge, in der globale oder
static member Variablen initialisiert werden!
(Der Standard definiert diese Reihenfolge zwar eindeutig für eine
Translation-Unit, aber wenn man braucht später im Code nur die
Reihenfolge zweier Definitionen ändern, und schon knallt es!)
Eine Funktion, eine Aufgabe!
Böse Beispiele: Stack::Pop()
und z.B. EvaluateSalaryAndReturnName().
(Grund: übersichtlicher, leichter exception-safe zu machen.)
Für g++ :
-ansi -fno-default-inline -ffor-scope -Wall -W -Wpointer-arith -Wcast-qual
-Wcast-align -Wconversion -Woverloaded-virtual -Wsign-compare
-Wnon-virtual-dtor -Woverloaded-virtual -Wfloat-equal -Wmissing-prototypes
-Wunreachable-code -Wno-reorder -D_GNU_SOURCE
Für SGI :
Für Intel unter Windoofs :
-Qansi -MDd -Gi- -GR -GX- -Qrestrict -Qoption,cpp,--new_for_init -TP
Siehe auch how to steal code.
Diese Guidelines sollen Euch helfen, einen guten Programmierstil zu entwickeln. Außerdem habe ich versucht, ein paar grundlegende Tips zum Programmieren zusammenstellen, die Euch (hoffentlich) helfen, das Programm schneller fertig zu bekommen, weniger Bugs zu produzieren, und Bugs schneller zu finden.
Nun höre ich einige von Euch stöhnen: "Jetzt darf ich noch nicht mal so programmieren wie ich will!", und: "Muß ich mir das alles wirklich durchlesen?". Das habe ich auch gedacht, als ich Guidelines zum ersten Mal in die Hand gedrückt bekam. Aus meiner langjährigen Programmier-Erfahrung kann ich Euch aber versichern: ja, es muß sein, wenn man in einem Team arbeitet. Und selbst wenn man nicht in einem Team arbeitet, sind einige Grundregeln und ein guter Programmierstil sinnvoll, weil sie Euch helfen. Auf jeden Fall macht man sich mit einem schlechten Stil bei den Kollegen unbeliebt! :-)
Insgesamt habe ich versucht, in dem Guideline-Teil so wenig Vorschriften und Einschränkungen wie möglich zu machen, und nur so viel wie nötig: es ist klar, daß es mehrere gute Programmierstile gibt1), und jeder soll und muß seinen eigenen Stil entwickeln. Es ist aber auch klar, daß es viel mehr schlechte als gute Stile gibt ...
| |
1) | Mathematisch gesagt: auf der Menge der Programmierstile gibt es nur eine Halbordnung, keine totale :) |
|
Generell gilt: Diese Guidelines dürfen gebrochen werden, wenn (und nur wenn) der Source-Code dadurch besser lesbar, oder robuster, oder besser zu warten wird.
Übrigens ist es selbstverständlich, daß man nur durch's Durchlesen dieser Guidelines nicht sofort den perfekten Code schreibt. Es ist noch kein Meister vom Himmel gefallen. Wie bei Man-Pages muß man auch in Guidelines immer wieder mal reinschauen. Auch ich arbeite immer noch an meinem Stil :) .
Diese Guidelines können nur die gröbsten Tips geben; die Feinheiten sind zu viele und zu individuell, als daß man sie in Guidelines auflisten könnte. Besser ist es, wenn in Eurem Kopf einfach ständig eine Art "Style-Daemon" läuft, während Ihr programmiert, und der ständig seine Datenbank erweitert ;-) .
In der zweiten Hälfte enthalten diese Guidelines einige Tips und Tricks zu Unix, C, und sonstigem, was man als Programmierer im täglichen Leben braucht oder gebrauchen kann.
Ich will niemals hören: "Ich weiß, daß man
X noch machen müßte, aber das mache ich später, wenn
alles läuft"
Glaube mir: Du wirst es später nicht machen. |
Nur eleganter Code ist guter Code. |
Kommentiere! (Es steht zwar im Prinzip im Code, aber keiner hat Lust auf Reverse-Engineering!) |
Wenn Du die Regeln verletzen mußt, kommentiere warum (und nicht daß Du sie verletzt hast). |
Wähle die Namen Deiner Funktionen, Variablen und Methoden sorgfältig!
Verwende bezeichnende Namen ("labeling names") und eine einheitliche Namenskonvention. |
Achte auf übersichtliches Indenting und Spacing! |
Frage Dich beim Schreiben immer "was ist wenn ..."! (vollständige Fallunterscheidung) |
Schreibe nie Code mit Nebeneffekten! Wenn es doch sein muß, kommentiere diese ausführlich und unübersehbar! |
Lerne Deine Tools vollständig zu beherrschen. |
| |
a) | Und dabei sagte doch Goethe: "Name ist Schall und Rauch". (Marthens Garten) |
|
Überlege Dir bei der Wahl eines Namens für eine Klasse, ein Objekt, eine Variable, oder einen Typ, was ein anderer Programmierer aus dem Namen erkennen kann, wenn er Deinen Code zum ersten Mal sieht und nichts darüber weiß. Er sollte am besten die Bedeutung aus dem Namen ersehen können. Längere Namen sind meistens besser zum Verstehen als kurze (zu lange sind für die Anwender Deines Codes natürlich auch lästig :)). Zum Beispiel ist ParameterUnavailException viel besser verständlich als parmunavlex.
Ein sehr gutes Kriterium dafür, daß ein objekt-orientiertes Design Fehler hat, sind Namen: wenn sie zu lang werden, wenn sie keinen Sinn mehr machen von einem globalen Blickpunkt aus, oder wenn alle Funktionen doIt, make und thing heißen, dann ist es höchste Zeit, das Design zu überprüfen! Wenn Klassennamen aus mehr als 3 Wörtern bestehen, dann ist das ein Indiz dafür, daß Du verschiedene Entities Deines System durcheinander bringst.
Wenn eine Funktion eine Eigenschaft zurückliefert, dann soll man den Namen besser aus "is" oder "has" + Adjektiv zusammensetzen; z.B. isFlat oder hasColor.
Manchmal sind Suffixes hilfreich, z.B. Max, Cnt, Key, Node, Action, etc.
Verwende die üblichen Konventionen für "temporäre" Variablennamen, also i, j, k, etc., für Integers (insbes. Schleifenvariablen und Indizes), s, s1 für String-Variablen, ch, ch1 für Characters, etc.
Wenn mehrere Funktionen/Methoden im selben Modul/Klasse ähnliche Parameter mit ähnlicher Bedeutung haben, so sollen diese Parameter auch dieselben (oder wenigstens ähnliche) Namen haben. Das gilt natürlich ganz besonders für überladene Methoden.
Namen, die für conditional compilation verwendet werden, sollen "all caps" sein (z.B. #ifdef DEBUG_ON).
Die Namen von enum-Typen sollen erkennen lassen, daß es sich um einen solchen handelt. Deswegen sollen diese mit einem E enden, z.B.: renVisibilityE. Die Namen der "Members" eines Enums werden wie Defines gebildet:
typedef enum // Kommentar zu meinem tollen Enum Typ { XYZ_RESULT_MIN, // ungültiger Wert (zum Parameter-Check) XYZ_RESULT_SENSIBLE, // blub blub XYZ_RESULT_SILLY, // bla bla XYZ_RESULT_STONED, // lall XYZ_RESULT_MAX // ungültiger Wert (zum Parameter-Check) } xyzResultE;Bei Enums innerhalb eines Klassen-Scopes sind Präfixe nicht notwendig. Die Namen der Members sollen erkennen lassen, zu welchem Enum sie gehören.
Bei struct-, union- oder pointer-Typen ist eine Kennzeichnung nicht notwendig, da der Typ aus dem Kontext hervorgeht. Wer will kann trotzdem sich Suffixes analog zum enum-Konvention überlegen. Möglichkeiten sind z.B.: renViewpointS, oder renViewpoint_s für struct-Types; objPolyhedronP für Pointer. Andere Konventionen sind denkbar; ich finde die Konvention "Cap-Suffix" am schönsten (und am schnellsten zu tippen :)).
Weiterhin fände ich es toll, wenn Ihr Euch Konventionen überlegt, die semantische Bedeutung einer/s Variable/Objektes im Namen zu kennzeichnen; also z.B. alle Vektoren mit dem Buchstaben v beginnen lassen, alle Matrizen-Namen mit mat beenden, alle Exception-Objekte mit ex beginnen lassen, etc.
Methoden, die verwendet werden um Instanzvariablen zu setzen, sollen mit set beginnen. Methoden, die den Wert einer Instanzvariablen liefern, sollen wie die Instanzvariable heißen (oder mit get beginnen). Die Variable selbst beginnt dann mit Underscore.
Wenn mehrere Klassen zusammen eine Library ergeben, dann kann es manchmal ganz sinnvoll sein, wenn die Klassennamen wiederum einen Präfix haben (z.B. pf für Performer). Es ist nicht nötig, die Methoden- oder Variablennamen dieser Klassen mit Präfix zu schreiben. Die Files einer Klasse werden wie die Klasse selbst genannt (z.B. steht in File matrix.cc die Klasse libMatrix).
Alle Methoden fangen mit einem Kleinbuchstaben an. Wenn es keine zu große Umstellung für Dich ist, dann verwende die inCaps Notation (also getBla oder setNewWonderfulBlub).
Vermeide Redundanzen beim Naming. In folgender Zeile:
myWindow->setWindowVisibility( libWindow::WINDOW_VISIBLE);mußte man 4× "window" tippen und 2× "visible". Genauso gut und verständlich ist:
myWindow->setVisibility( true );(Aufgrund des Namens der Methode weiß jeder, daß man ihr nur Boole'sche Werte übergeben kann, deswegen ist 1 als Parameter ok hier.)
typedef enum // renderer options { renWithWindow, // create window renStereo, // stereo window renWindowDecorations // windows has decorations } renOptionsE; void renderInit( renOptionsE options );was man dann so aufrufen konnte:
renderInit( renWithWindow | renStereo );In C++ geht das so nicht mehr. Ich schlage daher folgenden "Umweg" vor (Alex' Idee):
typedef enum { ... }; typedef int myEnumE; void foo( myEnumE options );Leider kann man den üblichen automatischen Dokumentationsextraktionstools nicht beibringen, daß sie den unbenannten enum dokumentieren sollen aber unter dem anderen Namen myEnumE!
Einerseits helfen Kommentare, Eure Gedanken besser zu ordnen (und damit sauberer zu programmieren); andererseits hilft es Euch, wenn Ihr in einem Jahr etwas an dem Code ändern müßt (oder gar andere) --- sagt nicht, daß Ihr Euch das merken könnt, oder daß alles selbsterklärend ist! :) .
Es gibt vier Arten von Kommentaren:
Der Kommentar muß auf jeden Fall klar machen, welche Bedeutung die Parameter haben, welche Klassenvariablen (oder static Variablen) verwendet werden (möglichst wenige), was zurückgeliefert wird, welche Bedingungen eingehalten werden müssen durch den Caller. Selbstverständlich gehört eine Beschreibung der Funktion dazu.
Wenn einige Parameter Rückgabe-Parameter sind, so muß das eindeutig gemacht werden! (Im Bsp. width.)
Hier ein Beispiel für den Kommentar einer Funktion:
/** Do something * * @param param1 blubber (in) * @param param2 bla (out) * * @return * -1 falls fehlgeschlagen, 0 wenn alles ok. * * Diese Funktion berechnet ... * Kann jederzeit aufgerufen werden. * * @throw Exception * XCoffee, falls kein Kaffee mehr da. * * @warning * Erwartet dass die Funktion init() schon aufgerufen wurde. * * @pre * Param1 wurde von der Funktion blub() berechnet. * * @sideeffects * @arg The global variable @c M_Interest * Nebenwirkungen, globale Variablen, die veraendert werden, .. * * @todo * Schneller machen. * * @bug * Produziert einen core dump, wenn @a param1 = 0.0 ist. * * @internal * Basiert auf dem Algorithmus von ... * * @see * eineAndereFunktion() * **/Nach meiner Erfahrung geht es am schnellsten, wenn man den Kommentar zu einer Funktion dann schreibt, wenn sie "halb" fertig ist, weil dann noch alles frisch ist. Wenn sie vollends fertig ist, sollte man noch einmal drüber sehen, ob der Kommentar noch korrekt ist.
Manchmal hilft es auch, wenn man den Kommentar teilweise schreibt bevor man mit Codieren anfängt! Z.B. ein paar Zeilen, was genau die Funktion tun soll, und einige Parameter auflisten kann schon viel zur Ordnung der eigenen Gedanken helfen.
Wer erst ein ganzes Modul ohne Kommentar schreibt --- "den Kommentar schreib' ich am Ende wenn alles läuft" ---, der schreibt mit ziemlicher Sicherheit überhaupt keinen Kommentar mehr. (Weil es einfach zu viel auf einmal ist, und weil die Feinheiten wie z.B. Caveats nicht mehr im Gedächtnis sind.)
Wichtig ist, daß man durch Überfliegen der Kommentar-Zeilen im Funktions-Body einen Überblick über die Funktion und wie sie "funktioniert" gewinnt. Der in-line Kommentar sollte nur beschreiben was im entsprechenden Code-Block passiert, nicht wie es passiert (Bsp.: "berechne Mittelwert" ist besser als "summiere und teile durch n").
Wenn es wichtige Bedingungen gibt, die eingehalten werden müssen, oder Schleifeninvarianten, so ist es sinnvoll, diese in einem Kommentar zu vermerken, damit diese nicht aus Versehen später verletzt werden, wenn man (evtl. jemand anders!) den Code modifiziert. Ein Beispiel steht oben, ein weiteres ist:
// do the following *after* ... !
Hier ist ein Beispiel eines schlechten Kommentars:
a = malloc( 100 * sizeof(int) ); // gimme more memory x = glob( ... ); // do file completion // now sort the elements qsort( e, n, sizeof(elemT), compfunc );Kommentiere nicht neu entdeckte Library-Funktion: jeder kann in den Man-Pages selbst nachsehen.
Kommentare von Variablen und Typen könnten ungefähr so formatiert werden:
#define MAX_REC_DEPTH 1000 // max depth of a boxtree static int RecursionDepth = 0; // used in bxtConstructGraph typedef struct // Kommentar für den struct allg. { vmmPointP x, y; // Kommentar der einzelnen Members int a, // Kommentar .... b; // .. der einzelnen Members } MyStructS;
Die .cpp-Files enthalten keine CVS-Keywords außer einem Id-Keyword. Dieses ist für die Produktversion vorgesehen und wird nur für diese Version expandiert (zur Identifizierung der einzelnen Versionen, aus denen das System zusammen gesetzt ist).
Das bedeutet, daß alle in ihr ~/.cvsrc folgende Zeilen eintragen müssen:
status -v update -P -ko add -ko checkout -P -ko diff -ko -b -B -d cvs -z 9 edit -a none tag -cDamit werden unnötige "diffs" vermieden, die nur aufgrund verschiedener Expandierungen der CVS-Keywords entstehen. (Für die Produktversion muß dann cvs mit der Option -r aufgerufen werden.)
Verwende 4-er Einrückungen!
Source-Zeilen sollten möglichst nicht länger als 80
Zeichen sein. (Es gibt Ausnahmen.)
Pro Zeile soll nur ein Statement stehen2) (es gibt berechtigte Ausnahmen).
| |
2) | Eine psychologische Untersuchung hat gezeigt, daß Programmierer in Zeilen denken, d.h., daß die kleinsten Einheiten, mit denen Programmierer Code erfassen und verstehen, einzelne Zeilen sind. |
|
Jeder C-File included stdlib und stdio (falls das nicht schon in einem "globalen" defs.h gemacht wird).
Achte auf eine "schöne" Formatierung der Funktionsprototypen, des Deklarationsblockes von lokalen Variablen, etc. Deine Kollegen werden die Nase rümpfen, wenn es "saumäßig" aussieht.
Hier findet man ein graphisches, annotiertes Beispiel, wie Source-Code aussehen soll.
Der "Inhalt" von Header-Files muß mit ifndef gegen Mehrfach-Including geschützt werden, wie im template.h schon gemacht. (Die pragma-Zeile erledigt dasselbe wie die ifndef-Klammer und ist effizienter beim Compilieren, ist aber nicht auf allen Plattformen verfügbar.)
Der Name eines Header-Files ist gleich wie der dazugehörige C-File (also Foo.h zu Foo.cpp). Man sollte Namen vermeiden, die schon in /usr/include für Standard-Header-Files vergeben sind, z.B. math.h oder gl.h).
Mache C-Header-Files kompatibel mit C und C++. Das bedeutet, daß C-Header-Files in durch ein extern "C" {} geklammert werden müssen:
#ifdef __cplusplus extern "C" { #endif .... #ifdef __cplusplus } #endifSo können sie sowohl in C-Files als auch C++-Files included werden.
for ( ... ) { if ( .. ) { .. } else { ... } }So können die schließenden Klammern am leichtesten zugeordnet werden.
Der K&R-Style ist verboten, da schlecht lesbar (Ziel dieses Styles ist, den Code so "dicht" wie möglich zu machen):
for ( ... ) { if ( .. ) { .. } else { ... } } else { ...
Es ist kein Muß, aber es ist schöner, wenn Variablen und Kommentare so tabuliert werden, daß sie in der selben Spalte anfangen:
int x, y; // dominant coord planes of polygon int xturns, yturns; // # turns of xslope, yslope objPolyhedronP p1, p2; int i;ist viel schöner als
int x, y; // dominant coord planes of polygon int xturns, yturns; // # turns of xslope, yslope objPolyhedronP p1, p2; int i;Wenigstens die Kommentare sollten gleich ausgerichtet sein (es sei denn, sie passen sonst nicht in die Zeile).
Spaces innerhalb einer Zeile sind genauso wichtig:
for(i=obj->begin();i<obj->l()&&obj->M(i)!=-1;i++){ obj->M(i)=-1; }istvielschlechterzulesenals
for( i = obj->begin(); i < obj->l() && obj->f(i) != -1; i++ ) { obj->f(i) = -1; }
Das gilt sowohl für Modifikationen des Codes durch den Autor selbst wenige Monate nachdem der Code entstanden ist (best case), als auch für Modifikationen 1-2 Jahre später durch jemand, der keine Ahnung vom "großen Bild" hat (worst case).
Man kann nicht viele konkrete Regeln aufstellen, die Code gut wartbar machen --- man muß sich durch Erfahrung ein Gefühl dafür schaffen, welche Konstrukte im Code später schlecht wartbar sind. Man kann aber doch folgende allgemeine Tips beherzigen:
foo( a, b ) { .... } bar( x, y, z ) { ,,, // zusaetzlicher code ... // selber code wie in foo ,,, // zusaetzlicher code }dann muß man bar umformen in:
bar( a, b, c ) { ... foo( a, b ) ... }Auch, wenn man eine solche Möglichkeit zur Faktorisierung erst später entdeckt!
Kandidaten, bei denen fast immer Faktorisierung angewendet werden muß, sind mehrere Konstruktoren einer Klasse, Increment-/Decrement-Operatoren, zwischen dem Copy-Konstruktor und dem Zuweisungsoperator, der Operator == und !=, etc.
Noch ein Beispiel: Ganz schlecht ist
if ( ... ) { blabla ... } else { gleicher blabla wie oben anderer code ... }
Das läßt sich ganz schlecht überblicken. Und wenn man in einem Jahr mal den Code blabla ändern muß, passiert es sehr leicht, daß man einen der beiden Zweige vergißt! Und dann sucht man stundenlang nach einem Bug, falls er überhaupt gleich auftaucht und nicht erst 2 Monate später, wenn man schon längst vergessen hat, daß man da überhaupt was geändert hat ...
if ( a = b )wird garantiert später von jemand, der einen Bug in dieser Funktion sucht, "repariert" zu
if ( a == b )
for ( c = s; c < ...; ... ) c = f(...);tatsächlich eine for-Schleife ohne Body gemeint (dann wurde das Semikolon vergessen), oder ist es nur schlecht eingerückt? Schreibt man dagegen
for ( c = s; c < ...; ... ) {}; c = f(...)oder (je nachdem was gemeint war)
for ( c = s; c < ...; ... ) c = f(...);dann ist es eindeutig.
Zu guter Lesbarkeit gehört auch eine gute Strukturierung des ganzen Files (siehe Strukturierung und Layout), sinnvolle Modularisierung, Strukturierung der einzelnen Zeilen, als auch Kommentare (siehe Kommentare)
Alle hier aufgeführten Bugs sind tatsächlich vorgekommen! Die meisten haben etliche Stunden gekostet, um sie zu finden.
Fazit: wenn Du nach einem Programmier-Abschnitt nochmal 2 Minuten darüber nachdenkst, ob Du wirklich alle Fälle bedacht hast, dann kannst Du später locker einige Stunden frustrierende Bug-Suche sparen! "Was passiert, wenn jener Zeiger NULL ist?", "Was passiert, wenn der String doch länger als 100 Zeichen ist, weil z.B. ein Pfad-Name ziemlich lang ist?", "Was passiert, wenn diese Anzahl von ... 0 ist?", "Was passiert, wenn das graphische Objekt sich verändert? wenn es sich bewegt? wenn es seine Form ändert? oder seine Farbe?", "Was passiert, wenn das Objekt nicht direkt unter der Wurzel des Szenengraphen hängt?".
Ich kenne sogar Fälle, wo extrem schlechter Code (Bugs, schlechte Modularisierung, miserable Modifizierbarkeit, etc.) hinterher 3 Leute jeweils(!) 1 Mann-Woche (verteilt auf 1 Jahr) gekostet hat, um ihn zu warten, anzupassen, und debuggen. Der Code wurde (leider) in nur 1 Woche gehackt/zusammenkopiert --- hätte man noch 1 Woche investiert, ihn sauber zu schreiben und zu testen, hätte man in der Summe 2 Mann-Wochen gespart.
Wenn eine Klasse keinen Konstruktor
braucht/hat, deklariere einen Default-Konstruktor als private ohne
Implementierung im C-File
(Mit Kommentar not implemented).
Das verhindert, daß der Compiler einen erzeugt, der evtl. falsch ist.
Deklariere immer einen Copy-Konstruktor und einen Zuweisungsoperator.
Wenn die Klasse diese nicht braucht, mache sie private ohne
Implementierung.
Das Problem bei C++ ist nämlich, daß man dem Code nicht ansieht,
wann der Copy-Konstruktor und der Zuweisungsoperator aufgerufen werden!
Siehe dieses Beispiel.
Konstruktoren können keinen Error-Code liefern. Dazu gibt es zwei Lösungen:
Class *o = new Class(); if ( o->init() < 0 ) { error ... }Problem: es können trotzdem Excpetions entstehen (z.B. kann new fehlschlagen).
Rufe keine virtuellen Methoden in Konstruktoren auf.
Verwende, wenn es geht, Initialisierung anstatt Zuweisung.
Bei der Verwendung von Zuweisungen im Konstruktor werden evtl. viele
temporäre Instanzen erzeugt - was eine schlechte Performance ergibt.
Basistypen (int, float, etc.) können im Konstruktor
per Zuweisung initialisiert werden.
Hier ein teures Beispiel mit Zuweisung:
class String { public: String(void); // make 0-length string String( const char *s); // copy constructor String& operator=( const String &s ); private: ... } class Name { public: Name( const char *t ) { s = t; } private: String s; } void main( void ) { // how expensive is the following ?? Name neighbor = "Joe"; }Folgendes passiert:
Und hier die bessere Alternative mit Initialisierung im Konstruktor. Einziger Unterschied zu obigem Code-Beispiel ist der Konstruktor von Name:
Name::Name( const char *t ) : s(t) {}
Wenn man Konstruktoren mit genau einem Parameter (conversion constructors) nicht explicit macht, dann kann es passieren, daß diese an Stellen verwendet werden, wo man es nicht "sieht", z.B.:
class A { A(float); } void foo(A a) { .. } foo( 1.0 ); // hier wandelt der Compiler 1.0 automatisch in ein A um!Manchmal kann das erwünscht sein, aber i.A. ist es schwer, in solchem Code Performance-Probleme zu finden (was besonders bei Computer-Graphik wichtig ist).
dynamic_cast<type>( expression )eine Funktion, die NULL liefert, falls expression nicht vom Typ type ist, sonst aber einen Zeiger des gewünschten Typs liefert.
Selbstdefinierte Cast-Operatoren können sehr undurchsichtige Effekte
haben (wie 1-Parameter-Konstruktoren).
Beispiel:
class A { public: A() { .. }; explicit A( char* ) { .. }; ~A () {}; }; class B { public: B() { .. }; ~B () {}; operator char *() { .. }; }; void foo(void) { B b; A a(b); // geht, da b nach char* gecastet werden kann! }Also: sparsam verwenden.
Wirf keine Exceptions in Destruktoren!!
Leite alle Exception-Klassen von std::exception ab (#include<stdexcept>). Eventuell macht es Sinn, eine der Standard-Unterklassen von exception zu verwenden oder davon abzuleiten (logic_error, runtime_error, domain_error, invalid_argument, length_error, out_of_range, bad_cast, bad_typeid, range_error, overflow_error, bad_alloc).
Catch by reference (catch (XClass &x)), never catch by value (catch (XClass x)). (Grund: die Exception, die ankommt, könnte eine Unterklasse sein.) Oder einfach nur catch(XClass).
Mache die try-Blöcke groß, wenn es geht.
C-style Callbacks (z.B. Callbacks für C-Libraries) sollen immer als "no-throw" deklariert werden:
void myCallback( ) throw ()
Befolge das Idiom "Resource Allocation is Initialization". Vermeide new im Konstruktor, oder schachtele es in try (denn der Destruktor wird nicht aufgerufen im Falle einer Exception). Verwende evtl. auto_ptr aus der stdlib (sie liefern einen einfachen Mechanismus, wie man Speicher automatisch wieder freigeben kann). Verwende evtl. "strong pointers" (siehe die Official Resource Management Page).
Always perform unmanaged resource acquisition in the
constructor body, never in initializer lists. In other words,
either use "resource acquisition is initialization" (thereby
avoiding unmanaged resources entirely) or else perform the
resource acquisition in the constructor body.
For example, say T was char and t_ was a
plain old char* that was new[]'d in the
initializer-list; then in the handler there would be no way to
delete[] it. The fix would be to instead either wrap the
dynamically allocated memory resource (e.g., change char*
to string) or new[] it in the constructor body where it
can be safely cleaned up using a local try-block or otherwise.
Verwende Exceptions nicht, wenn es den Code komplizierter macht. Dann ist vermutlich ein normales Return-Code-Schema besser.
Deklariere keine Exception-Spezifikation (throw( X, Y ));
statt dessen, dokumentiere die möglichen Exceptions im
Kommentar zu der Funktion.
Grund:
Inline-Methoden können sein: Zugriffs- (auf Instanzvariablen) und
Forwarding-Methoden (die nichts tun außer eine andere Methode aufrufen).
Die inline-Deklaration ist heutzutage aber kaum noch nötig, da der
Compiler bei eingeschalteter Optimierung das von alleine macht
(s.a. Optimierungen).
Achtung:
folgende Funktionen sollen nie inline sein!
Eine Methode, die per Design die Instanz nicht verändern soll, soll man mit const deklarieren. Das verhindert, daß später aus Versehen doch Code eingefügt wird, der etwas verändert. Außerdem können nur solche Methoden für const-Instanzen aufgerufen werden.
Achtung: der default Assignment-Operator macht nur eine "shallow copy"!
Der Assignment-Operator soll void zurückgeben. (Grund: dann kann etwas wie if ( a = b ) nie passieren.)
Verwende Operator-Overloading selten und einheitlich. Ein Operator soll immer
dasselbe "bedeuten". (Dasselbe gilt für
Funktionen-Overloading.)
Jeder Anwender erwartet, daß z.B. der ++-Operator irgend
einen internen Zustand "erhöht", und daß der
*-Operator irgend eine arithmetische Multiplikation ist.
Implementiere immer auch das semantische Gegenstück eines Operators.
Wenn es den Operator == gibt, dann erwartet jeder, daß
es auch != gibt, und wenn es ++ gibt, dann sollte es auch
-- geben (manchmal ist es natürlich nicht möglich, z.B.
bei einem Iterator durch eine einfach-verkettete Liste).
Wenn es den Operator < gibt, sollte es auch >,
<= und >= geben.
Wenn es + gibt, sollte es auch -, += und
-= geben.
Diese "balancierten" Operatoren sind sehr gute Kandidaten für
Faktorisierung!
Liefere nie einen Zeiger auf eine Instanzvariable zurück! Wenn es unbedingt sein muß, dann nur als const Zeiger oder Reference!
Vermeide call-by-value-Übergabe von Objekten als Argumente für eine Funktion.
| |
5) | Meiner bescheidenen Meinung nach sind References keine gelungene "Verbesserung" in C++! |
|
Noch ein Grund, warum Referenzen immer mit const deklariert sein sollten, der auch Pragmatiker überzeugen dürfte: temporäre Objekte sind prinzipiell const. Wenn also ein formaler Parameter eine nicht-const Referenz ist, dann kann man so etwas nicht schreiben:
foo( A() );
Offset Pointer to Members müssen sehr gut begründet werden können! Normalerweise sind sie ein Zeichen dafür, daß im Design etwas nicht stimmt (z.B. falsche Identifizierung, welches die dem Problem am besten angepaßten Objekte sind, oder falsche Verteilung der Funktionalität).
Verwende keine temporären Objekte in Funktionsaufrufen. Es sei denn, Du weißt genau, wann diese wieder gelöscht werden (weißt Du's?).
// Haesslich!! setColor( &(Color(black)) ); // So ist's schoen Color color(black); setColor( &color );Initialisierung von Instanzen per Zuweisung ist verboten!
A a = A(); // verbotenschreibe
A a; a = A(); // ok (wenn auch unnötig)wenn es denn sein muß.
falsch:
ca = Dotprod(v1, v2) / (Len(v1) * Len(v2)); sa = sqrtf( 1 - ca*ca );richtig:
h = vmmLen(v1) * vmmLen(v2); if ( h < epsilon ) /* fallback stuff */ else { ca = Dotprod(v1, v2) / h; if ( ca >= 1.0 ) ca = 1.0; if ( ca <= -1.0 ) ca = -1.0; sa = sqrtf( 1 - ca*ca ); }
total falsch:
if ( x == a ) ...immer noch falsch:
if ( x < a ) ... else if ( x > a ) ... else ...besser:
if ( x > a-epsilon && x < a+epsilon ) ...richtig:
#include <math.h> if ( fabs(x - a) <= epsilon * fabs(a) )
file = fopen("/igd/a4/home/mies/bla", "r"); // hart codierter File-Name! fscanf(file, ...); // kann Core-Dump geben!nur etwas besser:
#define BlaFile "/igd/a4/home/mies/bla" file = fopen(BlaFile, "r"); // immer noch hart kodiert! if ( ! file ) { fprintf(stderr, "couldn't open ..."); exit(1); // exit ist immer schlecht! // es soll immer - moeglichst // sinnvoll - weitergehen
ein bißchen besser:
file = fopen( getenv("BLAFILE"), "r" ); // kann schon wieder Core-Dumpen! if ( ! file ) { fprintf(stderr, "couldn't open ..."); ...
am besten:
blafileenv = getenv("BLAFILE"); if ( ! blafileenv ) { fprintf(stderr, "env.var BLAFILE not set - using default %s\n", BLAFILEDEFAULT ); blafileenv = BLAFILEDEFAULT; } file = fopen( blafileenv, "r" ); if ( ! file ) { perror("open"); fprintf(stderr, "couldn't open %s!\n", blafileenv ); ...... // hier moeglichst irgendwelche return; // sinnvollen Default-Werte setzen } fscanf(file, ...);
Bei system(): Wie schon erwähnt gibt es immer Ausnahmen. Der system call ist eine solche --- hier müssen sogar feste Pfade benutzt werden! Das Problem: man macht sich sonst von der Umgebung (PATH) des Users abhängig.
Beispiel: man möchte per system eine remote shell starten. Falsch ist:
system( "rsh machine ..." );denn das Kommando rsh ist vielleicht gar nicht im PATH des Users enthalten, und wenn, dann ist vielleicht zuerst die restricted-shell im PATH, und nicht die remote shell!
Deswegen: bei system das Kommando immer mit absolutem Pfad angeben (mit #define am Programmanfang deklarieren!). Am besten testet man vorher mit stat noch, ob es den Befehl auch wirklich gibt. Also im Beispiel:
#define RSH_PROG "/usr/bsd/rsh" ... err = stat( RSH_PROG, &statbuf ); // auf manchen Unices ist rsh if ( err ) // nicht da wo man sie vermutet! ... err = system( RSH_PROG " machine ..." );
Ganz falsch:
void foo( int bla ) { if ( bla == 1 ) .. else if ( bla == 2 ) ..Problem: Du weißt nie, wer alles foo() aufruft! Was passiert, wenn man die Bedeutung von bla mal ändern muß?
Besser:
typedef enum { Fall1, Fall2, ... } FooFaelleE; void foo( FooFaelleE bla ) { ... }
Falls man mehrere Fälle "verodern" möchte, dann muß man const int verwenden (jedenfalls in C++).
Bei Makros muß man aufpassen, sowohl wenn man sie verwendet als auch, wenn man sie definiert! Denn: Makros und deren Parameter können Nebeneffekte haben! Generell soll man Makros so schreiben, daß das Prinzip der geringsten Überraschung gilt. Aus diesem Grund haben wir eine Namenskonvention (all-caps) für Makros, die sie als solche deutlich kenntlich macht.
Mehrfache Auswertung von Argumenten: Wenn foo() ein Makro ist, sollte man nie Argumente übergeben, die Nebeneffekte haben, z.B.
foo( i++ )ist streng verboten!
if ( arg < Max ) x = arg;?!
Noch viel Schlimmeres kann in so einem Fall passieren, wenn das Argument eine Funktion ist:
foo( bar(x) )Wie oft wird bar(x) aufgerufen?! Was ist, wenn das Makro foo in der rekursiven Funktion bar() selbst vorkommt?!
Und was noch viel schlimmer ist: selbst wenn kein Bug entsteht, so wird das ganze Programm trotzdem seehhr laaangsam, weil bar() viel zu oft aufgerufen wird --- und das kann man praktisch überhaupt nicht mehr herausfinden!!
Variablen in Makros müssen auf jeden Fall so gewählt werden, daß sie nie genau so lauten können wie tatsächliche Variablen. Auch solch ein Bug ist praktisch nicht zu finden! (I.A. wird der Compiler noch nicht einmal eine Warning ausgeben!) Deswegen verwende immer Großbuchstaben für Makro-Variablen; am besten verdoppelte Buchstaben, oder ähnliches.
Bei der Definition von Makros muß man immer alle möglichen Fälle und Kontexte in Betracht ziehen, wie das Makro verwendet werden könnte. Zwei typische Fehler sind:
#define Bla( X ) if ( X < 0 ) X = 0;Wenn dieses Makro in einem weiteren if-else verwendet wird, ist schon ein Bug produziert, für den der Anwender noch nicht mal etwas kann:
if ( ... ) Bla( x ) else ...Problem: der Compiler wird das else auf das innere if ( x < 0 ) beziehen! Abhilfe: den ganzen if-Ausdruck im Makro mit {} klammern.
#define blub( X ) \ z = .... ; \ y = .... ;nicht geklammert wird (mit {}), dann entsteht bei der Verwendung in folgendem Statement
if ( ... ) blub( X );ein Bug, der sehr schwer zu finden ist!
Wenn ein Makro mehr als 7 Zeilen (= Anweisungen) lang ist, sollte man sowieso eine Funktion daraus machen -- der Compiler kann solche "kleinen" Funktionen inline-en.
#define Bla( X, Y ) X-Yführt zu einem schwer zu findenden Bug, wenn man das Makro in einem weiteren Ausdruck verwendet, z.B.
z = Bla(a,b) * c;Das Resultat ist nämlich a - b*c, was sicher nicht die Intention des Programmierers war! Abhilfe: #define Bla( X, Y ) (X - Y).
Ein weiteres Beispiel, wieso Klammerung nötig ist:
#define vmmPrintVec( V ) printf( "%f %f %f", V[0], V[1], V[2] )liefert vollkommenen Blödsinn (bis zu Core-Dump!), wenn man es so verwendet:
vmmPrintVec( *v )(Prioritäten der Operatoren * und []!) In diesem Beispiel muß man das Makro also so schreiben:
#define vmmPrintVec( V ) printf( "%f %f %f", (V)[0], (V)[1], (V)[2] )
Genauso sollte man versuchen, Makros so zu definieren, daß Argumente nur einmal verwendet werden (was natürlich nicht immer geht). Z.B. kann man statt
#define blub( X ) \ bla = X; \ blub = malloc( X * ... );besser schreiben
#define blub( X ) \ bla = X; \ blub = malloc( bla * ... );
|
|
7) | Ein Zitat aus dem Netz: An ounce of prevention is worth a ton of code. (Anonymus). |
|
Funktionen, die nicht mehr als ca. 100× pro Frame aufgerufen werden, sollen immer die Parameter checken auf gültigen Wertebereich! Das kann den Code dieser Funktionen locker auf das doppelte anwachsen lassen --- aber: das ist es wert!
void foo( char *param ) { char blub[MaxBlubLen]; strcpy( blub, param ); \\ was passiert, wenn param laenger als MaxBlubLen ist ?!! }Besser ist:
strncpy( blub, param, MaxBlubLen ); blub[MaxBlubLen-1] = 0;Noch besser ist natürlich eine zusätzliche Warning.
typedef enum { XYZ_MIN, XYZ_VALUE_1, // kommentar XYZ_VALUE_2, // kommentar XYZ_MAX, } xyzTypeE;Damit kann man in einer Funktion den gültigen Wertebereich mit
if ( e <= XYZ_MIN || e >= XYZ_MAX ) Fehlermeldungchecken. Dieser Check bleibt auch gültig, wenn man nachträglich Werte zum enum-Typ hinzufügt.
Dasselbe gilt in C++, wenn der "enum" mit Hilfe mehrerer const ints deklariert wird.
Glaube aber einem erfahrenen (und leid-geprüften) Programmierer: es wird vorkommen! (Vorausgesetzt, Dein Code überschreitet eine gewisse "kritische Größe", das sind ungefähr 5,000 Zeilen.)
Deswegen: in jeden switch und in die meisten if's gehört ein default: bzw. else für den "can't happen"-Fall! Der muß wenigstens dafür sorgen, daß das Programm eine auffällige Fehlermeldung liefert und ohne Core-Dump weiterläuft.
System calls. Auch system calls (z.B. malloc() oder open oder fork/sproc) können schief gehen! Sogar dann, wenn es gar nicht passieren kann. (Z.B. kann nämlich immer passieren, daß der Speicher oder die i-node table voll ist.)
Deswegen sieht ein fopen immer so aus:
f = open("bla", "r") if ( f < 0 ) { perror("open"); fprintf(stderr, "module: Failed to open file ..."); do something sensible instead }und jeder malloc so:
m = malloc( n * sizeof(type) ); if ( ! m ) { fprintf(stderr, "module: malloc failed!\n"); ... // do something sensible instead {
Man könnte sich dafür natürlich Wrapper-Makros schreiben. Meine Erfahrung allerdings ist, daß diese dann oft umständlich im Code aussehen, und man spart eigentlich nur ein bißchen Tiparbeit, welche man mit einem vernünftigen Editor sowieso reduzieren kann.
Das assert-Makro (siehe man assert)
kann helfen, die Wartbarkeit zu erhöhen, und hilft gleichzeitig, Bugs
schneller zu erkennen (auch wenn man an der betreffenden Stelle gar keinen
gesucht hat).
Außerdem werden durch das assert-Makro explizit Bedingungen
im Code sichtbar gemacht, z.B. Schleifeninvarianten, oder Vor- und
Nachbedingungen.
Achtung: achte darauf, daß dieses Makro in der Produktversion nicht
aktiviert ist! (-DNDEBUG)
Verwende keine Pfade beim Includen (z.B.
#include<../mydefs.h>)! Verwende statt dessen die
-I-Option des Compilers
(dann kann man später wesentlich leichter
die Libraries re-organisieren, ohne daß
alle Source-Files geändert werden müssen).
Verwende #include <...> für Standard-Header-Files
(normalerweise in /usr/include) und
#include "..."
für alle anderen (kleiner Speedup beim Compilieren).
Der Header-File sollte immer auch in dem C-File included werden, in dem die entsprechenden Funktionen oder Variablen tatsächlich definiert werden. Dann kann der Compiler checken, daß die Deklaration immer noch mit der Definition übereinstimmt.
Verwende isascii bevor Du eines der anderen ctype.h-Makros verwendest. Z.B.
if ( isascii(*c) && isdigit(*c) )
scanf kann aufhören bevor es alle Parameter gescant hat. Return-Wert checken!
Verwende einen malloc-Wrapper, der Form
#define xmalloc( PTR, SIZE, ACTION ) \ { \ PTR = malloc( SIZE ); \ if ( ! PTR ) \ ACTION; \ }Das zwingt einen dazu, tatsächlich sich Gedanken zu machen zu dem Fall, daß kein Speicher mehr frei ist.
Falls das fall-through feature eines case-Statements verwendet
wird, so muß das kommentiert werden.
Das default-Statement eines case muß immer
vorhanden sein.
Vermeide eingebettete Statements. Auch ++ und --
zählen.
Nur manchmal kann es den Code leserlicher machen, wie z.B.
while ( (c = getchar()) != EOF ) { process the character }
if ( strcmp(s,t) )
| |
6) | Mache Dir eine Kopie der Tabelle der Präzedenzen aller Operatoren, z.B. aus dem Insight-Book C Language Reference Manual, Chapter 7, Table 7.1 . |
|
if ( ch = '\r' )was immer 1 liefert! (Gemeint war natürlich ==.)
if ( '\r' = cr )weil dann schon der Compiler meckert! Das klappt natürlich nur, wenn man auf der einen Seite eine Konstante hat. Ich persönlich finde diese Schreibweise auch nicht so hübsch ;-)
typedef struct blubT { ... } blubT;
Vermeide exzessive "typedef"-itis! Es macht keinen Sinn, einen Typ intReturnType einzuführen, oder myFloatT, oder typedef int bool, oder uint!
Unäre Operatoren werden i.A. ohne Space geschrieben, binäre Operatoren (außer "." und "->") haben links und rechts ein Space. Bei komplexen Ausdrücken muß man von Fall zu Fall neu entscheiden.
Wenn ein for-Loop lange Sections enthält, schreibe jede Section auf eine eigene Zeile, z.B.:
for ( i = 0; i < plhGetNFaces(o)*2 + plhGetNPoints(o); i += n/2 + (empty ? 1 : 2) )
Verwendung von break und continue innerhalb derselben Schleife sollte vermieden werden.
Schreibe ANSI-C! (komplette Prototypen)
RTTI ist erlaubt (kostet inzwischen keine Performance mehr). Aber verwende es nie anstelle von virtuellen Methoden.
Bevor Du die Implementierung "tune-st" (optimierst), frage Dich, ob die Implementierung wirklich schon so weit fortgeschritten ist, daß das Sinn macht!9)
| |
9) | "Premature Optimization is the Root of All Evil" -- Donald E. Knuth. |
|
Wenn optimiert werden soll, dann nur nach einem Profiling! Du wirst staunen, wo die Zeit wirklich verloren geht.
Zuerst läßt man den Compiler optimieren. Dies geschieht mit folgenden Compile-/Link-Optionen:
cc -n32 -O ...
cc -n32 O -INLINE:=ONschaltet Inlining für einzelne Files an, d.h., Funktionen werden innerhalb dieses Files inlined.
Für C-Code bringt es nur etwas, wenn man weiß, daß man kleine(!) Funktionen hat, die ein paar 1000 Mal aufgerufen werden.
Inlining über mehrere Files hinweg geht mit
cc -O -IPA:inline=ON-IPA muß auf der Compile-Zeile als auch auf der Link-Zeile angegeben werden.
Wenn man wissen will, was da eigentlich abgeht, macht man
cc -O -INLINE:=ON:list=ONDann wird auf stderr ausgegeben, was inlined wird.
Generell ist meine Erfahrung: der Compiler weiß sehr gut, wann es sich lohnt! Wenn man trotzdem unbedingt möchte, daß eine bestimmte Funktion inlined wird, macht man
cc -O -INLINE:=ON:must=foo,bar(Fuer C++ müssen natuerlich die "mangled names" angegeben werden.)
Für Inlining aus Libraries kann man -IPA verwenden, wenn es eine .a-Lib ist (nicht .so) und wenn diese Library auch mit -IPA) erzeugt wurde. Ansonsten muß man -INLINE:library= nehmen. Man sollte außerdem -IPA:plimit=192 setzen, sonst wird der Code zu groß (behauptete jemand in der Newsgroup).
cc -n32 -O3 -OPT:alias=typed -OPT:fast_sqrt=ON:fast_exp=ON:IEEE_arithmetic=3 -OPT:ptr_opt=ON:Olimit=3000 -OPT:unroll_times_max=6 -LNO:opt=1:gather_scatter=2 -IPA:alias=ON:addressing=ON:aggr_cprop=ON -IPA:inline=ON -INLINE:must=foo,bar
while ( *i++ = *j++ ) ;ist nicht schneller (sogar eher langsamer) als
while ( *j ) *i = *j , i ++ , j ++;(Noch besser in diesem Fall ist strcpy oder memcpy :))
Mit register short i; anstatt einfach nur int i; zwingst Du den Compiler höchstens, seine optimierte Register-Allozierung fallenzulassen, um Deiner register Anweisung nachzukommen! (falls er es überhaupt beachtet.)
Inlining einer Funktion bringt wirklich nur dann etwas, wenn diese aus 1-2 Zeilen besteht! (In allen anderen Fällen explodiert nur die Code-Größe.)
Ganz analog ist es mit dem Faktorisieren von Funktionen: wer alles in eine Funktion packt, oder aus jeder Funktion ein Makro10) macht, der soll mal ganz schnell in CPU benchmarks nachschauen! (Da kann man nachsehen, wie teuer ein Funktionsaufruf wirklich ist.)
| |
10) | OK, ich gebe zu, das haben wir im Y leider auch gemacht --- zu unserer Entschuldigung kann man sagen, daß damals (1994) die Compiler noch nicht sehr gut optimieren konnten (kein inlining), und daß wir damals einfach noch nicht wußten, wie schnell ein Funktionsaufruf wirklich ist! |
|
Eigene Pointer-Arithmetik lohnt sich meistens nicht:
for ( p = array + n - 1; p >= array; p -- ) { p->item = ... oder *p = ... }ist genauso effizient wie
for ( i = n-1; i >= 0; i -- ) p[i] = ...Die zweite Variante ist um den Faktor 10 schneller (Weil der Compiler mehr Freiheit zum Optimieren hat)!
Es kann extrem peinlich werden, wenn ein Informatiker die Oberstufen-Mathematik nicht beherrscht. Es ist schon vorgekommen, daß Leute den Ausdruck 1 + q + q2 + ... + qn mit einer Schleife berechnet haben! (Geometrische Reihe)
Durch ungeschicktes Codieren kann der effizienteste Algorithmus zunichte werden.
Ein Beispiel:
Ein Algorithmus verarbeitet einen String der Länge N und hat
Komplexität O(N*log(N)).
Ein Zwischenschritt ist das Konkatenieren von k
Teilstrings der Gesamtlänge N.
Geschickte Implementierung:
char *teilstring[k]; char gesamtstring[N]; char *gesamtende = gesamtstring; char *charptr; for ( i = 0; i < k; i ++ ) { charptr = teilstring[i]; while ( *gesamtende++ = *charptr++ ); }hier ist der Aufwand genau a*N. Weniger gut:
for ( i = 0; i < k; i ++ ) { strcpy( gesamtende, teilstring[i] ); gesamtende += strlen( teilstring[k] ); }hier ist der Aufwand genau a*2N. (Weil jeder teilstring[i] genau 2× durchlaufen wird.)
for ( i = 0; i < k; i ++ ) strcat( gesamtstring, teilstring[i] );hier ist der Aufwand genau a*N2!
Noch ein "schlechtes" Beispiel:
length = sqrt( pow( point1[0] - point2[0], 2) + pow( point1[1] - point2[1], 2) + pow( point1[2] - point2[2], 2) );Wenn dieser Code häufig ausgeführt wird, ist die Performance im Eimer! Abgesehen davon ist es einfach extrem häßlich, das Quadrat einer Zahl mit pow statt mit x*x zu berechnen. Außerdem zeugt so etwas davon, daß der Programmierer das System nicht kennt, von dem sein Code ein Teil werden soll --- denn jedes graphische System stellt garantiert schon eine ganze Menge von Funktionen für die allfällige Vektor-Matrix-Arithmetik zur Verfügung.
Verwende alloca(), wenn Du temporär Speicher brauchst, der nach dem Ende der Funktion nicht mehr benötigt wird. Das geht schneller, die Gefahr von memory leaks ist kleiner, und es vermeidet Speicherfragmentierung. Verwende alloca nicht, falls Du evtl. viel Speicher brauchst, denn falls auf dem Stack nicht mehr genügend Speicher vorhanden ist, wird das Programm von Unix gekillt.
Niemand programmiert mehr einen String-Copy, Quicksort, Hashtables, Listen, dynamische Arrays, etc.! Dafür gibt es gute, effiziente, bewährte Standard-Libraries! (siehe RTFM) --- selber programmieren dauert viel zu lange, gibt mehr Möglichkeiten für Bugs, und ist nie schneller als die Standard-Funktionen, da diese sorgfältig in Assembler geschrieben wurden und getunet sind.
Verwende Pre-Increment, statt Post-Increment.
Grund: bei Post-Increment muß der Compiler zuerst eine Kopie des Objektes
erzeugen (Copy-Ctor!), dann die Methode des Objektes aufrufen, und schließlich
die Kopie wieder verwerfen. Dabei hat es der Compiler wesentlich schwerer zu erkennen,
daß der erste Aufruf des Copy-Ctors eingespart werden kann.
Beim Pre-Increment fällt dies wesnetlich leichter.
In "Worse is better"
ist ein interessanter Gedanke zum Thema Vollständigkeit,
Einfachheit, und Konsistenz:
manchmal kann es besser sein, etwas von der Konsistenz- oder
Vollständigkeitserhaltung dem Aufrufer aufzubürden, nämlich dann,
wenn es der Aufrufer viel leichter erreichen kann als die Implementierung in
der Library.
Das einzige Problem ist eigentlich "nur",
das richtige Maß zu treffen!
Sei B eine Unterklasse von A; gegeben ein Stück Code, in dem Objekte
der Klasse A vorkommen.
Dann muß der Code sich immer noch genau so verhalten, wenn man die Objekte vom Typ A durch Objekte vom Typ B ersetzt. Dies muß auch für alle anderen Unterklassen B' von A erfüllt sein. |
Die Idee ist, daß ein Anwender der Unterklassen von A immer das gleiche Verhalten erwarten kann, wenn er nur Features aus der Klasse A verwendet -- und ansonsten sollte die Objekte auch ähnliches Verhalten haben.
Sie sollte aber so designt sein, daß sie erweiterbar ist, für den Fall, daß zusätzliche Features benötigt werden.
Manchmal ist es besser, das Design so anzulegen, daß es in globalen
Algorithmen angelegt wird, die als Templates implementiert werden und
über Iteratoren eine zusätzliche Abstraktion bekommen.
Dies ist genau der Ansatz, den die STL verfolgt.
Robustheit ist übrigens stark verknüpft mit Stabilität (s. Bugsuche) und eine durchgängige Überprüfung der Eingabe-Parameter auf Plausibilität.
Jeder Bug holt einen früher oder später ein. (Es gibt Bugs, die tauchen erst nach 1 Jahr auf!) Meistens passiert das genau dann, wenn man gerade überhaupt keine Zeit hat, ihn zu reparieren. (Wegen Demo, oder Abgabe, etc.)
Viel schlimmer noch sind Bugs und unrobuste Software, die den Kunden frustrieren! 1 frustrierter Kunde = 10 verlorene neue Kunden.
Warum ist es so verwerflich, das Rad neu zu erfinden?
| |
11) | Generell sind diese und andere (z.B. math) Funktionen wesentlich effizienter als jeder selbst-geschriebene Code, da sie optimiert und manchmal sogar für die jeweilige Maschine in Assembler geschrieben sind. |
|
Die (sogenannten) Gründe dafür, daß fremder Code nicht benutzt wird, sind meistens:
Wie findet man den Code zum Problem? Zuerst schaut man mit man -k keyword in die Man-Pages ("apropos" Button bei xman). Dann sucht man in den online books (insight und infosearch). Dann fragt man Kollegen. Oft bringt auch eine Suche im Netz oder eine Anfrage in der entsprechenden Newsgroup, z.B. comp.*.{source,software}.* brauchbaren Code (FAQ zuerst lesen!).
Welche Arten von Code-Diebstahl (im Software-Engineering heißt das code re-use) gibt es?
Tools für die Bug-Suche:
| |
12) | abgesehen davon, daß er, in leichten Varianten, auf allen Unixes vorhanden ist |
|
Den dbx startet man mit dbxprogram. Die wichtigsten Befehle:
r options | startet das program mit options als Parameter. Hat man die options einmal angegeben, so braucht man danach nur noch r eintippen. |
t | zeigt stack trace. |
W | zeigt Stelle im Source (falls Source vorhanden). |
stop in function | setzt Breakpoint auf den Eintritt in Funktion. |
stopi at [&]function | setzt Breakpoint vor erste Instruktion von function. Bei stop wird immerhin der Prolog der Funktion ausgeführt. So kann man sicher herausfinden, welche Werte die Argument-Register haben. |
stop at number | setzt Breakpoint auf Zeile im aktuellen File. |
file "name" | schaltet aktuellen File um. |
c | continue. |
p C-Ausdruck | zeigt Wert des Ausdruckes. |
dump | druckt den Wert aller lokalen Variablen einer Funktion. |
<return> | wiederholt den letzten Befehl. |
help [topic] | Online-Hilfe. |
Online Hilfe bekommt man mit help, help most_used, help cplusplus_names.
ctrace ist ein Source-Code-Instrumentierer, der einen C-File so modifiziert, daß jede Zeile mit den Werten der modifizierten Variablen ausgegeben werden, während das Programm ausgeführt wird. (Funktioniert nicht für C++, glaub ich. Gibt's ein PD-Tool?) Doku: siehe Man-Pages.
Apropos "Nachdenken": meistens führt die richtige Mischung aus Intuition, kombiniert mit einem raschen Aufruf des Debuggers und ein paar gezielten Breakpoints am schnellsten zum Ziel. Es dauert relativ lange, diese Intuition zu erlangen, aber es lohnt sich und macht einen guten Programmierer aus. Voraussetzung ist, daß man das "große Bild" ("the big picture") von der involvierten Software hat.
Das Lesen von Man-Pages erfordert ein bißchen Übung; hat man aber erst mal das Prinzip geschnallt, ist es gar nicht mehr so schwer und man findet relativ schnell die Dinge, die man braucht. Und bevor Du anfängst zu schimpfen über die "Scheiß-Man-Pages" --- warte damit bis Du selbst mal Doku schreiben mußt!
Man-Pages, die man als Unix-User kennen sollte:
ls, cp, ln, tar,
vi (oder anderer Editor), find, die man page seiner Shell,
ed (der Abschnitt über reguläre Ausdrücke),
grep,
Man Pages, die man als C-Programmierer kennen sollte:
string, bstring,
printf, scanf, atoi, fputs, fgets,
putc, putchar, getchar,
math, stdarg, stdio, stdlib,
malloc, alloca, memcpy,
open, fopen, read, write, writev,
readv,
isalpha, isdigit, isspace,
intro(2), environ(5)
dbx oder cvd, cc, ld, nm, make oder pmake, rcs oder sccs
Man-Pages, die man immer wieder lesen muß, auf jeden Fall immer dann, wenn ein neues Release des Betriebssystems herausgekommen ist: cc (Achtung: es gibt mehrere! einige sind veraltet!), ld, rld, dbx, ipa(5)
Standard-Libs und -Funktionen, die man als Programmierer unter Unix kennen sollte (zumindest wissen, daß es sie gibt):
Als "advanced programmer" sollte man kennen:
Das wichtigste Tool überhaupt für einen Programmierer ist der Editor. Dein Editor ist für Dich ein Werkzeug, das Dir helfen soll, schnell und effizient zu programmieren. Meiner Meinung nach sollte er folgende Features besitzen:
Meiner Meinung nach erfüllen nur vim (der aufwärtskompatible Nachfolger von vi) und emacs diese Bedingungen. Dabei hat vim noch den Vorteil, daß vi auf jeder Unix-Maschine garantiert vorhanden ist. Und emacs hat den Nachteil, daß die meisten Leute den reinen Textmodus gar nicht bedienen können. 14)
| |
14) | Abgesehen von diesen beiden Vor-/Nachteilen ist die Wahl des "richtigen" Editors eine reine Angelegenheit des "Charakters": nedit und xemacs ist gut für Leute, die sofort losschreiben wollen und kein Problem damit haben, die Control- und/oder Alt-Taste gedrückt zu halten, um einen Befehl des Editors auszuführen. vim ist gut für Leute, die gerne so wenig wie möglich tippen, um einen Befehl an den Computer zu geben, aber dafür kein Problem haben, erst einmal "i" oder "o" zu tippen, bevor sie losschreiben können. |
|
Ein anderes wichtiges Tool ist ein Man-Page-Reader. Hier empfehle ich xman, oder besser noch tkman.
Ab und zu sollte man purify über seinen Code laufen lassen. Das ist ein Tool zum Finden von Memory-Leaks und Memory-Corruption. (Low-cost-Alternative: electric fence.)
[2] Ian Darwin: Can't happen, or, Real Programs Dump Core. SoftQuad, Inc., 1984-1985. canthappen.ps
[3] L. W. Cannon et al. Recommended C Style and Coding Standards. Bell Labs, 1990. cstyle.ps
[4] Mike Haley: Writing C++ Source Code in the Medical Visualization Group. Fraunhofer Center for Research in Computer Graphics, Inc.
[5] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, MA.
[6] Ellemtel Telecommunications Systems Labporatories: Programming in C++ -- Rules and Recommandations. Älvsjö, Sweden. C++rules.ps
[7] Richard P. Gabriel: The Rise of "Worse is Better". http://www.kde.org/food/worse_is_better.html, http://opera.cit.gu.edu.au/essays/wib.html
[8] ?: Programmierrichtlinien ARVIKA, ARVIKA Konsortium.
[9] tmh@possibility.com: C++ Coding Standard, 1999-05-12, http://www.possibility.com/Tmh/.
[10] David Williams: C++ portability guide, version 0.7, http://www.mozilla.org/hacking/portable-cpp.html
[11] Geotechnical Software Services: C++ Programming Style Guidelines, http://www.geosoft.no/style.html
[12] Scott Meyers: How Non-Member Functions Improve Encapsulation, http://www.cuj.com/archive/1802/feature.html
[13] Peter Schröder: Some Programming Style Suggestions, http://mrl.nyu.edu/~dzorin/intro-graphics/handouts/style.html