Ich boykottiere die neue Rechtschreibung!
(... bis auf Ausnahmen)

C++ Programmierrichtlinien
plus einige Tips & Tricks

C++ Programming (Style) Guidelines
plus some Tips & Tricks

Original location of this document:
www.in.tu-clausthal.de/~zach/progr/guidelines.html

Gabriel Zachmann
TU Clausthal, Institut fuer Informatik
Julius-Albert-Str. 4
D-38678 Clausthal
email: zach tu-clausthal.de

Version 2.3, Nov 2004


Inhalt

Teil 1 - Kurzfassung (für Experten)

Teil 2 - für Noch-Nicht-Experten


Intro
Programmier-Stil
    Die n Gebote für Programmierer
    The Python Way
Namenskonventionen
    Intro
    Meta-Naming (C++)
    C und C++
    C
    C++
Kommentare
Strukturierung und Layout der Files
    Anordnung innerhalb einer Klasse
    Einrückung
Wartbarkeit
    Leserlichkeit
Gute und schlechte Programmier-Praxis
    C++
    Round-off errors
    Fest-verdrahtete Pfade
    Arrays
    Magic numbers
    Makros
    Annahmen
    Eingabe-Parameter
    "Can't happen"-Fälle
    Misc
    Anfänger-Bugs
    Kleinere Angelegenheiten
    Optimierungen
Object-oriented Design
    Allgemeine Richtlinien
    Liskov's Substitution Principle
    Open/Closed Principle
    Klasse oder Algorithmus?
Robustheit
Arbeitsmethoden
    "Später kommt nie"
    Wie klaut man Code?
    Wie sucht man Bugs?
    RTFM
    Tools
    Bibliographie, weitere Guidelines

Teil 1 - Kurzfassung (für Experten)

Dieser Teil ist für erfahrene Programmierer gedacht.

Namenskonventionen

An dieser Stelle nur Beispiele, die unsere Konventionen verdeutlichen sollen. Ergänzende Details gibt es weiter unten. (Zum Meta-Naming.)
Natürlich ist das wirklich Wichtige von Namen deren Aussageb) für den Benutzer (wirklich gute Programmierer erkennt man an ihren guten Objekt- und Methodennamen).


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".
 

Kommentare

Verwende die Templates in template-comments.cpp.
Mehr zum Kommentieren.

Strukturierung und Layout der Files

Verwende die Templates template.cpp und template.h. (Details dazu.)
Für C-Files verwende template.c. C++-Files haben das Suffix .cpp, Header-Files haben das Suffix .h, die Implementierung von Templates haben das Suffix .hh.

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!
K&R-Style ist verboten!
Strukturiere eine Zeile sinnvoll durch Spaces.
Mehr zum Thema Einrückung und Spaces.

Teile größere Sinnzusammenhänge des Codes (oder auch des Headers) durch eine Leerzeile ab (quasi ein "Absatz").

Do's and Dont's

Lies Scott Meyers' "Effective C++" und "More effective C++".

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ültig
Gib beim Compilieren auf SGI die Option -LANG:ansi-for-init-scope an.
Gib beim Compilieren mit Intel's Compiler die Option -Qoption,cpp,--new_for_init 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.)

sample_icon

Code-Beispiel

Für Eilige ist hier ein Beispiel mit annotiertem Code, der einige der Regeln dieser Guidelines enthält.

Compiler-Optionen

Folgende Optionen sollen immer gesetzt sein:

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

Teamarbeit

Wir wollen möglichst viel Code-Reuse machen.
Darum sollten alle folgendes tun:
Frage!
Bevor Du anfängst zu programmieren, frage auf der Mailing-List, ob es nicht schon dieses oder ein ähnliches Stück Code im System gibt.

Siehe auch how to steal code.

Erzähle!
Wenn Du etwas implementiert hast, eine neue Funktion, eine neues Feature, eine neue Aktion/Event, etc., schreibe eine kurze Mail an die Mailing-List.
So können andere vielleicht einmal davon profitieren und Deinen Code / Dein Feature benutzen.




Teil 2 - für Noch-Nicht-Experten

Intro

Diese Guidelines und Tips sind für alle diejenigen gedacht, die relativ neu in der Abteilung sind, und die noch keine jahrzehntelange Programmiererfahrung in C/C++ haben (also z.B. HiWis und Diplomanden).

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.

Good points, bad points

Dieser Abschnitt ist aus "C++ Coding Standard", aber weil er recht gut zusammenfaßt, wozu Guidelines gut sind, möchte ich ihn hier einfach kopieren:

Good Points

When a project tries to adhere to common standards a few good things happen:

Bad Points

Now the bad:

Programmier-Stil

Dazu gehören syntaktische als auch "semantische" Gepflogenheiten. Ich behaupte, daß es ohne einen guten Programmierstil nicht möglich ist, guten Code zu schreiben (im Sinne von Robustheit, Wartbarkeit, Effizienz, Eleganz). Ich behaupte auch, daß man nur mit einem guten Programmierstil langfristig effizient programmieren kann! (Denn: schlechter Stil -> mehr Bugs oder schlechtes Design -> längere Bugsuche bzw. mehr Redesigns -> mehr Zeitaufwand in der Summe.)

Die n Gebote für Programmierer

Rahmt Euch die ein und hängt sie neben den Badezimmerspiegel! :-)

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.

The Python Way

Hier ist noch eine kleine Liste von Programmierregeln, die ich aus dem Netz aufgeschnappt habe. Ich lasse sie in Englisch.

  1. Beautiful is better than ugly.
  2. Explicit is better than implicit.
  3. Simple is better than complex.
    (Aus Big Ball of Mud: "A complex architecture may be an accurate reflection of our immature understanding of a complex system or problem.")
  4. Complex is better than complicated.
  5. Flat is better than nested.
  6. Sparse is better than dense.
  7. Readability counts.
  8. Special cases aren't special enough to break the rules.
  9. Although practicality beats purity.
  10. Errors should never pass silently.
  11. Unless explicitly silenced.
  12. In the face of ambiguity, refuse the temptation to guess.
  13. There should be one -- and preferably only one -- obvious way to do it.
  14. Now is better than never.
  15. Although never is often better than right now.
  16. If the implementation is hard to explain, it's a bad idea.
  17. If the implementation is easy to explain, it may be a good idea.
  18. Namespaces are one honking great idea --- let's do more of those!

Namenskonventionen

Intro

Es kommt einigen von Euch vielleicht lächerlich oder nervig vor, daß wir auf Namen so großen Wert legena). Tatsächlich ist aber eine gute Benennung von Variablen, Funktionen, Methoden und Klassen das allerwichtigste Kriterium für einen guten Programmierer (und ein gutes Design)! Besonders beim objekt-orientierten Programmieren ist das fast noch wichtiger als bei reinem C. Eine schlechte Benennung kann eine Library fast unbrauchbar machen.

 
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.

Meta-Naming (C++)

Die von Stroustrup eingeführten Begriffe member function etc. sind eine Unsitte! Die Dinger heißen "Klassenmethoden" (static member functions) und "Instanzenmethoden" (non-static member functions). Analog für Variablen.

C und C++

Funktionen/Methoden tun meistens etwas, deswegen sollten ihre Namen zusammengesetzt sein aus verb + Substantiv (inCaps-Notation). Hier ein Beispiel für unsere Konvention: calcDistance. (Andere Konventionen wären: SetParam, oder create_stripe. Ich persönlich finde die Underscore-Schreibweise nicht so schön.)

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.
Siehe das "Enum-Problem in C++".

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.

C

Es gilt dasselbe wie oben für C++; allerdings müssen Funktionen zusätzlich ein Präfix (2-4 Buchstaben) haben, welches für das Modul steht, in dem sie definiert sind: Präfix + Verb + Substantiv. Z.B.: plhCalcDistance, INTOsetButtonParam, oder pf_create_stripe. Dasselbe gilt für Funktionen, die eine Eigenschaft liefern: Präfix + "Is" + Adjektiv; z.B. plhIsFlat.

C++

Man verwendet für Klassen dieselben Namenskonventionen wie für Funktionen, außer daß Klassennamen mit einem Großbuchstaben anfangen. Es ist nicht nötig, Klassennamen mit einem großen C als extra Präfix oder Suffix zu versehen (redundant). Klassenvariablen/-methoden und Instanzvariablen/-methoden werden nach der selben Namenskonventionen gebildet.

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.)
Das Enum-Problem in C++
In C war es einfach, Enum's dort zu verwenden, wo mehrere "Optionen" verodert als Parameter übergeben werden sollten. Z.B.:
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!
(Das einzige Tools, das ich kenne, und das es schaffen könnte, ist Perceps.)
Auch alle anderen mir bekannten Varianten zur Lösung des Enum-Problems können nicht "transparent" von den Dokumentations-Tools verarbeitet werden.

Kommentare

Generell gilt: So wenig Kommentar wie möglich, so viel Kommentar wie nötig.

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:

  1. Am Anfang einer Klasse ein Überblick über das "große Bild", die Funktionen des Files (der Klasse), verwendete "Compile-Flags" (conditional compilation per ifdef).
  2. Vor jeder Funktion eine Beschreibung für diese, Ein-/ Ausgabe-Parameter, pre- und post-conditions, Seiteneffekte, Caveats, Bugs, etc. (Zu pre- und post-conditions siehe auch das assert-Makro).
  3. Im Code selbst für größere Blöcke von Zeilen.
  4. Für Variablen (alle globalen bzw. Klassenvariablen und teilweise lokale), Members von struct-typedefs und ähnlichem.
Kommentare sollten in einer einheitlichen Form im ganzen Modul gemacht werden (siehe das Kommentar-Template). Kommentare für Funktionen sollen mit dem im Template gezeigten Block gemacht werden, damit sie durch ein automatisches Tool extrahiert werden können. Ihr könnt Kommentare in Englisch oder in Deutsch machen, je nach dem, wo Ihr Euch wohler fühlt.

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;

Strukturierung und Layout der Files

Ein Template für C bzw. C++ Files findet man im CVS in internal/Templates/class.{cpp,h}. (Ersetze CLASSNAME durch den Namen der Klasse, oder, besser noch, laß das den Editor beim ersten Erzeugen des Files machen.)

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 -c
Damit 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.)
(Abgesehen davon sollten alle diesen File ~/.cvsignore in ihrem Home stehen haben.)

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.

sample_icon Hier findet man ein graphisches, annotiertes Beispiel, wie Source-Code aussehen soll.

Anordnung innerhalb einer Klasse

Klassen haben mehrere Abschnitte, analog zu C-Files, die folgendermaßen geordnet sein sollen:
  1. public Konstanten, public Typen
  2. public Variablen (falls zugelassen)
  3. public Methoden
  4. protected Zeugs (selbe Reihenfolge)
  5. private "parts" :-) (selbe Reihenfolge)

Header-Files

Der Aufbau von Header-Files ist ähnlich wie der von C-Files.

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).

Reines C
In einen Header-File gehört nur das, was ein Anwender der Lib oder des Object-Files wirklich wissen muß --- alles andere gehört in die entsprechenden C-Files oder in "interne" Header-Files, die nicht im "Anwender-Header-File" included werden. (Das kann bei großen Projekten nicht-trivial werden! :) ) Zu normalen Applikationen gibt es i.A. keine extra Header-Files!
Bei C++ geht das nicht so einfach/elegant, da man leider auch die private-Methoden im Header-File deklarieren muß.

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
}
#endif
So können sie sowohl in C-Files als auch C++-Files included werden.

Einrückung und Spaces

Wir verwenden folgende Konvention:
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;
}

Wartbarkeit

Maintainance besteht i.A. aus leichtem Modifizieren des Sources. Diejenigen, die diese Maintainance machen, sind fast nie diejenigen, die den Source ursprünglich erstellt haben (aus verschiedenen Gründen). Selbst wenn derjenige, der den Code modifiziert, der ursprüngliche Autor ist: wenn man sich ein Jahr lang nicht mehr ständig mit diesem Code beschäftigt hat, dann sind die Details und manchmal auch das "große Bild" vergessen!

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:

  1. Falls eine Funktion ausschließlich über das Public-Interface einer Klasse implementiert werden kann, dann soll diese Funktion keine Member-Funktion sein! Das erhöht die Kapselung. (Siehe [12])
  2. Mit Kommentaren kann man für andere Hinweise geben.
  3. Faktorisieren: Wenn man zwei Funktionen hat
    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 ...

  4. Vermeide Code, den man später "mißverstehen" kann; z.B. Zuweisungen in Bedingungen:
    if ( a = b )
        
    wird garantiert später von jemand, der einen Bug in dieser Funktion sucht, "repariert" zu
    if ( a == b )
        

    Oder: Ist in dem Code

    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.

Leserlichkeit

Oberstes Ziel in diesem Zusammenhang ist die leichte Lesbarkeit des Source-Codes für Andere! (Insbesondere für mich.) Wer nach dem Motto programmiert, "es war für mich hart zu programmieren, dann soll es für die anderen wenigstens schwer zu lesen sein", der verhält sich einfach nur unkollegial.

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)

Gute und schlechte Programmier-Praxis

Oder: "It's not a feature - it's a bug."

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.

C++

Vererbung

Bevor Du eine Klasse B als Unterklasse von A deklarierst, frage Dich, ob zwischen den beiden Klassen wirklich die Beziehung "B ist ein A" besteht, oder ob nicht eher die Beziehung Insbesondere wird oft fälschlicherweise Mehrfach-Vererbung verwendet, wenn ein Objekt eigentlich Pointer auf mehrere andere Objekte enthält (mehrfache "benutzt"-Beziehung)!

Konstruktoren und Destruktoren

Schreibe immer einen virtual Destruktor, auch wenn die Klasse keinen braucht! (Problem: delete auf Zeiger auf Basisklasse.) Die einzige Ausnahme sind sehr kleine Klassen (speichermäßig), und wenn der Extra-Speicher für die vtable nicht akzeptabel ist. In solch einem Fall muß das unbedingt im Klassenkommentar vermerkt werden!

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:

  1. In Konstruktoren darf nur Code stehen, der garantiert nicht fehlschlagen kann.
    Verwende statt dessen immer eine init-Funktion, um die "wirkliche" Initialisierung eines Objektes zu erledigen (die auch mißlingen kann):
            Class *o = new Class();
            if ( o->init() < 0 )
            {
                error ...
            }
            
    Problem: es können trotzdem Excpetions entstehen (z.B. kann new fehlschlagen).
  2. Verwende Exceptions.
Initialisiere immer alle Instanzvariablen im Konstruktor. Verwende keine globalen Variablen im Konstruktor.

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:
  1. Name::Name wird aufgerufen mit Parameter "Joe"
  2. neighbor.s wird durch den Default-Konstruktor String::String erzeugt. Das erzeugt einen 1-Byte großen Speicherblock für das Zeichen '\0'.
  3. Ein temporärer String "Joe" wird erzeugt als Kopie des Parameters t mit Hilfe des Copy-Konstruktors (noch ein malloc).
  4. Die Zuweisung wird durchgeführt (mittels des =-Operators).
    Dazu wird der alte String in s deleted, ein neuer erzeugt mit new und dann ein strcpy gemacht.
  5. Der temporäre String wird gelöscht (delete/free).
Insgesamt: 3 news, 2 strcpys und 2 deletes.

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)
{}
  1. Name::Name wird aufgerufen mit Parameter "Joe"
  2. s wird initialisiert von t mittels String::String( const char *)
  3. String::String("Joe") macht ein new und einen strcpy.
Insgesamt: 1 new, 1 strcpy. (keine temporären Objekte, delete!)

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).

Casts

Verwende die neuen Casts:

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.

Exceptions

Achte auf "exception-safety": wenn eine Exception geworfen wird, muß das Objekt immer noch konsistent sein, und keine Resourcen (z.B. Speicher) lecken, und das Programm insgesamt in einem "vernünftigen" Zustand sein, so daß die Ausführung fortgesetzt werden kann.
Das bedeutet, daß der Programmierer bei jede Zeile bedenken muß, daß eine Exception geworfen werden könnte.

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:

Siehe auch Exception-specification rationale der Boost-Library.

Methoden, Funktionen und Operatoren

Wenn eine Methode irgendwo in der Vererbungshierarchie virtual deklariert ist, dann soll sie überall in der ganzen Hierarchie virtual deklariert werden.
Damit ist besser dokumentiert, daß eine Methode überladen werden kann, bzw. daß die entsprechende Methode in der Oberklasse tatsächlich virtual ist. Der Standard sagt zwar " once virtual, always virtual", aber "explizit ist besser als implizit".

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.

Pointer oder Reference?

Das Problem: Man kann Funktionsparametern, die als Reference deklariert sind, nicht ansehen, daß sie eben nicht call-by-value sind5)! Wenn Du gerne References als formale Parameter in Funktionen verwenden möchtest, dann nur als const typ &parameter!
Grund: Wenn man die Konvention vereinbart, daß ein Pointer-Parameter verändert werden darf und ein Referenz-Parameter nicht, dann kann man den Referenz-Parameter auch gleich mit const deklarieren, da damit auch der Compiler gewissermaßen über diese Konvention "informiert" wird (und damit besser optimieren kann).
 
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() );

Misc

Instanz- oder Klassenvariablen sollen nie public sein. Verwende statt dessen get- und set-Funktionen. (Sonst wird "data hiding" verletzt.)
(Ausnahme: const public Variablen. Diese können nur in der Initialisierungsliste der Konstruktoren gesetzt werden.)

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!
Statt
A a = A();      // verboten
schreibe
A a;
a = A();        // ok (wenn auch unnötig)
wenn es denn sein muß.
Grund: die erste Variante liefert verschiedenes Verhalten mit verschiedenen Compilern, und kann dazu führen, daß der Destruktor einmal mehr aufgerufen wird als der Konstruktor (SGI's Compiler-Bug).

Floating-Point Arithmetik und Round-Off Errors

In den Beispielen hier sind alle Variablen floats.

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) )

Fest-verdrahtete Pfade

Ganz miserabel:
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 ..." );

Arrays

Feste Array-Größen
Bedenke: Durch "feste" Arraygrößen sind schon viele Security-Holes in Unix entstanden (das ist die Klasse der "buffer overflow security leaks")! (z.B. rsh mit 100k Argument-String, oder >1000 telnet connections pro sec.)
Array-Indizierung
In C werden Arrays grundsätzlich mit 0 beginnend indiziert! Niemals mit 1 beginnend (obwohl es in Fortran so gemacht wird.) Wer es doch so macht verwirrt alle anderen und produziert direkt oder indirekt garantiert einen off-by-one Bug.

Magic numbers

Numerische Konstanten heißen oft auch "magic numbers". Sie machen den Code mindestens unwartbar und unverständlich, und sorgen auch für den einen oder anderen Bug.

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++).

Makros

Durch automatisches Inlining moderner Compiler sind Makros meistens überflüssig geworden. Außerdem ist das Debugging von Makros extrem mühsam, das Inlining von Funktionen hingegen kann man ausschalten. Verwende Makros nur, wenn es mit einer Funktion nicht geht.

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!
Wenn das Makro nämlich expandiert wird zu:
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:

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 * ... );

Annahmen

Verlasse Dich niemals darauf, daß Funktionen sich so verhalten, wie Du denkst, wenn es nicht explizit in der Man-Page steht! Einige Beispiele:

Eingabe-Parameter

Viele Bugs entstehen dadurch, daß Parameter nicht auf Gültigkeit und Plausibilität gecheckt werden7)

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!

"Can't happen"-Fälle

Eigene Funktionen. Wenn Du Deine eigenen Funktionen verwendest, wird es viele Stellen geben, wo gewisse Parameter-Kombinationen oder Variablen-Belegungen zwar laut Code vorkommen könnten, wo Du aber weißt, daß das nicht passieren kann, weil Du die Funktion nur mit bestimmten Parametern aufrufst.

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.

Misc

Verwende nie denselben Filenamen mehrfach in einem Software-System! Weder bei Header-Files noch bei C-Files.

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
}

Anfänger-Bugs

Jeder Anfänger macht folgende Bugs --- mach Dir also nichts daraus, wenn sie Dir auch passieren :) (auch mir sind sie passiert):

Kleinere Angelgenheiten

Wenn man structs "vorne" und "hinten" typedefen muß, so soll man denselben Namen wählen:
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.

Optimierungen

Generell gilt: optimiere zuerst den Algorithmus, und nicht die Implementierung durch vereinzelte "Tricks"! Der Compiler kennt die CPU viel besser als Du.

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:

  1. cc -n32 -O ...
    
  2. Für C++-Code kann Inlining ein bißchen Geschwindigkeit bringen, wenn man viele kleine get- und set-Funktionen hat. Dazu muß man keinen Source im Header-File schreiben! Das geht mit der richtigen Compiler-Option:
    cc -n32 O -INLINE:=ON
    
    schaltet 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=ON
        
    Dann 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).

  3. Die ganz heftigen Compiler-Optionen sind:
    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
        
Wer mehr zu Inlining und anderen Compiler-Optionen für die Optimierung wissen will, macht man 5 ipa, oder schaut im Insight-Book "MIPSpro Compiling and Performance Tuning Guide" nach.

Beispiele von Pseudo-Optimierungen

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 :))
Denn: mit Nebeneffekten (*i++) nimmt man dem Compiler sogar Möglichkeiten zur Optimierung! (Z.B. durch Vertauschen von Assembler-Zeilen.) Außerdem ist strcpy() sehr sorgfältig in Assembler codiert.

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)

Ungeschicktes Codieren

Vermeide FPEs (floating-point exceptions). Zwar werden sie i.a. ignoriert, kosten aber doch Zeit, da die Exception trotzdem erzeugt und bearbeitet wird. FPEs können u.a. durch Rechnen mit uninitialisierten Variablen oder NaN'sentstehen.

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.)
Miserabel:
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.

Object-oriented Design

In diesem Abschnitt werden ein paar grundlegendste Richtlinien von objekt-orientiertem (oo) Design (OOD) beschrieben. Die meisten sind unabhängig von der Sprache (man kann ja ein OOD sogar in Assembler implementieren).

Echte Optimierungen

Richte Arrays, deren Größe in der selben Größenordnung wie eine Cache-Zeile ist, an entsprechenen Memory-Boundaries aus. (Bsp.: eine Cache-Zeile ist 64 Bytes lang (Pentium 4), also richte Arrays von ungefähr dieser Größe auch an 64-Byte-Boundaries aus.)

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.

Allgemeine Richtlinien

Hier einige grundlegende Richtlinien eines jeden Moduls oder Library:
  1. Einfachheit.
    Sowohl das Interface als auch die Implementierung muß einfach sein Die Einfachheit des Interfaces hat Priorität gegenüber der Einfachheit der Implementierung -- trotzdem ist eine einfache Implementierung wichtig, besonders für die Wartbarkeit.
  2. Korrektheit.
  3. Konsistenz.
    Dieses Kriterium ist vielleicht am schlechtesten objektiv meßbar. Nichtsdestotrotz ist Korrektheit genauso wichtig wie Einfachheit. Um Konsistenz zu erreichen kann man, wenn unbedingt nötig, ein wenig von der Einfachheit opfern -- aber nie umgekehrt!
  4. Vollständigkeit.
    Das Design / die Library / das Modul muß alle möglichen Situationen abdecken, und ein paar mehr, da es sehr schwer ist, alle Fälle vorauszusehen.
    Falls die Einfachheit extrem leiden würde, ist es besser auf die Vollständigkeit zu verzichten.
Wichtig ist auch, die richtige Beziehung ("is-a", "uses", "is-like") zwischen Klassen zu finden. Es ist falsch, als erstes an Vererbung zu denken.

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!

Liskov's Substitution Principle

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.

Open/Closed Principle

Dieses Prinzip verlangt, daß Klassen sowohl offen als auch geschlossen sind in folgendem Sinn: Dieses Prinzip zielt auf Stabilität. Wenn eine Klasse einmal für gut befunden ist durch Reviews, Tests, und Praxis, dann sollte sie nicht mehr verändert werden.

Sie sollte aber so designt sein, daß sie erweiterbar ist, für den Fall, daß zusätzliche Features benötigt werden.

Klasse oder Algorithmus?

Manche Leute übertreiben es mit der Objektisiererei. Sie machen aus allem ein Objekt. Vielleicht glauben sie, so besonders "objekt-orientiert" (und damit "in") zu sein.
aber wie schon Alexandr Stepanov (das Master-Mind hinter der STL) sagte: "Es ist Blödsinn, aus allem ein Objekt (eine Klasse) zu machen -- ein Sortieralgorithmus ist kein Objekt."

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

Darunter versteht man zweierlei:
  1. Graceful Degradation: Das Programm muß auch unter "widrigen" Bedingungen so sinnvoll wie möglich weiterlaufen. Z.B., wenn ein Konstruktor oder malloc() nicht geklappt hat, weil zu wenig Speicher vorhanden, oder wenn ein File nicht da ist, der eigentlich da sein müßte, oder das Programm sonst irgendwas nicht bekommt, was es eigentlich braucht -- es muß weitergehen ohne Core-Dump!
  2. Robustheit des Source-Codes gegen Application-Bugs: wenn jemand Deine Funktionen verwendet, aber die Doku dazu nicht genau gelesen hat (es gibt doch eine, oder?!), dann darf die Funktion nie abstürzen oder totalen Blödsinn produzieren.
Grundsätzlich gelten die Gesetze von Murphy: wenn etwas schief gehen kann, dann geht es auch schief, und zwar immer erst beim Kunden!

Robustheit ist übrigens stark verknüpft mit Stabilität (s. Bugsuche) und eine durchgängige Überprüfung der Eingabe-Parameter auf Plausibilität.

Arbeitsmethoden

Es ist klar, daß jeder seinen eigenen Arbeitsstil und -methoden hat, wie auch jeder seinen eigenen Programmierstil hat. Allerdings ist es unbedingt notwendig, daß Du Dir beizeiten einen sorgfältigen Arbeitsstil angewöhnst.

"Später kommt nie"

Sorgfältiges Programmieren braucht zuerst (scheinbar) länger, aber es ist auf die Dauer effektiver.

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.

Wie klaut man Code?

Viele Programmierer wollen leider alle Räder selber neu erfinden. Man nennt es das "not invented here" Syndrom, oder "not invented by myself" Syndrom.

Warum ist es so verwerflich, das Rad neu zu erfinden?

Auf keinen Fall darf jemand Funktionen neu schreiben, welche schon in Unix dabei sind oder in einer Library, die vom Rechnerhersteller mitgeliefert wird! Dazu gehören: Siehe auch RTFM.

Die (sogenannten) Gründe dafür, daß fremder Code nicht benutzt wird, sind meistens:

Fazit: man muß schon sehr gute Gründe haben, wenn man das Rad neu erfinden will. Ein existierendes Rad zu benutzen oder zu verbessern ist fast immer der schnellere, robustere und zukunftsträchtigere Weg.

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?

Wie sucht man Bugs?

Da gibt es leider keine Strategie, die garantiert und immer auf dem schnellsten Weg zum Erfolg führt. Hier ein paar Faustregeln und Tips:

Tools für die Bug-Suche:

  1. Der Debugger ist immer noch das wichtigste Tool (nicht printf). Deswegen: lerne effizient mit Deinem Debugger umzugehen! Meiner Meinung nach ist dbx immer noch das Tool, mit dem man am schnellsten Debuggen kann (in 99% aller Fälle) 12), auch wenn er keine bunten Icons hat und man sich ein paar Befehle mehr merken muß.
     
    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.
    Am besten kopiert man .dbxinit aus meinem Home in sein eigenes Home.

    Online Hilfe bekommt man mit help, help most_used, help cplusplus_names.

  2. Code-Instrumentierer: hat man einen Verdacht auf einen Memory-Bug, so hilft purify oft weiter. (Low-cost-Alternative: electric fence.)

    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.

  3. Compiler: verschiedene Compiler-Optionen (SGI).
  4. Run-time Loader (rld): er sorgt beim Start eines Programms dafür, daß alle Libraries dazugelinkt werden und Referenzen aufgelöst werden.
    Hilfreiches Flag zum Debuggen: setenv _RLD_ARGS "-clearstack". Wenn Dein Programm danach geht, dann initialisierst Du irgendwelche Variablen nicht!
  5. Libraries: (sog. Intercept-Layers). malloc_cv eignet sich ziemlich gut zum Finden von Memory-Corruption. Es ist schnell dazugelinkt, kein Instrumentieren ist nötig, und es ist hinreichend mächtig. (S. Man-Page für mehr Info.)

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.

RTFM

Als Programmierer muß man sich daran gewöhnen, Doku zu lesen. Seien dies Man-Pages, Insight-Books, White-Papers, oder was auch immer! Man muß sich auch daran gewöhnen, sie gründlich und trotzdem schnell zu lesen.

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:

Tools

Noch ein kleines Wort über Tools die man als Programmierer im täglichen Leben braucht. Als Programmierer mußt Du 4 Tools gut beherrschen: Deinen Editor (welcher auch immer), den Compiler, den Debugger, und Makefiles. (Von jedem guten Handwerker verlangt man auch, daß er seine Werkzeuge kennt und beherrscht.)

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:

  1. Er sollte hinreichend mächtige Features besitzen: Search-and-Replace von regulären Ausdrücken, auch über einzelne Text-Bereiche; Makros; automatisches Indenting und Exdenting; Wiederholen von Kommandos und Command- und Search-Histories; Unterstützung von Tags; wenn der Cursor auf einem Identifier ist, muß man mit wenigen Tasten oder Mausklicks zur Deklaration dieses Identifiers springen können (egal ob Typ, Variable oder Funktion); Aufruf von Make vom Editor aus, und Unterstützung der anschließenden Abarbeitung der Fehlerliste des Compilers; nice-to-have ist Syntax-Highlighting (nicht nur für C); key-mapping und Abkürzungen;
  2. Auf jeder Plattform verfügbar sein. (Denkt daran, daß Ihr höchstwahrscheinlich nicht immer an SGIs arbeiten werdet.)
  3. Auch ohne Grafik funktionieren, d.h., wenigstens einen rein text-basierten Modus haben, denn Ihr auch bedienen könnt. (Früher oder später werdet Ihr in der Situation sein, daß Ihr an einem vt100-Emulator sitzt und remote etwas editieren müßt ...)

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.)

Referenzen

[1]   Henry Spencer: How to Steal Code, or, Inventing the Wheel only Once. how-to-steal-code.ps

[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



Gabriel Zachmann
Last modified: Sun Aug 02 15:07:32 MDT 2009