C++ Programmierrichtlinien
plus einige Tips & Tricks
Original location of this document:
cgvr.informatik.uni-bremen.de/progr/guidelines.shtml
Gabriel Zachmann
Universität Bremen, Fachbereich Mathematik/Informatik, Computergraphik und virtuelle Realität
Linzer Straße 9a, OAS
28359 Bremen
zach at informatik.uni-bremen.de
Version 2.3.2, Oct 2013
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 |
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 Instanzmethoden), wenn es möglich ist. (Grund: siehe "Effective C++", Item 19. Letzten Endes läuft es auf folgendes hinaus: wenn es nicht unbedingt in der Klasse sein muß, definiere eine Methode außerhalb der Klasse, also als globale Methode. Dazu Scott Meyer: "I've been battling programmers who've taken to heart the lesson that being object-oriented means putting functions inside the classes containing the data on which the functions operate. After all, they tell me, that's what encapsulation is all about. They are mistaken." Hier findet man eine ausführliche Begründung von Scott Meyers (Quelle) ).
Reference-Parameter werden immer mit const deklariert, ansonsten als Pointer (d.h., sie können verändert werden) (Grund).
Benutze Namespaces.
Schreibe niemals using namespacemy_package;
in einem Header-File!
Wenn Methoden überladen werden, müssen sie virtual sein.
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.)
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 Intel unter Windows :
-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 ...
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:
- programmers can go into any code and figure out what's going on
- new people can get up to speed quickly
- people new to C++ are spared the need to develop a personal style and defend it to the death
- people new to C++ are spared making the same mistakes over and over again
- people make fewer mistakes in consistent environments
- programmers have a common enemy :-)
Bad Points
Now the bad:
- the standard is usually stupid because it was made by someone who doesn't understand C++
- the standard is usually stupid because it's not what I do
- standards reduce creativity
- standards are unnecessary as long as people are consistent
- standards enforce too much structure
- people ignore standards anyway
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.
- Beautiful is better than ugly.
- Explicit is better than implicit.
- 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.") - Complex is better than complicated.
- Flat is better than nested.
- Sparse is better than dense.
- Readability counts.
- Special cases aren't special enough to break the rules.
- Although practicality beats purity.
- Errors should never pass silently.
- Unless explicitly silenced.
- In the face of ambiguity, refuse the temptation to guess.
- There should be one -- and preferably only one -- obvious way to do it.
- Now is better than never.
- Although never is often better than right now.
- If the implementation is hard to explain, it's a bad idea.
- If the implementation is easy to explain, it may be a good idea.
- 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.
Ü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:
- Am Anfang einer Klasse ein Überblick über das "große Bild", die Funktionen des Files (der Klasse), verwendete "Compile-Flags" (conditional compilation per ifdef).
- 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).
- Im Code selbst für größere Blöcke von Zeilen.
- 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 (Einzeiler) * * @param param1 blubber (in) * @param param2 bla (out) * * @return * Beschreibung der Rückgabe-Werte, z.B. * -1 falls fehlgeschlagen, 0 wenn alles ok. * * Ausführliche, mehrzeilige Beschreibung, z.B. * Diese Funktion berechnet ... * Basiert auf dem Algorithmus von ... * * @throw Exception * Genaue Beschreibung der Exceptions und deren Bedingungen, z.B. * XOutOfMemory, falls kein Speicher mehr verfügbar. * XCoffee, falls kein Kaffee mehr da. * * @warning * Beschreibung von Eingabe-Parameter-Werten oder Zuständen, * die zu Fehlern oder Abstürzen führen, die aber nicht abgefangen werden. * (sollte nur sehr selten vorkommen!) * * @pre * Genaue Beschreibung der Vorbedingungen, z.B. * Erwartet, dass die Funktion init() schon aufgerufen wurde. * Param1 muss von der Funktion blub() berechnet worden sein. * * @sideeffects * Nebenwirkungen, globale Variablen, die verändert werden, .., z.B. * @arg The global variable @c M_Interest will be modified. * * @todo * Schneller machen. * * @bug * Produziert einen core dump, wenn @a param1 = 0.0 ist. * * @internal * * @see * Querverweise auf andere Funktionen, z.B., weil sie etwas ähnliches berechnen, * oder weil es irgend eine Abhängigkeit zwischen beiden gibt. Beispiel: * 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 -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.)
(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).
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.
Anordnung innerhalb einer Klasse
Klassen haben mehrere Abschnitte, analog zu C-Files, die folgendermaßen geordnet sein sollen:- public Konstanten, public Typen
- public Variablen (falls zugelassen)
- public Methoden
- protected Zeugs (selbe Reihenfolge)
- 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:
- 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])
-
Mit Kommentaren kann man für andere Hinweise geben.
- Im Kommentar vor der Funktion: "Assumptions" und "Caution". Diese Hinweise werden während des Schreibens der Funktion/Methode ausgefüllt! Schon nach 1 Woche weiß sonst auch der Autor nicht mehr, was es alles zu beachten gibt, wenn man die Funktion aufrufen oder gar verändern will!
-
In-line-Kommentar im Body der Funktion für Bedingungen,
z.B.: /* now nelems = 2 */:
oder /* MUST be done before ... because ... */
Dann weiß man auch noch in einem Jahr, wenn man die Funktion verändern muß, daß diese Bedingung im Rest der Funktion gültig sein muß. Außerdem dient es zur eigenen Kontrolle, daß man sich tatsächlich überlegt hat, daß diese Bedingung existiert für den Rest der Funktion! (Wer erinnert sich noch an wp- oder Hoare-Kalkül?) -
Falls ein bestimmter Code-Abschnitt sehr sensibel gegenüber
Änderungen ist, kommentiere das.
Falls Änderungen in einem Stück Code Änderungen in einem anderen Stück Code notwendig macht, kommentiere das (in ersterem!).
-
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 ...
-
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" zuif ( 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 dagegenfor ( 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
- "B benutzt A" oder
"A ist Teil von B" besteht.
In diesem Fall enthält einfach die eine Klasse einen Pointer auf eine Instanz der anderen. Es gibt keine Vererbung zwischen beiden Klassen. - "B ist wie ein A" besteht.
In diesem Fall sind beide Klassen Unterklasse einer gemeinsamen Oberklasse. Das kann bedeuten, daß man erst einmal eine neue Oberklasse anlegen muß und eine Menge Code von Klasse A "nach oben" schaffen muß.
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:
- 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). - Verwende Exceptions.
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:
- Name::Name wird aufgerufen mit Parameter "Joe"
- neighbor.s wird durch den Default-Konstruktor String::String erzeugt. Das erzeugt einen 1-Byte großen Speicherblock für das Zeichen '\0'.
- Ein temporärer String "Joe" wird erzeugt als Kopie des Parameters t mit Hilfe des Copy-Konstruktors (noch ein malloc).
- 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. - 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)
{}
- Name::Name wird aufgerufen mit Parameter "Joe"
- s wird initialisiert von t mittels String::String( const char *)
- 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:
- const_cast<..> um ein const wegzucasten (oder hinzucasten);
- static_cast<type>(expression) statt dem old-style C-Cast (type); Grund: solch ein Cast läßt sich leichter mit grep (und durch "Draufsehen") finden; außerdem wird die Constness nicht verändert.
- dynamic_cast kann dazu verwendet werden, einen Zeiger auf
ein Objekt der Basisklasse in einen Zeiger auf ein Objekt der
Unterklasse zu verwandeln.
Tatsächlich ist der Ausdruck
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.
Dieser Cast funktioniert natürlich nur, wenn RTTI unterstützt wird, und nur, wenn die Basisklasse eine vtable (d.h., eine oder mehrere virtuelle Metoden) hat!.
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:
- Kurz gesagt: auch Experten tun es nicht.
Etwas länger gesagt: - Manche Compiler erzeugen sehr langsamen Code, wenn sie solche Exception-Spezifikationen sehen;
- Wenn dann doch eine Exception kommt, die nicht in der Spezifikation steht, wird unexpected() aufgerufen -- das ist oft nicht das, was man will;
- Für Methoden in Templates kann man sowieso keine sinnvolle Exception-Spezifikation schreiben, da man nichts über den zukünftigen Basistyp weiß, mit dem das Template instanziiert wird.
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!
- Konstruktoren und Destruktoren
- virtuelle Funktionen
(macht auch keinen Sinn, da diese Funktionen erst zur Laufzeit gebunden werden.) - Funktionen mit variabler Anzahl Parameter (foo( int n, ... )
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 ¶meter!
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).
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(); // verbotenschreibe
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:
-
Nicht terminierte ifs; z.B.:
#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. -
Code-Blöcke muß man klammern!
Wenn folgendes Makro
#define blub( X ) \ z = .... ; \ y = .... ;nicht geklammert wird (mit {}), dann entsteht bei der Verwendung in folgendem Statementif ( ... ) 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.
-
Nicht geklammerte Ausdrücke; z.B.:
#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 * ... );
Annahmen
Verlasse Dich niemals darauf, daß Funktionen sich so verhalten, wie Du denkst, wenn es nicht explizit in der Man-Page steht! Einige Beispiele:
- IDs von Visuals können verschieden sein, auch wenn Du dasselbe Visual angefordert hast!
- memrealloc kann den Speicherblock verschieben, auch wenn Du den Block verkleinerst.
Eingabe-Parameter
Viele Bugs entstehen dadurch, daß Parameter nicht auf Gültigkeit und Plausibilität gecheckt werden7)
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!
- Checke Zeiger auf NULL. Außer, die Funktion wird mehr als 100× pro Frame aufgerufen. (siehe auch Pointer oder Reference)
-
Mit einem einzigen Buchstaben kann man manchmal schon
einen Core-Dump verhindern.
Z.B.:
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. -
Wenn ein Parameter ein enum-Typ ist,
dann soll dieser Typ mit Min-/Max-Werten deklariert werden:
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 mitif ( 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.
- Checke, daß nicht aus Versehen ein Verzeichnis gelesen wird, wo eigentlich ein File gelesen werden sollte. (Siehe stat(2).)
"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):
-
Vergleich auf String-Gleichheit mit
if ( strcmp(s,t) ) - scanf mit called-by-value Parametern (ergibt i.A. einen Core-Dump an dieser Stelle). printf mit falschem Format-String ergibt meistens zufälligen Output oder einen Core-Dump. Moderne Compiler geben, mit den richtigen Compiler-Einstellungen, wenigstens eine Warning aus.
- Die Funktion (oder Makro) abs auf floats oder doubles anwenden gibt Müll -- dafür muß man fabs oder fabsf nehmen!
-
Präzedenz der Operatoren * und ++ falsch gemerkt
6) Merkhilfe: while ( *a++ = *b++ ); kopiert
Strings. (Dafür gibt es natürlich strcpy.)
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 .
- Der Wert von getenv wird direkt verwendet, und nicht auf NULL getestet! (Was ist, wenn die Environment-Variable nicht definiert ist? Was ist, wenn die Variable zwar definiert ist, aber eine leerer String ist?)
-
= statt == in ifs.
Ein beliebter Fehler, der jedem mal passiert, ist
if ( ch = '\r' )was immer 1 liefert! (Gemeint war natürlich ==.)
Man kann den Vergleich umgekehrt schreiben: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 ;-)
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)
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 ...
-
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=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).
-
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.)
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 wesentlich leichter.
Allgemeine Richtlinien
Hier einige grundlegende Richtlinien eines jeden Moduls oder Library:
- 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. - Korrektheit.
- 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! - 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:
- sie soll offen für Erweiterungen sein (durch Ableitung oder Delegation);
- sie geschlossen für Änderungen sein (außer Erweiterungen).
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:
- 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!
- 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?
- Die Zeit könnte statt dessen sinnvoller genutzt werden, das User-Interface zu verbessern, zusätzliche Funktionalität einzubauen, den Code zu optimieren, den Source zu dokumentieren (wir machen an dieser Stelle eine kleine Lachpause), oder noch ein paar Tests auf der Software zu machen.
- "Frischer" (nicht-trivialer) Code hat immer mehr Bugs als "reifer" (oft benutzter, getesteter und debuggter).
- Weil die Zeit drängt (wann tut sie das nicht bei Programmierern?) wird ein sub-optimaler Algorithmus implementiert. Oft reicht der später nicht mehr, wenn die Anforderungen an das Gesamtsystem gestiegen sind.
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:
- stdio-, stdlib-, string-Funktionen, etc. 11)
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.
- Sortieren (qsort), Suchen (bsearch), Bäume (tsearch), Hash-Tables (hsearch)
- Lineare-Algebra-Funktionen für große Systeme (libblas), FFT (libfft), numerische Standardalgorithmen (Numerical Recipes),
- Parsen von command line arguments. (Siehe z.B. ~zach/KnowHow/c/ParseCommandLine.c) Wie oft habt Ihr void main( int argc, char **argv) geschrieben?
- Aus der STL: vector, map, hashmap, algorithm, ...
- Standard Library: numeric_limits,
- SCSL (scientific library)
Die (sogenannten) Gründe dafür, daß fremder Code nicht benutzt wird, sind meistens:
- "Es tut nicht ganz das was ich will": aber oft tut es fast dasselbe, und es wäre leicht, ein paar kleine Wrapper zu schreiben.
- "Eine Library-Funktion aufzurufen kostet zu viel CPU-Zeit": Das hört man nur von Leuten, die ihr Programm nie profiled haben.
- "Ich wußte nicht, daß die Funktion / Library / der Code existiert": Kompetente Programmierer kennen den Inhalt ihrer Toolboxes, ihr Unix, und haben auch schon ab und zu die diversen PD-Repositories gebrowst.
- " Ich hätte es anders gemacht": ist der Triumph persönlichen Geschmacks über professionelles Programmieren.
- Das "Not invented here" Syndrom, das "Not good enough" Syndrom, und das "Not understood here" Syndrom.
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?
- Pipes: eines der revolutionären Konzepte in Unix.
Wenn man ein "Tool" schreibt, das hauptsächlich als Filter dient, dann sollte man es auf jeden Fall so schreiben, daß es auch stdin/stdout lesen/schreiben kann; dann kann man es in einer Pipeline verwenden. - Modifikationen am Source.
Vorteil: größte Flexibilität; man kann Code entfernen, der nicht benötigt wird.
Nachteil: Wartbarkeit ist schwierig. Man muß immer beide Sources warten, Bug-fixes oder Verbesserungen in der einen Variante müssen meistens "von Hand" in der anderen Version nachgezogen werden. Oft wird das nicht gemacht, "weil gerade keine Zeit ist". Folge: die Schere geht immer weiter auf zwischen beiden Varianten, bis es bald unmöglich wird, Änderungen von der einen Variante in die andere einzubauen. - Libraries:
Sind in vieler Hinsicht die beste Art, re-usable code
zu schreiben.
Man muß allerdings sagen, daß es schwer ist, ein sauberes Interface zu designen, und daß es sehr schwierig oder unmöglich werden kann, ein schlechtes Interface später zu verbessern, wenn die Library schon eine gewisse Verbreitung hat. - Templates: Damit sind nicht die C++-Templates gemeint, sondern "Code-Skelette". Z.B. ist das Parsen von Command-line-Argumenten ein häufiger Kandidat für diese Art von code re-use (siehe ~zach/KnowHow/c), aber auch Man-Pages oder "große" Makefiles.
- include: Code re-use per #include ist verboten!
- Aufruf: Manchmal macht man sich das Leben am einfachsten,
wenn man einfach existierende Programme aufruft per
system() oder popen().
Nachteil, der Datenaustausch ist oft etwas mühsam,
und der Aufruf dauert wesentlich länger als ein Library-Call,
da erst ein extra Prozeß und eine Subshell gestartet werden
müssen.
Es ist ein häufiger Anfänger-Fehler, für Kleinigkeiten system() oder popen() zu verwenden, z.B. für ein einfaches "ls" oder "rm".
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:
- Am besten macht man erst gar keine. Ein gutes Hilfsmittel (welches mir schon etliche Stunden Bug-Suche erspart hat) ist die Compiler-Option -Wall (mit gcc/g++). Damit erhält man eine ganze Menge Warnings, die auf potentielle Bugs oder nicht-portablen Code hinweisen. (Natürlich muß man einige überflüssige Warnings mit -woff abschalten.) Der neue Compiler bemerkt sogar den Bug, daß der printf-Format-String nicht zu den Argumenten paßt!
- Falls es ein "sporadischer" Bug ist, versuche eine Situation (eine "Probleminstanz") zu finden, in der er immer (oder möglichst häufig) auftritt. Wenn das nicht klappt hilft nur noch Nachdenken!
- Versuche dann, eine "minimale" Probleminstanz oder Eingabe zu finden, so daß der Bug gerade noch aber trotzdem reproduzierbar auftritt. Im Falle der Computer-Graphik bedeutet "minimale Probleminstanz" meistens: eine kleine Szene, wenige Punkte, kleine Objekte, wenige Objekte, minimale Konfiguration des Programms, etc.
- Versuche genau die Symptome zu bestimmen; oder genau die Situation oder die Reihenfolge von Ereignissen, bei der der Bug auftritt. Ist jedes der Ereignisse notwendig um den Bug hervorzurufen?
- Versuche dann, die Stelle im Ablauf einzukreisen, bei der der Bug auftritt. Das kann eine Funktion sein, oder ein Abschnitt einer Funktion (Befehle stop in function und s bzw. n in dbx).
- Versuche lokale Variablen zu finden, die korrupt sind (Befehl dump in dbx).
-
Versuche per Intuition zu erraten, was für eine Art Bug es ist:
- ein lokaler Bug, der innerhalb einer Funktion oder eines Moduls besteht
- ein Zusammenspiel-Bug, der dadurch entsteht, daß verschiedene Komponenten des Programms/Systems nicht richtig zusammenspielen, obwohl jede für sich das richtige tut
- ein Memory-Bug, der durch Speicher-Korruption entsteht (irgendwelche Arrays sind zu klein oder Maximal-Grenzen werden nicht gecheckt, oder es werden Pointer benutzt, die nicht (mehr) gültig sind)
- Ob ein Pointer gültig ist, kann man oft daran erkennen, ob er auf eine durch 4 (oder sogar 8) teilbare Adresse zeigt (bei Non-chars).
- unvollständige Fallunterscheidung; diese Bugs sind sehr schwer zu finden und auch sehr schwer zu erkennen.
- ein Grafik-Bug
- ein Annahmen-Bug (irgendwo wird eine Annahme über den Zustand irgendwelcher Variablen oder Datenstrukturen gemacht, die verletzt wird --- warum auch immer)
Tools für die Bug-Suche:
-
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:
Am besten kopiert man .dbxinit aus meinem Home in sein eigenes Home.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.
-
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.
-
Compiler: verschiedene Compiler-Optionen (SGI).
- -trapuv sorgt dafür, daß lokale Variablen mit einem Wert initialisiert werden, der garantiert zu einem Core-Dump führt, wenn diese Variablen benutzt werden, bevor sie gesetzt werden.
- -TENV:large_stack hilft manchmal, wenn der Stack korrumpiert wird oder überläuft.
- -fullwarn
erkennt eine ganze Menge schlechten/unschönen Code,
der zwar im Prinzip erlaubt, meistens aber ein Bug oder
schlechter Programmier-Stil ist (z.B. falscher Typ eines
Parameters).
Manche Warnings sind überflüssig, aber man kann sie global mit -woff abschalten.
Wenn eine Warning nur an vereinzelten Stellen keinen Sinn macht, dann sollte man diese nicht generell ausschalten, sondern gezielt an diesen Stellen im Code mit #pragma woff.
Übrigens, die Warning "Parameter not used" (oder so ähnlich) kann man dadurch umgehen, indem man den Namen des Parameters einfach im Prototyp wegläßt. - -DEBUG:trap_uninitialized:div_check=3:subscript_check -DEBUG:varargs_interface_check:varargs_prototypes -DEBUG:verbose_runtime checken verschiedene Runtime-Errors durch zusätzlichen Code.
-
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! - 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):
-
Signale:
psignal,
SYSV Versionen: signal(5), signal(2), kill(2), killpg(3B)
POSIX: sigset, sigvec, sigaction, sigsetopts, siginfo, sigsend, sigqueue, psiginfo, sigsuspend, sigsetjmp - malloc_cv(3): zum Finden von Memory-Corruption. Funktioniert auch mit us*-Funktionen (Purify leider nicht).
- STL.
-
Parallelisierung:
mfork & Co.;
sproc, blockproc, barrier,
prctl, sysmp
usinit, usconfig, usmalloc,
ussetlock, ustestlock, usnewlock,
test_and_set, add_then_test, is_mips2, - stdarg
Als "advanced programmer" sollte man kennen:
- Searching and sorting: tsearch, twalk, bsearch, qsort, lsearch, hsearch
- Serial ports: termio, ioctl, serial, read, write, usio, cserialio
- Matchen von Strings mit regulären Ausdrücken: regexp, regcomp, regerror, regexec
- Funktionen für "globbing", d.h., Filename- Path-, und Word-Expansion, so wie die Shells das machen: gmatch(3), wordexp(3), glob(3), fmatch(3), regexp(5), regexp(3)
- Intro-Man-Pages zu advanced topics: environ(5), exec(2), time(5)
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:
- 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;
- Auf jeder Plattform verfügbar sein. (Denkt daran, daß Ihr höchstwahrscheinlich nicht immer an SGIs arbeiten werdet.)
- 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)
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] tmh@possibility.com: C++ Coding Standard, 1999-05-12, http://www.possibility.com/Tmh/.
[9] David Williams: C++ portability guide, version 0.7, http://www.mozilla.org/hacking/portable-cpp.html
[10] Geotechnical Software Services: C++ Programming Style Guidelines, http://www.geosoft.no/style.html
[11] Scott Meyers: How Non-Member Functions Improve Encapsulation, http://www.cuj.com/archive/1802/feature.html
[12] Peter Schröder: Some Programming Style Suggestions, http://mrl.nyu.edu/~dzorin/intro-graphics/handouts/style.html
Gabriel Zachmann
Last modified: Tue Feb 11 16:44:06 MET 2014

