Copyright IBM Corp. 1987, 2004. Alle Rechte vorbehalten.

Der Begriff "Rational" und Rational-Produkte sind Marken der Rational Software Corporation. Namen anderer Unternehmen und Produkte können Marken der jeweiligen Unternehmen sein und dienen nur als Referenz.

Die englische Originalfassung dieses Dokuments wurde von Luan Doan-Minh, Calypso Software Inc., Vancouver (B. C.), Kanada, für die Rational Software Corp. erstellt.


Inhalt

Einführung

Grundlegende Prinzipien
Voraussetzungen
Klassifizierung der Richtlinien
Wichtigste Richtlinie

Codeorganisation und -stil

Codestruktur
Codestil

Kommentare

Benennung

Allgemeines
Namespaces
Klassen
Funktionen
Objekte und Funktionsparameter
Ausnahmen
Verschiedenes

Deklarationen

Namespaces
Klassen
Funktionen
Typen
Konstanten und Objekte

Ausdrücke und Anweisungen

Ausdrücke
Anweisungen

Spezielle Themen

Speicherverwaltung
Fehlerbehandlung und Ausnahmen

Portierbarkeit

Pfadnamen
Datendarstellung
Typkonvertierungen

Wiederverwendung

Kompilierungsprobleme

Zusammenfassung der Richtlinien

Anforderungen oder Einschränkungen
Empfehlungen
Tipps

Bibliographie


Kapitel 1

Einführung

An großen Softwareprojekten arbeiten generell entsprechend große Entwicklerteams. Für den von großen Teams erzeugten Code muss es einen Projektqualitätsmaßstab geben, der auf einem Standard basiert. Die Erstellung eines Programmierungsstandards oder einer Reihe von Richtlinien ist für große Projektteams daher sehr wichtig.

Ein Programmierungsstandard ist aber auch aus folgenden Gründen sinnvoll:

Dieses Dokument stellt Regeln, Richtlinien und Hinweise für die Programmierung mit C++ vor, die als Basis für einen Standard verwendet werden können (und nachfolgend allgemein als Richtlinien bezeichnet werden). Sie sind für Softwareentwickler bestimmt, die in großen Projektteams arbeiten.

Die aktuelle Version dieses Dokuments beschäftigt sich vorrangig mit der Programmierung (obwohl es nicht immer einfach ist, strikt zwischen Programmierung und Design zu unterscheiden). Designrichtlinien werden zu einem späteren Zeitpunkt hinzugefügt.

Die hier vorgestellten Richtlinien gelten für folgende Aspekte der Entwicklung mit C++:

Die Richtlinien wurden aus einem reichen branchenspezifischen Erfahrungsschatz zusammengestellt (im Literaturverzeichnis finden Sie die entsprechenden Quellen, Autoren und Referenzen):

Die meisten Richtlinien basieren zu einem geringen Teil auf anerkannten Softwareprinzipien und größtenteils auf bewährten Softwareverfahren und Praxiserfahrungen. Es gibt auch einige wenige Richtlinien, die auf subjektiven Meinungen basieren, da das Programmieren eine sehr subjektive Tätigkeit ist. Es gibt keinen allgemein anerkannten richtigen oder besten Weg, Code zu schreiben.

Grundlegende Prinzipien

Die meisten Regeln und Richtlinien haben klaren und verständlichen C++-Quellcode zum Ziel, der einen wichtigen Beitrag zur Zuverlässigkeit und Wartungsfreundlichkeit von Software leistet. Was mit klarem und verständlichem Code gemeint ist, geht aus den drei folgenden grundlegenden Prinzipien hervor [Kruchten 94].

Möglichst wenig Überraschungen - Quellcode wird während seiner Lebensdauer häufiger gelesen als geschrieben. Dies gilt insbesondere für Spezifikationen. Code lässt sich im Idealfall wie eine englische Beschreibung der erforderlichen Schritte lesen, hat jedoch den Vorteil, dass er diese Schritte auch gleich ausführt. Programme werden eher für Menschen als für Computer geschrieben. Das Lesen von Code ist ein komplexer mentaler Prozess, der durch Uniformität unterstützt werden kann. Dieses Prinzip der Uniformität sorgt für möglichst wenig Überraschungen. Ein einheitlicher Stil für ein Projekt ist ein wichtiger Grund, aus dem sich ein Team von Softwareentwicklern auf Programmierungsstandards einigt. Solche Standards sind nicht als Strafe oder als Kreativitäts- und Produktivitätshemmnis zu verstehen.

Zentrale Wartung - Eine Designentscheidung sollte nach Möglichkeit nur an einem Punkt der Quelle ausgedrückt werden. Die Konsequenzen dieser Entscheidung sollten größtenteils programmatisch von diesem Punkt abgeleitet werden. Eine Verletzung dieses Prinzips kann sich sehr nachteilig auf die Wartungsfreundlichkeit und Zuverlässigkeit, aber auch auf die Verständlichkeit des Codes auswirken.

Möglichst wenig Schnickschnack - Zur Verbesserung der Lesbarkeit gilt schließlich noch das Prinzip, möglichst wenig Schnickschnack aufzunehmen. Dieses Prinzip soll helfen, eine Überhäufung des Quellcodes mit visuellem Schnickschnack zu vermeiden. Dazu gehören Balken, Fenster und Texte mit geringem Informationsgehalt oder mit Informationen, die nicht zum Verständnis des Verwendungszwecks der Software beitragen.

Die in diesem Dokument enthaltenen Richtlinien sind nicht als strenge Restriktionen gedacht, sondern sollen als Anleitung für die richtige und sichere Verwendung von Sprachfeatures verstanden werden. Der Schlüssel zu guter Software liegt hier in folgenden Faktoren:

Voraussetzungen

Die hier vorgestellten Richtlinien gehen von einigen wenigen Grundvoraussetzungen aus:

Klassifizierung der Richtlinien

Die Richtlinien sind nicht alle gleich wichtig, sondern können auf der folgenden Skala eingestuft werden:

Tipp: Tippsymbol

Eine mit dem obigen Symbol gekennzeichnete Richtlinie ist ein Tipp oder ein Ratschlag, den Sie nach Belieben annehmen oder ausschlagen können.

Empfehlung: Handsymbol OK

Eine mit dem obigen Symbol gekennzeichnete Richtlinie ist eine Empfehlung, für die es eher technische Gründe gibt. Sie kann sich auf die Portierbarkeit oder Wiederverwendbarkeit, in einigen Implementierungen auch auf die Leistung auswirken. Empfehlungen sollten nur aus gutem Grund außer Acht gelassen werden.

Anforderung oder Einschränkung: Zeigefingersymbol

Eine mit dem obigen Symbol gekennzeichnete Richtlinie ist eine Anforderung oder eine Einschränkung, deren Nichtbeachtung definitiv zu fehlerhaftem, unzuverlässigem oder nicht portierbarem Code führt.

Wichtigste Richtlinie

Zeigefingersymbol Lassen Sie sich vom gesunden Menschenverstand leiten.

Wenn Sie keine anwendbare Regel oder Richtlinie finden können oder eine Regel offensichtlich nicht zutrifft, halten Sie sich an die grundlegenden Prinzipien und bauen Sie auf den gesunden Menschenverstand. Diese Regel gilt vor allen anderen. Gesunder Menschenverstand ist auch bei vorhandenen Regeln und Richtlinien unabdingbar.


Kapitel 2

Codeorganisation und -stil

Dieses Kapitel enthält eine Anleitung zur Programmstruktur und zum Layout.

Codestruktur

Für ein großes System werden normalerweise mehrere kleinere funktionale Subsysteme entwickelt. Diese Subsysteme setzen sich wiederum aus einer Reihe von Codemodulen zusammen. In C++ enthält ein Modul in der Regel die Implementierung für eine Abstraktion, in seltenen Fällen auch für eine Gruppe eng zusammengehöriger Abstraktionen. Eine Abstraktion wird in C++ üblicherweise als eine Klasse implementiert. Eine Klasse besteht aus zwei verschiedenen Komponenten, einer Schnittstelle, die für Klassenclients sichtbar ist und eine Deklaration oder Spezifikation der Leistungsmerkmale und Zuständigkeiten der Klasse enthält, und einer Implementierung der deklarierten Spezifikation (der Klassendefinition).

Ein Modul hat ähnlich wie die Klasse eine Schnittstelle und eine Implementierung. Die Modulschnittstelle enthält die Spezifikationen für die Modulabstraktionen (Klassendeklarationen). Die Modulimplementierung ist die Implementierung der Abstraktionen (Klassendefinitionen).

Subsysteme können beim Aufbau des Systems auch zu kooperierenden Gruppen oder Ebenen zusammengefasst werden, um die Abhängigkeiten zu minimieren und besser steuern zu können.

Handsymbol OK Modulspezifikationen und -implementierungen in separate Dateien stellen

Die Spezifikation eines Moduls sollte nicht in derselben Datei wie die Implementierung enthalten sein. Die Spezifikationsdatei wird auch als Header bezeichnet. Die Implementierung eines Moduls können Sie in mehreren Implementierungsdateien speichern.

Wenn eine Modulimplementierung sehr viele Inline-Funktionen (allgemeine implementierungsinterne Deklarationen, Testcode oder plattformspezifischen Code) enthält, sollten Sie diese Abschnitte in gesonderten Dateien speichern und nach ihrem Inhalt benennen.

Falls die Größe der ausführbaren Programme ein Problem darstellen könnte, sollten Sie selten verwendete Funktionen ebenfalls in gesonderten Dateien speichern.

Den Namen einer Abschnittsdatei können Sie wie folgt konstruieren:

Handsymbol OK Gruppe von Dateinamenerweiterungen auswählen, um Header von Implementierungsdateien unterscheiden zu können

Häufig verwendete Dateinamenerweiterungen für Header-Dateien sind .h, .H, .hh, .hpp und .hxx und für Implementierungsdateien .c, .C, .cc, .cpp, und .cxx. Wählen Sie die gewünschten Erweiterungen aus und verwenden Sie sie konsistent.

Beispiel
SymaNetwork.hh  // Die Erweiterung ".hh" bezeichnet einen
                // Header des Moduls "SymaNetwork".
SymaNetwork.cc  // Die Erweiterung ".cc" bezeichnet eine
                // Implementierung des Moduls "SymaNetwork". 
Anmerkungen

Im Arbeitspapier zum C++-Normentwurf ist für Header, die in einem Namespace gekapselt sind, außerdem die Erweiterung ".ns" angegeben.

Handsymbol OK Nach Möglichkeit nur eine Klasse pro Modulspezifikation definieren

Nur in seltenen Ausnahmefällen sollte es mehrere Klassen in einem Modul geben, und dies auch nur dann, wenn die Klassen eng miteinander verknüpft sind (z. B. ein Container und sein Iterator). Es ist annehmbar, die Klasse main eines Moduls und deren unterstützende Klassen in dieselbe Header-Datei aufzunehmen, sofern alle Klassen immer für ein Clientmodul sichtbar sein müssen.

Begründung

Die Schnittstelle eines Moduls wird verkleinert und die Abhängigkeiten von dieser Schnittstelle werden reduziert.

Handsymbol OK Möglichst keine implementierungsinternen Deklarationen in Modulspezifikationen aufnehmen

Neben klasseninternen Elementen sollten die implementierungsinternen Deklarationen eines Moduls (Implementierungstypen und Unterstützungsklassen) nicht in der Spezifikation des Moduls erscheinen. Diese Deklarationen sollten in die erforderlichen Implementierungsdateien gestellt werden, sofern sie nicht von mehreren Implementierungsdateien benötigt werden. Im letztgenannten Fall sollten sie in eine zweite private Header-Datei gestellt werden. Die sekundäre private Header-Datei sollte dann bei Bedarf in andere Implementierungsdateien eingeschlossen werden.

Durch diese Vorgehensweise wird Folgendes sichergestellt:

Beispiel
// Die Spezifikation für das Modul foo ist in der Datei "foo.hh" enthalten.
//
class foo
{
.. Deklarationen
};
// Ende von "foo.hh"
// Die privaten Deklarationen für das Modul foo sind in der Datei
// "foo.private.hh" enthalten und werden von allen foo-Implementierungsdateien verwendet.
... private Deklarationen
// Ende von "foo.private.hh"
// Die Implementierung des Moduls foo ist in mehreren Dateien enthalten,
// "foo.x.cc" und "foo.y.cc"
// Datei "foo.x.cc"
//
#include "foo.hh" // Eigenen Header des Moduls einschließen
#include "foo.private.hh" // Für die Implementierung
// erforderliche Deklarationen einschließen
... Definitionen
// Ende von "foo.x.cc"
// Datei "foo.y.cc"
//
#include "foo.hh"
#include "foo.private.hh"
... Definitionen
// Ende von "foo.y.cc"            

Zeigefingersymbol Immer mit #include auf eine Modulspezifikation zugreifen

Ein Modul, das ein anderes Modul benutzt, muss die Vorprozessoranweisung #include verwenden, um die Spezifikation des Lieferantenmoduls sichtbar zu machen. Module sollten dementsprechend nie einen Abschnitt aus der Spezifikation eines Lieferantenmoduls nochmals deklarieren.

Wenn Sie Dateien einschließen, verwenden Sie die Syntax #include <Header> nur für Standard-Header und für die übrigen Header die Syntax #include "Header".

Die Anweisung #include muss auch für die eigenen Implementierungsdateien des Moduls verwenden werden. Eine Modulimplementierung muss zunächst ihre eigene Spezifikation und privaten sekundären Header einschließen (siehe Abschnitt "Modulspezifikationen und -implementierungen in separate Dateien stellen").

Beispiel
// Spezifikation des Moduls foo in der Header-Datei
// "foo.hh"
//
class foo
{
... Deklarationen
};
// Ende von "foo.hh"

// Implementierung des Moduls foo in der Datei "foo.cc"
//
#include "foo.hh" // Die Implementierung enthält ihre
// eigene Spezifikation
... Definitionen für Elemente von foo
// Ende von "foo.cc"            

Eine Ausnahme von der Verwendungsregel für #include bildet ein Modul, das nur Typen (Klassen) eines Lieferantenmoduls verwendet oder (mit Zeiger und Referenzdeklarationen) referenziert. In diesem Fall wird die Referenzierung bzw. der Einschluss mit einer Forward-Deklaration angegeben (siehe auch Abschnitt "Kompilierungsabhängigkeiten minimieren").

Fügen Sie nicht mehr ein, als absolut notwendig ist. Modul-Header sollten keine anderen Header einschließen, die nur für die Modulimplementierung erforderlich sind.

Beispiel
#include "a_supplier.hh"

class needed_only_by_reference;// Für eine Klasse wird eine Forward-
                               // Deklaration verwendet, wenn nur
                               // über einen Zeiger oder Verweis
                               // auf sie zugegriffen werden muss.
void operation_requiring_object(a_supplier required_supplier, ...);
//
// Operation, die ein tatsächliches Supplier-Objekt erfordert, so dass
// die Supplier-Spezifikation mit #include eingeschlossen werden muss

void some_operation(needed_only_by_reference& a_reference, ...);
//
// Eine Operation, die nur einen Verweis auf ein Objekt erfordert, so dass
// für den Supplier eine Forward-Deklaration verwendet werden sollte 
Begründung

Durch diese Regel wird Folgendes sichergestellt:

Handsymbol OK Definitionen von Inline-Funktionen eines Moduls in eine gesonderte Datei stellen

Wenn ein Modul viele Inline-Funktionen hat, sollten die Definitionen dieser Funktionen in eine separate Datei gestellt werden. Die Datei mit den Inline-Funktionen sollte am Ende der Header-Datei des Moduls eingeschlossen werden.

Lesen Sie hierzu auch den Abschnitt "Inline-Kompilierung mit dem Symbol No_Inline für bedingte Kompilierung unterbinden".

Begründung

Dieses Verfahren verhindert eine Überfrachtung des Headers mit Implementierungsdetails und sorgt für eine saubere Spezifikation. Die Vervielfältigung von Code kann reduziert werden, wenn Inline-Funktionen nicht kompiliert werden. Mit einer bedingten Kompilierung können Sie Inline-Funktionen zu einer einzelnen Objektdatei kompilieren (und müssen sie nicht wie wie bei der statischen Kompilierung in jedem Modul kompilieren, das die Funktionen verwendet). Inline-Funktionen sollten dementsprechend nicht in Klassendefinitionen definiert werden, solange sie nicht trivial sind.

Handsymbol OK Große Module bei problematischer Programmgröße in mehrere Umsetzungseinheiten unterteilen

Unterteilen Sie große Module in mehrere Umsetzungseinheiten, um beim Verlinken des Programms die Entfernung von Code, der nicht referenziert wird, zu erleichtern. Elementfunktionen, die nur selten referenziert werden, sollten aus Dateien, die allgemein verwendet werden, herausgehalten werden. Im Extremfall können einzelne Elementfunktionen auch in jeweils eine eigene Datei gestellt werden [Ellemtel 1993].

Begründung

Nicht alle Linker sind in der Lage, nicht referenzierten Code aus einer Objektdatei zu entfernen. Wenn große Module auf mehrere Dateien verteilt werden, können diese Linker die Größe ausführbarer Programme reduzieren, indem Sie die Verknüpfungen ganzer Objektdateien löschen [Ellemtel 1993].

Anmerkungen

Es könnte lohnend sein, zuerst darüber nachzudenken, ob das Modul nicht in kleinere Abstraktionen unterteilt werden sollte.

Handsymbol OK Plattformabhängigkeiten eingrenzen

Trennen Sie plattformabhängigen Code von plattformunabhängigem Code, um die Portierung zu erleichtern. Die Namen plattformabhängiger Module sollten mit dem Namen der Plattform qualifiziert werden, um die Abhängigkeit hervorzuheben.

Beispiel
SymaLowLevelStuff.hh         // Spezifikation
// "LowLevelStuff"
SymaLowLevelStuff.SunOS54.cc // SunOS-5.4-Implementierung
SymaLowLevelStuff.HPUX.cc    // HP-UX-Implementierung
SymaLowLevelStuff.AIX.cc     // AIX-Implementierung
Anmerkungen

Im Hinblick auf Architektur und Wartung sollten Plattformabhängigkeiten auf eine kleine Anzahl von untergeordneten Subsystemen beschränkt bleiben.

Wählen Sie eine Standardstruktur für Dateiinhalte aus und wenden Sie diese konsistent an.

Vorgeschlagen wird eine Dateiinhaltsstruktur, die die folgenden Abschnitte in der hier angegebenen Reihenfolge umfasst:

Begründung

Der obige Dateiinhalt ist so sortiert, dass die für den Client relevanten Informationen zuerst erscheinen. Dies ist konsistent zur Begründung für die Reihenfolge der öffentlichen, geschützten und privaten Abschnitte einer Klasse.

Anmerkungen

Möglicherweise muss der Copyrightvermerk am Anfang der Datei stehen, wenn es eine entsprechende unternehmensinterne Richtlinie gibt.

Handsymbol OK Schutz vor wiederholtem Einschließen von Dateien

Sie sollten vermeiden, dass Dateien wiederholt eingeschlossen und kompiliert werden. Verwenden Sie dazu in jeder Header-Datei das folgende Konstrukt:

#if !defined(module_name) // Vorprozessorsymbole zum
#define module_name       // Schutz vor wiederholtem Einschluss
                          // verwenden...  // Hier folgen Deklarationen.
#include "module_name.inlines.cc" // Hier folgt der optionale
                                  // Einschluss von Inlines.
// Keine weiteren Deklarationen nach Einschluss der Inline-Funktionen
// des Moduls.
#endif // Ende von module_name.hh            

Verwenden Sie den Moduldateinamen für das Symbol zum Schutz vor wiederholtem Einschluss. Achten Sie darauf, für das Symbol dieselbe Groß-/Kleinschreibung wie für den Modulnamen zu verwenden.

Handsymbol OK Inline-Kompilierung mit dem Symbol "No_Inline" für bedingte Kompilierung unterbinden

Verwenden Sie das folgende Konstrukt für bedingte Kompilierung, um die Inline- bzw. Out-of-line-Kompilierung der Inline-fähigen Funktionen zu steuern:

// Am Anfang von module_name.inlines.hh
#if !defined(module_name_inlines)
#define module_name_inlines

#if defined(No_Inline)
#define inline // Schlüsselwort inline ungültig machen
#endif

... // Platz für Inline-Definitionen
#endif // Ende von module_name.inlines.hh

// Am Ende von module_name.hh
//
#if !defined(No_Inline)
#include "module_name.inlines.hh"
#endif

// Am Anfang von module_name.cc, nach Einschluss von
// module_name.hh
//
#if defined(No_Inline)
#include "module_name.inlines.hh"
#endif            

Das Konstrukt für bedingte Kompilierung ist mit dem Konstrukt für den Schutz vor mehrfachem Einschluss vergleichbar. Wenn das Symbol No_Inline nicht definiert ist, werden die Inline-Funktionen mit der Modulspezifikation kompiliert und automatisch von der Modulimplementierung ausgeschlossen. Ist das Symbol No_Inline definiert, werden die Inline-Definitionen von der Modulspezifikation ausgeschlossen, jedoch mit dem ungültig gemachten Schlüsselwort inline in die Modulimplementierung eingeschlossen.

Begründung

Das obige Verfahren ermöglicht eine Reduzierung der Codevervielfältigung, wenn Inline-Funktionen out-of-line kompiliert werden. Bei einer bedingten Kompilierung wird eine Kopie der Inline-Funktionen im definierenden Modul kompiliert. Im Gegensatz dazu wird der Code bei der Kompilierung statischer Funktionen (interne Verknüpfung) in jedem Modul repliziert, das die Funktionen verwendet, wenn ein Compiler-Schalter die Out-of-line-Kompilierung angibt.

Anmerkungen

Die bedingte Kompilierung erhöht die Komplexität bei der Verwaltung der Build-Abhängigkeiten. Sie können diese Komplexität jedoch überschaubar halten, indem Sie Header und Definitionen von Inline-Funktionen immer wie eine logische Einheit behandeln, so dass die Implementierungsdateien sowohl von der Header-Datei als auch von der Definitionsdatei für die Inline-Funktionen abhängen.

Codestil

Handsymbol OK Für verschachtelte Anweisungen Stil mit kleiner konsistenter Einrückung verwenden

Verschachtelte Anweisungen sollten durch konsistente Einrückung visuell abgehoben werden. Zwei bis vier Leerzeichen haben sich als effizient für diesen Zweck erwiesen. Wir empfehlen eine gleichmäßige Einrückung von zwei Leerzeichen.

Die Begrenzer für Verbund- oder Blockanweisungen ({}) sollten genauso weit wie die umgebenden Anweisungen eingerückt sein (so dass die geschweiften Klammern {} vertikal ausgerichtet sind). Anweisungen innerhalb des Blocks sollten um die gewählte Anzahl Leerzeichen eingerückt werden.

Die case-Kennsätze einer Anweisung switch sollten dieselbe Einrückungsstufe wie die Anweisung switch haben. Anweisungen innerhalb der Anweisung switch können dann von der Anweisung switch und den case-Kennsätzen aus um eine Stufe weiter eingerückt werden.

Beispiel
if (true)
{  		// Neuer Block
foo(); // Anweisungen innerhalb des Blocks
       // um zwei Leerzeichen eingerückt
}
else
{
bar();
}
while (expression)
{
statement();
}
switch (i)
{
case 1:
do_something();// Anweisungen um eine Stufe
               // von der Anweisung switch
break;         // aus eingerückt
case 2:
  //...
default:
  //...
}            
Begründung

Die Einrückung um zwei Leerzeichen ist ein Kompromiss, der eine leichte Erkennung der Blöcke ermöglicht, aber auch genug verschachtelte Blöcke zulässt, ohne dass der Code zu weit über den rechten Rand der Bildschirmanzeige oder der gedruckten Seite hinausragt.

Handsymbol OK Funktionsparameter unter dem Funktionsnamen oder Namen des Geltungsbereichs einrücken

Falls eine Funktionsdeklaration nicht in eine Zeile passt, platzieren Sie den ersten Parameter in der Zeile mit dem Funktionsnamen. Stellen Sie alle folgenden Parameter in jeweils eine neue Zeile mit derselben Einrückung wie für den ersten Parameter. Bei dem nachfolgend gezeigten Deklarations- und Einrückungsstil sind Leerräume unter dem Rückgabetyp und Namen der Funktion gelassen, damit sie besser erkennbar sind.

Beispiel
void foo::function_decl( some_type first_parameter, 
                                     some_other_type second_parameter,
                                     status_type and_subsequent);                                          

Falls die Anwendung der obigen Richtlinie dazu führt, dass eine Zeile umgebrochen wird oder Parameter zu weit eingerückt sind, können Sie alle Parameter unter dem Funktionsnamen oder dem Namen des Geltungsbereichs (Klasse, Namespace) in jeweils einer gesonderten Zeile mit derselben Einrückung angeben.

Beispiel
void foo::function_with_a_long_name( // Der Funktionsname ist weniger gut zu erkennen.
                                     some_type first_parameter, 
                                     some_other_type second_parameter,
                                     status_type and_subsequent);                   

Lesen Sie auch die folgenden Ausrichtungsregeln.

Handsymbol OK Maximale Zeilenlänge an das Standardpapierformat für Druckausgabe anpassen

Die Länge der Programmzeilen sollte begrenzt sein, um den Verlust von Informationen beim Drucken auf Standardpapier (DIN A4 oder dem für Druckausgaben festgelegten Papierformat zu vermeiden.

Anmerkungen

Falls die Einrückungsstufe dazu führt, dass tief verschachtelte Anweisungen weit über den rechten Rand hinausragen, sollte der Code in kleinere oder einfacher zu verwaltende Funktionen gegliedert werden.

Handsymbol OK Konsistentes Line-Folding anwenden

Wenn Parameterlisten in Funktionsdeklarationen, Definitionen und Aufrufen oder Aufzählungsausdrücke in Aufzählungsdeklarationen nicht in eine Zeile passen, brechen Sie die Zeile nach den einzelnen Listenelementen um, so dass jedes Element in einer gesonderten Zeile steht (siehe auch Abschnitt "Funktionsparameter unter dem Funktionsnamen oder Namen des Geltungsbereichs einrücken").

Beispiel
enum color { red, 
             orange, 
             yellow, 
             green, 
             //...
             violet
                   };            

Falls die Deklaration einer Klassen- oder Funktionsschablone übermäßig lang ist, brechen Sie sie nach der Liste der Schablonenargumente auf mehrere Zeilen um. Beispieldeklaration aus der Standard Iterators Library [X3J16 95]):

template <class InputIterator, class Distance>
void advance(InputIterator& i, Distance n);            


Kapitel 3

Kommentare

Dieses Kapitel enthält Anleitungen für die Verwendung von Kommentaren im Code.

Kommentare sollten den Quellcode ergänzen und nicht umschreiben:

  • Kommentare sollten eine Ergänzung zum Quellcode sein und das erklären, was nicht offensichtlich ist. Eine Wiederholung von Sprachsyntax oder -semantik sollte vermieden werden.
  • Kommentare können dem Leser etwas über die zugrunde liegenden Konzepte und die Abhängigkeiten vermitteln und ihm helfen, besonders komplexe Datencodierungen oder Algorithmen zu verstehen.
  • Kommentare sollten auf Abweichungen von Codierungs- oder Designstandards, auf die Verwendung eingeschränkter Features und auf spezielle "Tricks" hinweisen.
  • Der Programmierer sollte zu jedem Kommentar ohne Umschweife angeben können, welchen Nutzen er bringt. Allgemein gilt, dass richtig ausgewählte Namen Kommentare oft überflüssig machen. Kommentare werden, solange sie nicht Teil formaler PDL (Programm Design Language) sind, nicht vom Compiler überprüft. Deshalb sollten Designentscheidungen gemäß dem Prinzip der zentralen Wartung im Quellcode und nicht in Kommentaren ausgedrückt werden, auch wenn dafür etwas mehr Deklarationen erforderlich sind.

    Handsymbol OK Kommentare im C++-Stil angeben

    Verwenden Sie anstelle des C-Begrenzers für Kommentare "/*...*/" den Begrenzer von C++ "//".

    Begründung

    Kommentare im C++-Stil sind besser zu erkennen und verringern die Gefahr, dass versehentlich große Teile des Codes auf Kommentar gesetzt werden, weil ein Begrenzungszeichen für das Kommentarende fehlt.

    Zählerbeispiel
    /* Beginn des Kommentars mit fehlendem Begrenzungszeichen für Kommentarende
    do_something();
    do_something_else(); /* Kommentar zu do_something_else */
                                  // Hier ist das Ende des Kommentars.---> */
                                  // Sowohl do_something als auch
                                  // do_something_else wurde
                                  // versehentlich auf Kommentar gesetzt.
    Do_further();      

    Handsymbol OK Kommentare sehr nah am Quellcode platzieren

    Kommentare sollten in der Nähe des Codes stehen, auf den sie sich beziehen, etwas eingerückt sein und mit einer leeren Kommentarzeile vom Code abgehoben werden.

    Kommentare, die für mehrere aufeinander folgende Quellenanweisungen gelten, sollten wie eine Einleitung über den Anweisungen stehen. Die Kommentare zu einzelnen Anweisungen sollten dementsprechend unterhalb der jeweiligen Anweisung stehen.

    Beispiel
    // Kommentar mit Vorbemerkungen, der für
    // mehrere nachfolgende Anweisungen gilt
    // 
    ...
    void function();
    //
    // Ein Kommentar unter der Anweisung, der sich
    // auf die vorherige Anweisung bezieht. 

    Handsymbol OK Kommentare am Zeilenende vermeiden

    Schreiben Sie Kommentare nicht in dieselbe Zeile wie ein Quellenkonstrukt, da die Ausrichtung solcher Kommentare oft verloren geht. Bei langen Deklarationen, z. B. für Aufzählungsausdrücke in einer Aufzählungsdeklaration, können solche Kommentare jedoch in den Beschreibungen der einzelnen Elemente toleriert werden.

    Handsymbol OK Kommentare mit Kopfdaten vermeiden

    Vermeiden Sie Kopfzeilen mit Informationen wie Telefonnummern oder Erstellungs- und Änderungsdatum. Telefonnummern sind schnell veraltet, und das Erstellungs- oder Änderungsdatum bzw. die Gründe für die Änderung werden am besten mit einem Tool für Konfigurationsmanagement (oder einer Versionsprotokolldatei) verwaltet.

    Vermeiden Sie vertikale Balken, geschlossene Rahmen oder Kästchen auch bei großen Konstrukten (wie Funktionen und Klassen), denn sie können nur mit Mühe in Form gehalten werden und sind im Grunde nichts als optischer Schnickschnack. Zusammengehörige Quellcodeblöcke sollten durch leere Kommentarzeilen und nicht durch Kommentarzeilen mit gestrichelten Linien oder doppelten Linien voneinander abgesetzt werden. Heben Sie Konstrukte innerhalb von Funktionen oder Klassen durch eine Leerzeile ab und verwenden Sie zwei Leerzeilen, um die einzelnen Funktionen voneinander abzugrenzen.

    Rahmen oder Masken geben dem Code den Anschein von Uniformität und erinnern den Programmierer daran, den Code zu dokumentieren. Häufig fördern Sie jedoch einen umschreibenden Stil [Kruchten 94].

    Handsymbol OK Kommentarabsätze durch eine leere Kommentarzeile trennen

    Trennen Sie die einzelnen Absätze eines Kommentarblocks nicht durch Leerzeilen, sondern durch leere Kommentarzeilen.

    Beispiel
    // Eine Erläuterung muss in noch einem
    // Absatz weiter ausgeführt werden.
    //
    // Die leere Kommentarzeile an dieser
    // Stelle macht deutlich, dass dieser
    // Absatz zum selben Kommentarblock gehört. 

    Handsymbol OK Redundanz vermeiden

    Vermeiden Sie die Wiederholung von Programmbezeichnern in Kommentaren und die Replikation bereits an anderer Stelle vorhandener Informationen. Geben Sie stattdessen einen Zeiger auf die Informationen an. Eine Änderung des Programms würde andernfalls eine Wartung an vielen Stellen erfordern. Wird vergessen, die erforderlichen Kommentaränderungen an allen Stellen vorzunehmen, sind irreführende oder falsche Kommentare die Folge, die letztlich schlechter als gar kein Kommentar sind.

    Handsymbol OK Selbstdokumentierender Code ist Kommentaren vorzuziehen

    Sie sollten immer bestrebt sein, Code zu schreiben, der sich selbst dokumentiert, und nach Möglichkeit auf Kommentare verzichten. Dies können Sie durch die Wahl besserer Namen, die Verwendung zusätzlicher temporärer Variablen oder eine Restrukturierung des Codes erreichen. Achten Sie in Kommentaren auf Stil, Syntax und Rechtschreibung. Fassen Sie Kommentare in natürlicher Sprache ab und vermeiden Sie einen kryptischen oder Telegrammstil.

    Beispiel
    Dieser Code sollte ersetzt werden:
    do
    {
      ...
    } while (string_utility.locate(ch, str) != 0); 
    // Suchschleife nach erfolgreicher Suche verlassen
    
    Verwenden Sie stattdessen diesen Code:
    do
    {
      ...
    found_it = (string_utility.locate(ch, str) == 0);
    } while (!found_it);      

    Handsymbol OK Klassen und Funktionen dokumentieren

    Selbstdokumentierender Code ist zwar Kommentaren vorzuziehen, aber dennoch müssen Informationen bereitgestellt werden, die über eine Erklärung für komplizierte Codeabschnitte hinausgehen. Folgende Informationen müssen als Mindestdokumentation vorhanden sein:

    Begründung

    Der Auftraggeber sollte anhand der Codedokumentation zusammen mit den Deklarationen in der Lage sein, den Code zu verwenden. Eine Dokumentation ist erforderlich, weil die vollständige Semantik von Klassen, Funktionen, Typen und Objekten nicht ausschließlich in C++ ausgedrückt werden kann.


    Kapitel 4

    Benennung

    Dieses Kapitel enthält Anleitungen für die Auswahl von Namen für verschiedene C++-Entitäten.

    Allgemeines

    Die Auswahl guter Namen für Programmentitäten (Klassen, Funktionen, Typen, Objekte, Literale, Ausnahmen, Namespaces) ist keine einfache Sache. Bei mittleren bis großen Anwendungen ist die Herausforderung durch Namenskonflikte oder das Fehlen von Synonymen zur Bezeichnung vergleichbarer, aber dennoch unterschiedlicher Konzepte noch größer.

    Die Verwendung einer Namenskonvention kann den Aufwand für die Findung geeigneter Namen verringern. Darüber hinaus unterstützt eine Namenskonvention die Konsistenz im Code. Eine sinnvolle Namenskonvention sollte Anleitungen zum typografischen Stil (Art und Weise der Schreibung von Namen) und zur Konstruktion (oder Auswahl) von Namen enthalten.

    Handsymbol OK Namenskonvention auswählen und konsistent anwenden

    Welche Namenskonvention Sie verwenden, ist nicht so wichtig. Entscheidend ist ihre konsistente Anwendung. Die Uniformität der Benennung ist weit wichtiger, denn Sie sorgt für möglichst wenig Überraschungen.

    Eine absolut konsistente Benennung wird nur selten zu erreichen sein, weil C++ eine Sprache ist, bei der die Groß-/Kleinschreibung beachtet werden muss, und in der C++-Community mehrere verschiedene Namenskonventionen verbreitet sind. Wir empfehlen, die Namenskonvention für das Projekt ausgehend von der Hostumgebung (z. B. UNIX oder Windows) und den vom Projekt verwendeten grundsätzlichen Bibliotheken zu wählen, um eine maximale Konsistenz des Codes zu erzielen.

    Anmerkungen

    Der aufmerksame Leser wird bemerkt haben, dass bei den Beispielen in diesem Text nicht alle Richtlinien befolgt wurden. Dies liegt zum Teil daran, dass die Beispiele aus verschiedenen Quellen stammen. Die Formatierungsrichtlinien wurden aber auch aus Gründen der Platzersparnis nicht peinlich genau beachtet. Die Botschaft dieser Richtlinien liegt jedoch im Text und nicht in den Beispielen.

    Zeigefingersymbol Keine Namen deklarieren, die mit Unterstreichungszeichen ('_') beginnen

    Namen mit einem führenden Unterstreichungszeichen ('_') werden oft von Bibliotheksfunktionen ("_main" und "_exit") verwendet. Namen mit zwei führenden Unterstreichungszeichen ("__") oder einem führenden Unterstreichungszeichen, gefolgt von einem Großbuchstaben, sind für die compilerinterne Verwendung reserviert.

    Vermeiden Sie auch Namen mit mehreren aufeinander folgenden Unterstreichungszeichen, weil häufig die genaue Anzahl der Unterstreichungszeichen schlecht zu erkennen ist.

    Handsymbol OK Typennamen vermeiden, die sich nur durch die Groß-/Kleinschreibung unterscheiden

    Es ist schwierig, sich die Namen verschiedener Typen zu merken, wenn sich diese nur durch die Groß-/Kleinschreibung von Buchstaben unterscheiden. Solche Namen können leicht verwechselt werden.

    Handsymbol OK Abkürzungen vermeiden

    Abkürzungen können verwendet werden, wenn Sie in der Anwendungsdomäne gebräuchlich sind (z. B. SFT für Schnelle Fourier-Transformation) oder in einem für das Projekt vereinbarten Abkürzungsverzeichnis enthalten sind. Andernfalls werden sehr wahrscheinlich an der einen oder anderen Stelle ähnliche, aber nicht ganz identische Abkürzungen auftauchen, die für Unklarheiten sorgen und zu Fehlern führen können (z. B. die Abkürzungen trid, trck_id, tr_iden, tid, tr_ident usw. für track_identification).

    Handsymbol OK Suffixe für Sprachkonstrukte vermeiden

    Die Verwendung von Suffixen zur Kategorisierung von Entitäten (z. B. von type für Typ und error für Ausnahmen) trägt in der Regel nicht zu einer besseren Verständlichkeit des Codes bei. Suffixe wie array und struct implizieren außerdem eine spezifische Implementierung. Bei einer geänderten Implementierung (veränderte Darstellung einer Struktur oder eines Arrays) würde sich dies negativ auf den gesamten Clientcode auswirken oder irreführend sein.

    In ganz bestimmten Situationen können Suffixe aber auch sinnvoll sein:

    Handsymbol OK Verständliche, lesbare und aussagekräftige Namen wählen

    Lassen Sie sich bei der Auswahl der Namen von ihrer Verwendung leiten. Unterstreichen Sie eine lokale (kontextspezifische Bedeutung) durch die Verwendung von Substantiven mit Adjektiv. Achten Sie auch darauf, dass die Namen mit dem jeweiligen Typ korrespondieren.

    Wählen Sie die Namen so, dass Konstrukte wie die folgenden einfach zu lesen sind und aussagekräftig erscheinen:

    object_name.function_name(...);
    object_name->function_name(...);      

    Eine schnellere Eingabe ist keine akzeptable Rechtfertigung für die Verwendung von Kürzeln oder abgekürzten Namen. Kurze Bezeichner und Bezeichner, die nur aus einem Buchstaben bestehen, sind oft keine gute Wahl und ein Zeichen von Faulheit. Ausnahmen bilden allgemein anerkannte Abkürzungen wie E für die Basis natürlicher Logarithmen oder Pi.

    Leider wird die Länge von Namen manchmal durch Compiler und Unterstützungstools beschränkt. Achten Sie deshalb darauf, dass sich lange Namen nicht nur in ihren abschließenden Zeichen unterscheiden, denn diese Zeichen könnten von solchen Tools abgeschnitten werden.

    Beispiel
    void set_color(color new_color)
    {
      ...
    the_color = new_color;
      ...
    }
    Dieser Code ist besser als der folgende:
    void set_foreground_color(color fg)
    
    und:
    void set_foreground_color(color foreground);{
      ...
    the_foreground_color = foreground;
      ...
    }      

    Die Benennung im ersten Beispiel ist besser als die in den beiden folgenden; new_color ist qualifiziert und entspricht dem Typ, wodurch die Semantik der Funktion unterstützt wird.

    Im zweiten Fall könnte der intuitive Leser schlussfolgern, dass fg für foreground (Vordergrund) steht. Bei einem guten Programmierungsstil sollte jedoch nichts der Intuition oder den Rückschlüssen des Lesers überlassen bleiben.

    Im dritten Fall wird der Parameter foreground (entfernt von seiner Deklaration) verwendet. Der Leser könnte somit glauben, dass foreground eigentlich 'foreground color' (Vordergrundfarbe) bedeuten soll. Es wäre durchaus vorstellbar, dass der Typ von 'foreground' implizit in eine Farbe (color) konvertierbar ist.

    Anmerkungen

    Wenn Sie Namen aus Substantiven und Adjektiven bilden und sicherstellen, dass die Namen mit dem jeweiligen Typ korrespondieren, verbessern Sie die Lesbarkeit und Semantik, da sich diese Namensbildung an der natürlichen Sprache orientiert.

    Handsymbol OK Korrekte Rechtschreibung der Namen beachten

    Namensabschnitte, die englische Wörter sind, sollten korrekt geschrieben sein und der für das Projekt erforderlichen Form entsprechen, d. h. in britischem oder amerikanischem Englisch geschrieben werden. Das Gleiche gilt für Kommentare.

    Handsymbol OK Bejahende Prädikatklausel für Boolesche Objekte, Funktionen und Argumente verwenden

    Verwenden Sie für Boolesche Objekte, Funktionen und Funktionsargumente eine Prädikatklausel in bejahender Form, z. B. found_it oder is_available, aber nicht is_not_available.

    Begründung

    Prädikate können verneint werden, so dass eine doppelte Verneinung entstehen würde, die schwerer verständlich ist.

    Namespaces

    Handsymbol OK Namespaces zur Partitionierung potenzieller Globalnamen in Subsysteme oder Bibliotheken verwenden

    Wenn ein System in Subsysteme zerlegt wird, verwenden Sie die Subsystemnamen als Namespace-Namen für die Partitionierung und Minimierung des globalen Namespace des Systems. Falls das System eine Bibliothek ist, verwenden Sie einen äußeren Namespace für die gesamte Bibliothek.

    Geben Sie jedem Subsystem und jeder Bibliothek einen aussagekräftigen Namen und zusätzlich einen Kurznamen oder ein Akronym als Aliasnamen. Achten Sie bei der Auswahl von Kurznamen und Akronymen als Aliasnamen darauf, dass es keine Überschneidungen gibt. In der ANSI-Bibliothek des C++-Normentwurfs, [Plauger 95], ist std beispielsweise als Aliasname für iso_standard_library definiert.

    Falls der Compiler das Namespace-Konstrukt noch nicht unterstützt, simulieren Sie Namespaces mit Namenspräfixen. Die öffentlichen Namen der Schnittstelle eines Subsystems für Systemmanagement könnten beispielsweise mit dem Präfix syma (kurz für Systemmanagement) versehen werden.

    Begründung

    Durch die Verwendung von Namespaces für den Einschluss potenzieller Globalnamen können Namensüberschneidungen vermieden werden, wenn Codes unabhängig (von Unterprojektteams oder Lieferanten) entwickelt wird, denn die logische Folge ist, dass nur Namen im Namespace global sind.

    Klassen

    Handsymbol OK Substantive oder substantivische Ausdrücke für Klassennamen verwenden

    Verwenden Sie für die Benennung einer Klasse ein allgemeines Substantiv im Singular oder einen substantivischen Ausdruck, das bzw. der die Abstraktion der Klasse ausdrückt. Für Basisklassen sollten Sie eher allgemeine Namen und für abgeleitete Klassen spezialisierte Namen verwenden.

    typedef ... reference; // Aus der Standardbibliothek
    typedef ... pointer; // Aus der Standardbibliothek
    typedef ... iterator; // Aus der Standardbibliothek
    class bank_account {...};
    class savings_account : public bank_account {...};
    class checking_account : public bank_account {...};      

    Wenn es bei den Namen für Objekte und Typen zu einem Namenskonflikt kommt oder nicht genug geeignete Namen vorhanden sind, verwenden Sie den einfachen Namen für das Objekt und fügen Sie für den Typnamen ein Suffix wie mode, kind, code usw. hinzu.

    Verwenden Sie die Pluralform, wenn Sie eine Abstraktion für eine Sammlung von Objekten ausdrücken möchten.

    typedef some_container<...> yellow_pages;      

    Wenn über die reine Sammlung von Objekten hinaus zusätzlicher semantischer Gehalt erforderlich ist, verwenden Sie die folgenden Verhaltensmuster und Namenssuffixe aus der Standardbibliothek:

    Funktionen

    Handsymbol OK Verben für Namen von prozeduralen Funktionen verwenden

    Verwenden Sie für Funktionen ohne Rückgabewerte (Funktionsdeklarationen mit einem void-Rückgabetyp) oder Funktionen, die Werte durch Zeiger oder Referenzparameter zurückgeben, Verben oder Aktionsausdrücke.

    Für Funktionen, die nur einen einzigen Wert durch einen Nicht-void-Rückgabetyp zurückgeben, sollten Sie Substantive verwenden.

    Verwenden Sie für Klassen mit allgemeinen Operationen (Verhaltensmuster) Operationsnamen, die in einer Auswahlliste für das Projekt definiert sind, z. B. begin, end, insert, erase (Containeroperationen aus der Standardbibliothek).

    Vermeiden Sie die Verwendung von "get" und "set" bei der Benennung ("get" und "set" als Präfixe für Funktionen), insbesondere bei öffentlichen Operationen für das Abrufen und Setzen von Objektattributen. Die Benennung von Operationen sollte nicht die Ebene der Klassenabstraktion und der Leistungserbringung verlassen. Das Abrufen und Setzen von Objektattributen sind Implementierungsdetails der tieferen Ebenen, die beim Publizieren die Kapselungsmöglichkeiten einschränken.

    Adjektive (oder Vergangenheitspartizipien) sollten für Funktionen verwendet werden, die Boolesche Werte zurückgeben. Bei Prädikaten ist es oft hilfreich, ein Substantiv mit dem Präfix is oder has zu versehen, so dass sich der Name wie eine Affirmation liest. Dies ist auch sinnvoll, wenn der einfache Name bereits für einen Typnamen oder ein Aufzählungsliteral verwendet wird. Achten Sie präzise und konsistent auf die Zeitform.

    Beispiel
    void insert(...);
    void erase(...);
    
    Name first_name();
    bool has_first_name();
    bool is_found();
    bool is_available();      

    Verwenden Sie keine verneinenden Namen, da dies zu Ausdrücken mit doppelter Verneinung führen kann (z. B. !is_not_found), die den Code schwer verständlich machen. In einigen Fällen lässt sich mit einem Antonym aus einem negativen Prädikat ein positives machen, ohne die Semantik zu ändern. Sie könnten beispielsweise "is_invalid" anstatt "is_not_valid" verwenden.

    Beispiel
    bool is_not_valid(...);
    void find_client(name with_the_name, bool& not_found);
    
    Diese Zeilen sollten wie folgt neu definiert werden:
    bool is_valid(...);
    void find_client(name with_the_name, bool& found);      

    Handsymbol OK Funktionsüberladung bei identischer allgemeiner Bedeutung verwenden

    Wenn Operationen dasselbe Ziel verfolgen, sollten Sie nicht versuchen, Synonyme zu finden. Arbeiten Sie stattdessen mit Überladung. Sie minimieren so die Anzahl der Konzepte und Variationen von Operationen im System und verringern damit die Komplexität insgesamt.

    Achten Sie beim Überladen von Operatoren darauf, dass die Semantik der Operatoren gewahrt bleibt. Sollten Sie die konventionelle Bedeutung eines Operators nicht wahren können, überladen Sie den Operator nicht. Wählen Sie dann lieber einen anderen Namen für die Funktion.

    Objekte und Funktionsparameter

    Handsymbol OK Bedeutung von Namen durch grammatikalische Elemente hervorheben

    Stellen Sie einem Objektnamen oder Parameternamen das Präfix "the" oder "this" voran, um Eindeutigkeit zu erzielen oder zu zeigen, dass diese Entität den Schwerpunkt der Aktion bildet. Ein sekundäres, temporäres oder Hilfsobjekt können Sie mit dem Präfix "a" oder "current" versehen.

    Beispiel
    void change_name( subscriber& the_subscriber,
    const subscriber::name new_name)
    {
      ...
    the_subscriber.name = new_name;
      ...
    }
    void update(subscriber_list& the_list,
    const subscriber::identification with_id,
    structure& on_structure,
    const value for_value);
    void change( object& the_object,
    const object using_object);      

    Ausnahmen


    Handsymbol OK Für Ausnahmen Namen mit negativer Bedeutung wählen

    Da Ausnahmen nur für Fehlersituationen gedacht sind, sollten Sie ein Substantiv oder einen substantivischen Ausdruck mit negativer Wertung verwenden:

    overflow, threshold_exceeded, bad_initial_value      

    Handsymbol OK Projektdefinierte Adjektive für die Namen von Ausnahmen verwenden

    Nutzen Sie die Wörter bad, incomplete, invalid, wrong, missing oder illegal aus einer für das Projekt vereinbarte Liste als Teil des Namens, anstatt systematisch auf das Wort error oder exception zurückzugreifen, das keinen spezifischen Informationsgehalt hat.

    Verschiedenes

    Handsymbol OK Großbuchstaben für Gleitkommaexponenten und Hexadezimalziffern verwenden

    Die Buchstaben 'E' in Gleitkommaliteralen und die Hexadezimalziffern 'A' bis 'F' sollten immer Großbuchstaben sein.


    Kapitel 5

    Deklarationen

    Dieses Kapitel enthält Anleitungen für die Verwendung und Form verschiedener C++-Deklarationsarten.

    Namespaces

    Bevor es in der Sprache C++ Namespaces gab, konnte der Namensbereich nur mit sehr begrenzten Mitteln verwaltet werden. Der globale Namespace war demzufolge ziemlich überfüllt, was zu Konflikten geführt hat, die die gleichzeitige Verwendung einiger Bibliotheken innerhalb eines Programms unmöglich machten. Das neue Sprachfeature Namespace löst das Problem mit der Überfüllung des globalen Namespace.

    Zeigefingersymbol Globale Deklarationen ausschließlich auf Namespaces beschränken

    Dies bedeutet, dass nur Namen in Namespaces global sein können. Alle anderen Deklarationen sollten sich im Geltungsbereich eines Namespace befinden.

    Wird diese Regel verletzt, kann es zu Namenskollisionen kommen.

    Handsymbol OK Namespace zum Gruppieren von Nichtklassenfunktionen verwenden

    Verwenden Sie für die logische Gruppierung von Nichtklassenfunktionen (z. B. von Klassenkategorien) oder von Funktionen, die einen weit größeren Geltungsbereich als Klassen haben (Bibliothek oder Subsystem), einen Namespace, um die Deklarationen logisch zu vereinheitlichen (siehe Abschnitt "Potenziell globale Namen mit Namespaces nach Subsystemen oder Bibliotheken partitionieren").

    Drücken Sie im Namen die logische Funktionsgruppierung aus.

    Beispiel
    namespace transport_layer_interface { /* ... */ };
    namespace math_definitions { /* ... */ };      

    Handsymbol OK Verwendung von globalen Bereichsdaten und Namespace-Bereichsdaten minimieren

    Die Verwendung von globalen Bereichsdaten und Daten aus dem Namespace-Geltungsbereich läuft dem Kapselungsprinzip zuwider.

    Klassen

    Klassen sind in C++ die grundlegende Design- und Implementierungseinheit. Sie sollten zur Erfassung von Domänen- und Designabstraktionen und als Kapselungsmechanismus für die Implementierung abstrakter Datentypen verwendet werden.

    Handsymbol OK Für die Implementierung abstrakter Datentypen class statt struct verwenden

    Verwenden Sie für die Implementierung einer Klasse (eines abstrakten Datentyps) den Klassenschlüssel class und nicht struct.

    Nutzen Sie den Klassenschlüssel struct für Datenstrukturen im C-Stil (POD, Plain Old Data). Dies gilt insbesondere bei der Anbindung an C-Code.

    Die Schlüssel class und struct sind zwar äquivalent und austauschbar, bei class liegt jedoch der Schwerpunkt auf der Standardzugriffssteuerung (privat) für eine bessere Kapselung.

    Begründung

    Eine einheitliche Regelung für die Unterscheidung zwischen class und struct lässt sich nur in einer semantischen Unterscheidung jenseits der Sprachregeln finden. Das Konstrukt class wird vorrangig für die Erfassung von Abstraktionen und für die Kapselung verwendet, wohingegen struct eine reine Datenstruktur darstellt, die in Programmen mit gemischten Programmiersprachen ausgetauscht werden kann.

    Handsymbol OK Klassenelemente absteigend nach Zugriffsmöglichkeit deklarieren

    Die Zugriffskennungen in einer Klassendeklaration sollten in der Reihenfolge public, protected, private (öffentlich, geschützt, privat) erscheinen.

    Begründung

    Die Sortierung "public, protected, private" für Elementdeklarationen stellt sicher, dass die Informationen, die für den Benutzer der Klasse von größtem Interesse sind, zuerst angezeigt werden. Der Benutzer der Klasse muss so nicht durch irrelevante Informationen oder Implementierungsdetails navigieren.

    Handsymbol OK Deklaration öffentlicher oder geschützter Datenelemente für abstrakte Datentypen vermeiden

    Die Verwendung öffentlicher oder geschützter Datenelemente reduziert die Kapselungsfähigkeiten einer Klasse und wirkt sich auf die Änderungsbeständigkeit eines Systems aus. Öffentliche Datenelemente haben zur Folge, dass die Implementierung für die Benutzer der Klasse offen gelegt wird. Geschützte Datenelemente legen die Implementierung der Klasse für die abgeleiteten Klassen offen. Jede Änderung an den öffentlichen oder geschützten Datenelementen der Klasse hat wirkt sich auf die Benutzer und die abgeleiteten Klassen aus.

    Handsymbol OK Kapselung mit Freunden schützen

    Diese Richtlinie erscheint auf den ersten Blick kontraproduktiv zu sein. Bei einer Freundschaft werden private Abschnitte Freunden zugänglich gemacht. Wie soll dieses Mittel geeignet sein, die Kapselung zu schützen? Wen Klassen in hohem Maße voneinander abhängig sind und interne Details voneinander kennen müssen, ist es besser, sich für eine Freundschaft zu entscheiden, als die internen Details über die Klassenschnittstelle zu exportieren.

    Werden interne Details als öffentliche Elemente exportiert, erhalten Klassenclients einen nicht erwünschten Zugriff. Der Export geschützter Elemente gibt potenziellen Abkömmlingen Zugriff und unterstützt so ein hierarchisches Design, dass ebenfalls nicht erwünscht ist. Eine Freundschaft gewährleistet einen selektiven privaten Zugriff, ohne eine Unterklassenbildung zu erzwingen, so dass die Kapselung vor all jenen, die Zugriff anfordern, geschützt wird.

    Ein gutes Beispiel für die Nutzung der Freundschaft zum Schutz der Kapselung ist die Freundschaft zu einer Testklasse. Die befreundete Testklasse kann die Klasseninterna sehen und den entsprechenden Testcode implementieren. Später kann die befreundete Testklasse aus dem auszuliefernden Code entfernt werden. Im auszuliefernden Code geht so keine Kapselung verloren und wird kein Code hinzugefügt.

    Handsymbol OK Möglichst keine Funktionsdefinitionen in Klassendeklarationen angeben

    Klassendeklarationen sollten nur Funktionsdeklarationen und keine Funktionsdefinitionen (Implementierungen) enthalten.

    Begründung

    Wenn Sie Funktionsdefinitionen in eine Klassendeklaration aufnehmen, überfluten Sie die Klassenspezifikation mit Implementierungsdetails. Dadurch wird die Klassenschnittstelle schlechter erkennbar und lesbar. Außerdem werden dadurch die Kompilierungsabhängigkeiten vermehrt.

    Funktionsdefinitionen in Klassendeklarationen verringern außerdem die Kontrolle über das Inlining von Funktionen (siehe auch Abschnitt "Inline-Kompilierung mit dem Symbol No_Inline für bedingte Kompilierung unterbinden").

    Zeigefingersymbol Für Klassen mit explizit deklarierten Konstruktoren immer einen Standardkonstruktor angeben

    Wenn eine Klasse in einem Array oder in einem der STL-Container verwendet werden soll, muss sie einen öffentlichen Standardkonstruktor angeben oder dem Compiler erlauben, einen solchen zu generieren.

    Anmerkungen

    Die Ausnahme von der obigen Regel bildet eine Klasse mit einem nicht statischen Datenelement eines Referenztyps. In einem solchen Fall ist es oft nicht möglich, einen sinnvollen Standardkonstruktor zu erstellen. Die Verwendung eines Verweises auf ein Objektdatenelement sollte daher in Frage gestellt werden.

    Zeigefingersymbol Copy-Konstruktoren und Zuordnungsoperatoren für Klassen mit Datenelementen vom Typ Zeiger immer deklarieren

    Der Compiler generiert für eine Klasse implizit einen Copy-Konstruktor und einen Zuordnungsoperator, sofern dies erforderlich ist und weder Konstruktor noch Operator explizit deklariert ist. Der vom Compiler definierte Copy-Konstruktor und Zuordnungsoperator implementieren eine Kopie, die in der Smalltalk-Terminologie als "flache Kopie" bezeichnet wird und de facto eine Bit-für-Bit-Kopie der einzelnen Elemente für Zeiger ist. Bei der Verwendung des Compiler generierten Copy-Konstruktors und der Standardzuordnungsoperatoren sind Speicherverluste garantiert.

    Beispiel
    // Adaptiert von [Meyers 92]
    void f()
    {
    String hello("Hello");// Voraussetzung, dass String
                          // mit einem Zeiger auf ein Char-Array
                          // implementiert wird
    { // Neuen Geltungsbereich (Block) eingeben
    String world("World");
    world = hello;        // Durch die Zuordnung geht der ursprüngliche
                          // Speicher von world verloren
    }	// Zerstörung von world beim
    	// Verlassen des Blocks;
    	// indirekt auch von hello
    String hello2 = hello; // Zuordnung des zerstörten hello zu
                           // hello2
    }      

    Im obigen Code geht der Speicher, in dem sich die Zeichenfolge "World" befindet, nach der Zuordnung verloren. Beim Verlassen des inneren Blocks wird world zerstört. Dabei geht auch der von hello referenzierte Speicher verloren. Das zerstörte hello wird hello2 zugeordnet.

    Beispiel
    // Adaptiert von [Meyers 1992]
    void foo(String bar) {};
    void f()
    {
    String lost = "String that will be lost!";
    foo(lost);
    }      

    Wenn foo im obigen Code mit dem Argument lost aufgerufen wird, wird lost mit dem vom Compiler definierten Copy-Konstruktor in foo kopiert. Da lost als eine Bit-für-Bit-Kopie des Zeigers auf "String that will be lost!" kopiert wird, wird die Kopie von lost beim Verlassen von foo zusammen mit dem Speicher, der "String that will be lost!" enthält, zerstört, vorausgesetzt, der Destruktor wird korrekt implementiert, um Speicher freizugeben.

    Zeigefingersymbol Konstruktorparameter nie neu deklarieren, um einen Standardwert zu erhalten

    Beispiel
    // Beispiel aus [X3J16 95, Abschnitt 12.8]
    class X {
    public:
    X(const X&, int); // Parameter int wird nicht
                      // initialisiert
    				   // Kein benutzerdeklarierter Konstruktor, so
    				   // dass der Compiler implizit einen deklariert
    };
    // Durch die verzögerte Initialisierung des Parameters int
    // mutiert der Konstruktor zu einem Copy-Konstruktor.
    //
    X::X(const X& x, int i = 0) { ... }      
    Begründung

    Ein Compiler, der in einer Klassendeklaration keine Standardsignatur eines Copy-Konstruktors sieht, deklariert implizit einen Copy-Konstruktor. Durch die verzögerte Initialisierung von Standardparametern kann ein Konstruktor jedoch zum Copy-Konstruktor mutieren, was bei Verwendung eines Copy-Konstruktors Mehrdeutigkeit zur Folge hat. Die Verwendung eines Copy-Konstruktors ist wegen dieser Mehrdeutigkeit als mangelhafte Formatierung anzusehen [X3J16 95, Abschnitt 12.8].

    Zeigefingersymbol Destruktoren immer als virtuell deklarieren

    Der Destruktor einer Klasse sollte immer als virtuell deklariert werden, solange die Klasse nicht explizit als nicht ableitbar konzipiert ist.

    Begründung

    Das Löschen eines abgeleiteten Klassenobjekts über einen Zeiger oder Verweis auf einen Basisklassentyp führt zu nicht definierbarem Verhalten, wenn der Klassendestruktor nicht als virtuell deklariert wurde.

    Beispiel
    // Schlechter Stil, nur um sich kurz zu fassen
    class B {
    public:
    B(size_t size) { tp = new T[size]; }
    ~B() { delete [] tp; tp = 0; }
      //...
    private:
    T* tp;
    };
    
    class D : public B {
    public:
    D(size_t size) : B(size) {}
    ~D() {}
      //... 
    };
    
    void f()
    {
    B* bp = new D(10);
    delete bp; // Undefiniertes Verhalten wegen
    		    // des nicht virtuellen Desktruktors
    	       // für die Basisklasse
    }      

    Handsymbol OK Deklaration zu vieler Umsetzungsoperatoren und nur eines Parameterkonstruktors vermeiden

    Die Verwendung eines einzelnen Parameterkonstruktors für die implizite Umsetzung können Sie auch vermeiden, indem Sie den Konstruktor mit dem Bezeichner explicit deklarieren.

    Zeigefingersymbol Nicht virtuelle Funktionen nie erneut definieren

    Nicht virtuelle Funktionen implementieren unveränderliches Verhalten und sind nicht für eine Spezialisierung durch abgeleitete Klassen geeignet. Wenn Sie diese Richtlinie nicht beachten, kann es zu nicht erwartetem Verhalten kommen. Ein Objekt kann sich zu verschiedenen Zeiten unterschiedlich verhalten.

    Nicht virtuelle Funktionen werden statisch gebunden. Welche Funktion für ein Objekt aufgerufen wird, ist daher von dem statischen Typ der Variablen abhängig, die im folgenden Beispiel den Objektzeiger auf A bzw. den Objektzeiger auf B referenziert, und nicht vom Typ des Objekts selbst.

    Beispiel
    // Adaptiert von [Meyers 92]
    class A {
    public:
    void f(); // Nicht virtuell: statisch gebunden
    };
    class B : public A {
    public:
    void f(); // Nicht virtuell: statisch gebunden
    };
    void g()
    {
    B x;
    A* pA = &x; // Statischer Typ: Zeiger auf A
    B* pB = &x; // Statischer Typ: Zeiger auf B
    pA->f(); // Ruft A::f auf
    pB->f(); // Ruft B::f auf
    }      

    Handsymbol OK Nicht virtuelle Funktionen mit Vernunft verwenden

    Da nicht virtuelle Funktionen Unterklassen begrenzen, indem Sie die Spezialisierung und die Polymorphie einschränken, sollten Sie eine Operation erst als nicht virtuell deklarieren, wenn Sie sicher sind, dass die Operation wirklich für alle Unterklassen invariant ist.

    Handsymbol OK Initialisierungsroutine des Konstruktors an Stelle von Zuordnungen im Konstruktor verwenden

    Während der Konstruktion sollte der Status eines Objekts mit einer Initialisierungsroutine des Konstruktors (Liste der Initialisierungsoperationen für Elemente) und nicht mit Zuordnungsoperatoren im Hauptteil des Konstruktors initialisiert werden.

    Beispiel
    Verwenden Sie folgende Angabe:
    class X 
    {
    public:
    X();
    private
    Y the_y;
    };
    X::X() : the_y(some_y_expression) { } 
    //
    // "the_y" von einer Initialisierungsroutine des Konstruktors initialisiert
    

    Die obige Angabe ist besser als die hier folgende:

    X::X() { the_y = some_y_expression; }
    //
    // "the_y" von einem Zuordnungsoperator initialisiert
    
    Begründung

    Beim Konstruieren eines Objekts werden alle Basisklassen und Datenelemente konstruiert, bevor der Hauptteil des Konstruktors ausgeführt wird. Für die Initialisierung von Datenelementen sind zwei Operationen erforderlich (Konstruktion und Zuordnung), wenn sie im Hauptteil eines Konstruktors ausgeführt wird. Bei der Verwendung einer Initialisierungsroutine des Konstruktors ist nur eine Operation nötig (Konstruktion mit Anfangswert).

    Bei großen verschachtelten Aggregatklassen (Klassen, die Klassen enthalten, die wiederum Klassen enthalten...) machen sich die Leistungseinbußen durch die Vielzahl der Operationen (Konstruktion + Elementzuordnung) deutlich bemerkbar.

    Zeigefingersymbol Elementfunktionen nie von einer Initialisierungsroutine des Konstruktors aufrufen lassen

    Beispiel
    class A
    {
    public:
    A(int an_int);
    };
    class B : public A
    {
    public:
    int f();
    B();
    };
    
    B::B() : A(f()) {} 
    // Nicht definiert; die Elementfunktion wird aufgerufen, obwohl A noch
    // nicht initialisiert wurde [X3J16 95].      
    Begründung

    Das Ergebnis einer Operation ist nicht definiert, wenn eine Elementfunktion direkt oder indirekt von der Initialisierungsroutine für einen Konstruktor aufgerufen wird, bevor alle Initialisierungsoperationen für Basisklassen abgeschlossen sind [X3J16 95].

    Handsymbol OK Vorsicht beim Aufruf von Elementfunktionen in Konstruktoren/Destruktoren

    Seien Sie achtsam, wenn Sie Elementfunktionen in Konstruktoren aufrufen. Auch wenn Sie eine virtuelle Funktion aufrufen, wird die im Konstruktor oder in der Klasse des Desktruktors oder einer der Basisklassen dieser Klasse definierte Funktion ausgeführt.

    Handsymbol OK Verwendung von static const für integrale Klassenkonstanten

    Wenn Sie integrale Klassenkonstanten (Integer) definieren, verwenden Sie an Stelle von #define oder globalen Konstanten static const-Datenelemente. Falls der Compiler static const nicht unterstützt, verwenden Sie enum.

    Beispiel
    Verwenden Sie folgende Angabe:
    class X {
    static const buffer_size = 100;
    char buffer[buffer_size];
    };
    static const buffer_size;
    

    Sie können auch diese Angabe verwenden:

    class C {
    enum { buffer_size = 100 };
    char buffer[buffer_size];
    };
    

    Verwenden Sie jedoch nicht die folgende Angabe:

    #define BUFFER_SIZE 100
    class C {
    char buffer[BUFFER_SIZE];
    };      

    Funktionen

    Handsymbol OK Für Funktionen immer einen expliziten Rückgabetyp deklarieren

    Dadurch werden Unklarheiten verhindert, wenn der Compiler das Fehlen eines Rückgabetyps bei Funktionen moniert, die nicht mit einem expliziten Rückgabetyp deklariert sind.

    Handsymbol OK In Funktionsdeklarationen immer formale Parameternamen angeben

    Verwenden Sie dieselben Namen in Funktionsdeklarationen und Funktionsdefinitionen, um Überraschungen zu vermeiden. Die Angabe von Parameternamen verbessert die Codedokumentation und die Lesbarkeit des Codes.

    Handsymbol OK Funktionen mit nur einem Rückkehrpunkt anstreben

    Frei über den Hauptteil der Funktion verteilte Rückkehranweisungen sind wie goto-Anweisungen und machen den Code schwerer lesbar und schwerer zu warten.

    Mehrere returns können nur in sehr kleinen Funktionen toleriert werden, wenn alle returns gleichzeitig erkennbar sind und der Code eine sehr regelmäßige Struktur hat.

    type_t foo()
    {
    if (this_condition)
    return this_value;
    else
    return some_other_value;
    }      

    Funktionen mit dem Rückgabetyp void sollten keine Rückkehranweisung enthalten.

    Handsymbol OK Keine Funktionen mit globalen Nebeneffekten erstellen

    Erstellen Sie möglichst wenig Funktionen, die globale Nebeneffekte haben, d. h. unangekündigt andere Daten als ihren internen Objektstatus ändern, z. B. globale Daten und Namespace-Daten (siehe auch Abschnitt "Verwendung von globalen Bereichsdaten und Namespace-Bereichsdaten minimieren"). Falls sich solche Funktionen nicht vermeiden lassen, sollten Sie in der Funktionsspezifikation alle Nebeneffekte klar dokumentieren.

    Der Code ist stabiler, besser verständlich und weniger kontextabhängig, wenn die erforderlichen Objekte als Parameter übergeben werden.

    Handsymbol OK Funktionsparameter absteigend nach ihrer Wichtigkeit und Volatilität deklarieren

    Aus der Sicht des Aufrufenden ist die Reihenfolge wichtig, in der Parameter deklariert sind:

    Bei dieser Sortierung können Sie davon profitieren, dass Standardwerte die Anzahl der Argumente in Funktionsaufrufen verringern.

    Handsymbol OK Deklaration von Funktionen mit variabler Parameteranzahl vermeiden

    Argumente für Funktionen mit einer variablen Anzahl von Parametern können nicht auf ihren Typ überprüft werden.

    Handsymbol OK Erneutes Deklarieren von Funktionen mit Standardparametern vermeiden

    Vermeiden Sie das Hinzufügen von Standardwerten zu einer Funktion in weiteren Deklarationen der Funktion. Funktionen sollten, abgesehen von Forward-Deklarationen, nur einmal deklariert werden. Andernfalls kann es bei Lesern, die nichts von weiteren Deklarationen wissen, zu Unklarheiten kommen.

    Handsymbol OK Verwendung von const in Funktionsdeklarationen maximieren

    Überprüfen Sie, ob Funktionen irgendeine Form konstanten Verhaltens zeigen (einen konstanten Wert zurückgeben, konstante Argumente verwenden oder ohne Nebeneffekte ausgeführt werden), und sichern Sie das Verhalten mit dem Bezeichner const zu.

    Beispiel
    const T f(...); // Funktion, die ein konstantes
    		         // Objekt zurückgibt
    T f(T* const arg);	// Funktion, die einen konstanten
    			          // Zeiger verwendet
    // Das Objekt, auf das gezeigt wird, kann geändert
    // werden, der Zeiger jedoch nicht.
    T f(const T* arg);      // Funktion mit einem Zeiger und
    T f(const T& arg);      // Funktion mit einem Verweis auf ein
                            // konstantes Objekt. Der Zeiger kann
                            // sich ändern, aber nicht das Objekt,
                            // auf das gezeigt wird.
    T f(const T* const arg);  // Funktion mit einem konstanten Zeiger
                              // auf ein konstantes Objekt.
                              // Weder der Zeiger noch das Objekt
                              // kann sich ändern.
    T f(...) const;  // Funktion ohne Nebeneffekt:
                     // Der Objektstatus wird nicht geändert,
                     // so dass eine Anwendung auf konstante
                     // Objekte möglich ist. 

    Handsymbol OK Übergabe von Objekten in Form von Werten vermeiden

    Die Übergabe und Rückgabe von Objekten in Form ihres Wertes kann zu einem sehr hohen Konstruktor-/Destruktoraufkommen führen. Sie können diesen Konstruktor-/Destruktoraufwand vermeiden, indem Sie Objekte in Form einer Referenz übergeben und zurückgeben.

    Mit const-Referenzen können Sie angeben, dass als Referenz übergebene Argumente nicht modifiziert werden können. Typische Verwendungsbeispiele hierfür sind Copy-Konstruktoren und Zuordnungsoperatoren.

    C::C(const C& aC);
    C& C::operator=(const C& aC);      
    Beispiel

    Schauen Sie sich Folgendes an:

    the_class the_class::return_by_value(the_class a_copy)
    {
    return a_copy;
    }
    the_class an_object;
    return_by_value(an_object);      

    Wenn return_by_value mit an_object als Argument aufgerufen wird, wird der Copy-Konstruktor the_class aufgerufen, um an_object in a_copy zu kopieren. Der Copy-Konstruktor the_class wird erneut aufgerufen, um a_copy in das temporäre Objekt bei Rückkehr der Funktion zu kopieren. Der Destruktor the_class wird aufgerufen, um a_copy nach der Rückkehr von der Funktion zu zerstören. Einige Zeit später wird der Destruktor the_class erneut aufgerufen, um das von return_by_value zurückgegebene Objekt zu zerstören. So müssen für den obigen "tatenlosen" Funktionsaufruf zwei Konstruktoren und zwei Destruktoren aufgerufen werden.

    Noch schlimmer wäre die Situation, wenn the_class eine abgeleitete Klasse wäre und Elementdaten anderer Klassen enthielte. Die Konstruktoren und Destruktoren der Basisklassen und der enthaltenen Klassen würden auch aufgerufen werden. Eine Eskalation der Konstruktor- und Destruktor-Aufrufe für den Funktionsaufruf wäre unvermeidlich.

    Anmerkungen

    Die obige Richtlinie könnte von Entwicklern als Einladung verstanden werden, Objekte immer per Referenz zu übergeben und zurückzugeben. Achten Sie jedoch darauf, keine Referenzen auf lokale Objekte zurückzugeben und keine Referenzen zurückzugeben, wo Objekte erforderlich sind. Die Rückgabe einer Referenz auf ein lokales Objekt wird unweigerlich in die Katastrophe führen, denn bei Rückkehr der Funktion ist die zurückgegebene Referenz an ein zerstörtes Objekt gebunden!

    Zeigefingersymbol Keine Verweise auf lokale Objekte zurückgeben

    Lokale Objekte werden beim Verlassen des Geltungsbereichs der Funktion zerstört. Bei Verwendung zerstörter Objekte ist die Katastrophe vorprogrammiert.

    Zeigefingersymbol Nie einen mit new initialisierten dereferenzierten Zeiger zurückgeben

    Die Nichteinhaltung dieser Richtlinie führt zu Speicherlecks.

    Beispiel
    class C {
    public:
      ...
    friend C& operator+( const C& left, 
    const C& right);
    };
    C& operator+(const C& left, const C& right)
    {
    C* new_c = new C(left..., right...);
    return *new_c;
    }
    C a, b, c, d;
    C sum;
    sum = a + b + c + d;      

    Da die Zwischenergebnisse der Operatoren operator+ nicht gespeichert werden, wenn die Summe berechnet wird, können die Zwischenobjekte nicht gelöscht werden. Dies führt zu Speicherlecks.

    Zeigefingersymbol Keinen nicht konstanten Verweis oder Zeiger auf Elementdaten zurückgeben

    Die Nichteinhaltung dieser Richtlinie führt zu einer Verletzung der Datenkapselung und kann böse Überraschungen nach sich ziehen.

    Handsymbol OK Für Makroerweiterung bevorzugt Inline-Funktionen statt #define verwenden

    Nutzen Sie Inline-Funktionen jedoch mit Verstand. Verwenden Sie nur sehr kleine Funktionen. Das Inlining großer Funktionen führt zu einer Aufblähung des Codes.

    Inline-Funktionen vermehren auch die Kompilierungsabhängigkeiten zwischen Modulen, denn die Implementierung der Inline-Funktionen muss für die Kompilierung des Clientcodes verfügbar sein.

    Die Veröffentlichung [Meyers 1992] beschäftigt sich ausführlich mit dem folgenden, ziemlich extremen Beispiel einer schlechten Makroverwendung:

    Beispiel

    Verwenden Sie nicht diese Angabe:

    #define MAX(a, b) ((a) > (b) ? (a) : (b))     

    Verwenden Sie lieber diese Angabe:

    inline int max(int a, int b) { return a > b ? a : b; }      

    Das Makro MAX ist mit einer Reihe von Problemen behaftet. Es ist nicht typsicher und weist ein nicht deterministisches Verhalten auf.

    int a = 1, b = 0;
    MAX(a++, b);     // a wird zweimal erhöht
    MAX(a++, b+10);  // a wird einmal erhöht
    MAX(a, "Hello"); // Vergleich von ints und Zeigern 

    Handsymbol OK Standardparameter anstelle einer Funktionsüberladung nutzen

    Wenn Sie sich auf einen Algorithmus beschränken können und dieser mit wenigen Parametern auskommt, sollten Sie anstelle der Funktionsüberladung Standardparameter verwenden.

    Durch die Verwendung von Standardparametern können Sie die Anzahl überladener Funktionen verringern, die Wartungsfreundlichkeit verbessern und die Anzahl der in Funktionsaufrufen erforderlichen Argumente reduzieren. Sie verbessern damit die Lesbarkeit des Codes.

    Handsymbol OK Allgemeine Semantik mit Funktionsüberladung ausdrücken

    Wenn für eine semantische Operation mit verschiedenen Argumenttypen mehrere Implementierungen erforderlich sind, nutzen Sie die Funktionsüberladung.

    Achten Sie beim Überladen von Operatoren darauf, dass die konventionelle Bedeutung gewahrt bleibt. Vergessen Sie nicht, zugehörige Operatoren, z. B. operator== und operator!=, zu definieren.

    Handsymbol OK Keine Funktionen überladen, die Zeiger und Integer verwenden

    Vermeiden Sie das Überladen von Funktionen mit nur einem Zeigerargument durch Funktionen mit nur einem Integerargument.

    void f(char* p);
    void f(int i);      

    Die folgenden Aufrufe könnten für Überraschungen sorgen:

    f(NULL); f(0);  

    Die Auflösung der Überladung ergibt f(int) und nicht f(char*).

    Zeigefingersymbol Operator operator= einen Verweis auf *this zurückgeben lassen

    In C++ können Zuordnungsoperatoren verkettet werden.

    String x, y, z;
    x = y = z = "A string";      

    Da der Zuordnungsoperator rechtsassoziativ ist, wird die Zeichenfolge "A string" z, z bis y und y bis x zugeordnet. Der Operator operator= wird für jeden Ausdruck rechts vom Gleichheitszeichen (von links nach rechts) effektiv einmal aufgerufen. Das bedeutet auch, dass das Ergebnis jedes Operators operator= ein Objekt ist. Für die Rückgabe stehen allerdings das linke und das rechte Objekt zur Auswahl.

    In der Praxis hat sich das folgende Format für die Signatur des Zuordnungsoperators bewährt:

    C& C::operator=(const C&);      

    Demnach kommt nur das linke Objekt in Frage (rechts: const-Referenz, links: keine const-Referenz), so dass *this zurückgegeben werden sollte. Eine ausführliche Erläuterung finden Sie in [Meyers 1992].

    Zeigefingersymbol Operator operator= auf Selbstzuordnung überprüfen lassen

    Für diese Überprüfung gibt es zwei gute Gründe. Zum Einen wird bei der Zuordnung eines abgeleiteten Klassenobjekts der Zuordnungsoperator jeder in der Vererbungshierarchie übergeordneten Basisklasse aufgerufen. Eine Vermeidung dieser Operationen kann zu einer deutlichen Laufzeitersparnis führen. Zum Zweiten wird bei der Zuordnung das Objekt "lvalue" zerstört, bevor das Objekt "rvalue" kopiert wird. Im Falle einer Selbstzuordnung wird das Objekt rvalue zerstört, bevor es zugeordnet wird. Das Ergebnis der Zuordnung ist damit undefiniert.

    Handsymbol OK Komplexität weitestgehend reduzieren

    Schreiben Sie keine übermäßig langen Funktionen, z. B. mit mehr als 60 Codezeilen.

    Reduzieren Sie die Anzahl der Rückkehranweisungen auf ein Minimum. Im Idealfall verwenden Sie nur eine Rückkehranweisung.

    Streben Sie eine zyklomatische Komplexität kleiner 10 an (Summe der Entscheidungsanweisungen + 1 für Funktionen mit nur einer exit-Anweisung).

    Streben Sie eine erweiterte zyklomatische Komplexität kleiner 15 an (Summe der Entscheidungsanweisungen + logische Operatoren + 1 für Funktionen mit nur einer exit-Anweisung).

    Verringern Sie den maximalen Referenzabstand (Abstand zwischen der Deklaration eines lokalen Objekts und der ersten Instanz seiner Verwendung) soweit wie möglich.

    Typen

    Tippsymbol Globale Systemtypen für das gesamte Projekt definieren

    Bei großen Projekten gibt es in der Regel eine Gruppe von Typen, die regelmäßig vom gesamten System verwendet werden. Hier macht es Sinn, diese Typen in globalen Dienstprogramm-Namespaces der tieferen Ebene zusammenzufassen (siehe Beispiel im Abschnitt "Verwendung fundamentaler Typen vermeiden").

    Handsymbol OK Verwendung fundamentaler Typen vermeiden

    Wenn Sie sich ein hohes Maß an Portierbarkeit zum Ziel gesetzt haben, eine Kontrolle des von numerischen Objekten belegten Speicherbereichs oder ein bestimmter Wertebereich erforderlich ist, sollten Sie keine fundamentalen Typen verwenden. In solchen Situationen ist es besser, die geeigneten fundamentalen Typen zu verwenden, um explizite Typnamen mit Größenbegrenzungen zu deklarieren.

    Vergewissern Sie sich, dass sich die fundamentalen Typen nicht durch Schleifenzähler, Array-Indizes etc. wieder in den Code einschleichen.

    Beispiel
    namespace system_types {
    typedef unsigned char byte;
    typedef short int integer16; // 16-Bit-Integer mit Vorzeichen
    typedef int integer32; // 32-Bit-Integer mit Vorzeichen
    typedef unsigned short int natural16; // 16-Bit-Integer ohne Vorzeichen
    typedef unsigned int natural32; // 32-Bit-Integer ohne Vorzeichen
      ...
    }      
    Begründung

    Die Darstellung fundamentaler Typen ist von der Implementierung abhängig.

    Tippsymbol Synonyme zur Stärkung der lokalen Bedeutung mit typedef erstellen

    Erstellen Sie Synonyme für vorhandene Namen mit typedef, um aussagefähigere lokale Namen zu erhalten und die Lesbarkeit zu verbessern. (Dies hat keine negativen Auswirkungen auf die Laufzeit.)

    Mit typedef können Sie auch Kürzel für qualifizierte Namen angeben.

    Beispiel
    // Vektordeklaration aus der Standardbibliothek
    //
    namespace std {
    template <class T, class Alloc = allocator>
    class vector {
    public:
    typedef typename 
    Alloc::types<T>reference reference;
    typedef typename 
    Alloc::types<T>const_reference const_reference;
    typedef typename 
    Alloc::types<T>pointer iterator;
    typedef typename 
    Alloc::types<T>const_pointer const_iterator;
      ...
    }
    }      

    Wenn Sie mit typedef erstellte Namen verwenden, sollten Sie den ursprünglichen Namen und das Synonym nicht in demselben Codeabschnitt angeben.

    Konstanten und Objekte

    Handsymbol OK Verwendung von Literalwerten vermeiden

    Geben Sie benannten Konstanten den Vorzug.

    Handsymbol OK Vorprozessordirektive #define nicht zum Definieren von Konstanten verwenden

    Verwenden Sie lieber const oder enum.

    Verwenden Sie nicht diese Angabe:

    #define LIGHT_SPEED 3E8

    Verwenden Sie lieber diese Angabe:

    const int light_speed = 3E8;

    Oder diese für die Dimensionierung von Arrays:

    enum { small_buffer_size = 100, 
    large_buffer_size = 1000 };      
    Begründung

    Das Debugging ist viel mühsamer, weil mit #defines eingeführte Namen während der Vorbearbeitung für die Kompilierung ersetzt werden und nicht in Symboltabellen auftauchen.

    Handsymbol OK Objekte nahe am ersten Verwendungspunkt deklarieren

    Handsymbol OK Konstante Objekte immer beim Deklarieren initialisieren

    Konstante Objekte (const), die nicht extern deklariert werden, haben eine interne Verbindung. Werden diese konstanten Objekte beim Deklarieren initialisiert, können die Initialisierungsroutinen zur Kompilierungszeit verwendet werden.

    Zeigefingersymbol Konstanz eines konstanten Objekts immer bewahren

    Konstante Objekte können im Nur-Lese-Speicher vorhanden sein.

    Handsymbol OK Objekte beim Definieren initialisieren

    Geben Sie in Objektdefinitionen Anfangswerte an, wenn es sich nicht um selbstinitialisierende Objekte handelt. Falls es nicht möglich ist, einen sinnvollen Anfangswert zuzuordnen, sollten Sie einen NIL-Wert zuordnen oder das Objekt später deklarieren.

    Es ist generell nicht empfehlenswert, große Objekte zu konstruieren und erst später durch eine Zuordnung zu initialisieren, da dies sehr aufwendig werden kann (siehe auch Abschnitt "Initialisierungsroutine des Konstruktors an Stelle von Zuordnungen im Konstruktor verwenden").

    Falls ein Objekt nicht ordnungsgemäß initialisiert werden kann, wenn es konstruiert wird, initialisieren Sie es mit einem konventionellen NIL-Wert, der die Bedeutung "nicht initialisiert" hat. Der NIL-Wert für die Initialisierung ist nur zu verwenden, um einen "nicht verwendbaren, aber bekannten Wert" zu deklarieren, der von Algorithmen kontrolliert zurückgewiesen werden kann, und um einen Fehler (nicht initialisierte Variable) anzuzeigen, wenn das Objekt vor seiner ordnungsgemäßen Initialisierung verwendet wird.

    Es ist nicht immer möglich, für alle Typen einen NIL-Wert zu deklarieren. Dies gilt insbesondere für Modulo-Typen, z. B. für eine Winkel. Wählen Sie in diesem Fall den am wenigsten wahrscheinlichen Wert.


    Kapitel 6

    Ausdrücke und Anweisungen

    Dieses Kapitel enthält Anleitungen für die Verwendung und Form verschiedener C++-Ausdrücke und -Anweisungen.

    Ausdrücke

    Tippsymbol Verbundausdrücke mit paarigen runden Klammern verdeutlichen

    Tippsymbol Zu tiefe Verschachtelung von Ausdrücken vermeiden

    Die Verschachtelungsebene eines Ausdrucks definiert die Anzahl verschachtelter paariger Klammern, die für die Auswertung eines Ausdrucks von links nach rechts erforderlich sind, wenn die Vorrangregeln für Operatoren ignoriert werden.

    Zu viele Verschachtelungsebenen machen Ausdrücke schwer verständlich.

    Zeigefingersymbol Keine bestimmte Auswertungsreihenfolge für Ausdrücke voraussetzen

    Gehen Sie nicht von einer bestimmten Auswertungsreihenfolge aus, solange diese nicht von einem Operator (Kommaoperator, Ternärausdruck, Konjunktionen/Disjunktionen) angegeben wird. Wird dennoch eine bestimmte Auswertungsreihenfolge vorausgesetzt, kann es zu bösen Überraschungen und zur Nichtportierbarkeit kommen.

    Kombinieren Sie beispielsweise nicht eine Variable in einer Anweisung mit einem Inkrement/Dekrement der Variablen.

    Beispiel
    foo(i, i++);
    array[i] = i--;      

    Tippsymbol Für Nullzeiger 0 statt NULL verwenden

    Die Verwendung von 0 oder NULL für Nullzeiger wird sehr kontrovers diskutiert.

    Gemäß Definition in C und C++ kann jeder konstante Ausdruck mit dem Wert als ein Nullzeiger interpretiert werden. Da 0 schlecht zu lesen ist und die Verwendung von Literalen nach Möglichkeit vermieden werden sollte, wird von Programmierern traditionell das Makro NULL als Nullzeiger verwendet. Bedauerlicherweise gibt es keine portierbare Definition für NULL. Einige ANSI-C-Compilers verwenden (void *)0. Für C++ ist dies jedoch eine schlechte Lösung.

    char* cp = (void*)0; /* Legales C, aber kein C++ */      

    Jede Definition von NULL im Format (T*)0 macht somit in C++ eine Umsetzung erforderlich. In den bisherigen Richtlinien, die die Verwendung von 0 für Nullzeiger anraten, wurde versucht, die Portierbarkeit des Codes zu verbessern und die erforderliche Umsetzung zu erleichtern. Viele C++-Entwickler verwenden dennoch lieber NULL und argumentieren, dass die meisten modernen Compiler (oder genauer gesagt, die meisten Header-Dateien) NULL als 0 implementieren.

    Die vorliegende Richtlinie spricht sich für die Verwendung von 0 aus, da 0 garantiert funktioniert, ganz gleich, welchen Wert NULL hat. Aufgrund der sehr unterschiedlichen Meinungen zu dieser Frage ist die Verwendung von 0 hier nur als Tipp eingestuft, den Sie annehmen oder ignorieren können.

    Zeigefingersymbol Keine explizite Typumwandlung im alten Stil

    Verwenden Sie für die explizite Typumwandlung die neuen Operatoren (dynamic_cast, static_cast, reinterpret_cast, const_cast).

    Sollten die neuen Operatoren für die explizite Typumwandlung nicht verfügbar sein, verzichten Sie ganz auf die explizite Typumwandlung. Dies gilt insbesondere für die Umwandlung eines Basisklassenobjekts in ein abgeleitetes Klassenobjekt.

    Verwenden Sie die Operatoren für explizite Typumwandlung wie folgt:

    Verwenden Sie nicht typeid zur Implementierung einer Typumschaltlogik. Nutzen Sie die Operatoren für explizite Typumwandlung für die automatische Typüberprüfung und -umwandlung. (Die Veröffentlichung [Stroustrup 1994] beschäftigt sich ausführlich mit diesem Thema.)

    Beispiel

    Verwenden Sie nicht die folgende Angabe:

    void foo (const base& b)
    {
    if (typeid(b) == typeid(derived1)) {
    do_derived1_stuff();
    else if (typeid(b) == typeid(derived2)) {
    do_derived2_stuff();
    else if () {
    
      }
    }      
    Begründung

    Die explizite Typumwandlung im alten Stil zerstört das Typsystem und kann zu kaum feststellbaren Programmfehler führen, die vom Compiler nicht erkannt werden. Das Speicherverwaltungssystem und virtuelle Funktionstabellen können beschädigt werden. Dasselbe gilt für nicht zugehörige Objekte, die beschädigt werden können, wenn auf das Objekt als abgeleitetes Klassenobjekt zugegriffen wird. Dieser Schaden kann bereits durch einen Lesezugriff eintreten, der nicht vorhandene Zeiger oder Felder referenzieren könnte.

    Mit den neuen Operatoren für explizite Typumwandlung ist die Umwandlung (in den meisten Fällen) sicherer und expliziter.

    Zeigefingersymbol Für Boolesche Ausdrücke neuen Typ bool verwenden

    Verwenden Sie nicht die alte Form Boolescher Makros oder Konstanten. Es gibt keinen Booleschen Standardwert true. Verwenden Sie den neuen Typ bool.

    Zeigefingersymbol Keine direkten Vergleiche mit dem Booleschen Wert true

    Da es im herkömmlichen Sinne keine Standardwert für true gibt (1 oder !0), können Vergleiche von Ausdrücken ungleich null mit true scheitern.

    Verwenden Sie stattdessen Boolesche Ausdrücke.

    Beispiel

    Vermeiden Sie Angaben wie diese:

    if (someNonZeroExpression == true) 
    // Wird möglicherweise nicht als true ausgewertet
    
    Verwenden Sie besser diese Angabe:
    if (someNonZeroExpression) 
    // Wird immer als wahre Bedingung (true) ausgewertet 

    Zeigefingersymbol Zeiger nie mit Objekten außerhalb desselben Arrays vergleichen

    Das Ergebnis solcher Operationen ist in den seltensten Fällen aussagekräftig.

    Zeigefingersymbol Einem gelöschten Objektzeiger immer einen Nullzeigerwert zuordnen

    Vermeiden Sie Katastrophen, indem Sie den Zeiger auf ein gelöschtes Objekt auf null setzen. Das wiederholte Löschen eines Nichtnullzeigers richtet Schaden an, das wiederholte Löschen eines Nullzeigers ist dagegen harmlos.

    Weisen Sie nach dem Löschen immer einen Nullzeigerwert zu, auch vor der Rückkehr einer Funktion, denn später könnte neuer Code hinzugefügt werden.

    Anweisungen

    Handsymbol OK Für die Verzweigung Boolescher Ausdrücke eine if-Anweisung verwenden

    Handsymbol OK Für die Verzweigung diskreter Werte eine switch-Anweisung verwenden

    Verwenden Sie an Stelle mehrerer "else if" eine switch-Anweisung, wenn die Verzweigungsbedingung ein diskreter Wert ist.

    Zeigefingersymbol Zum Abfangen von Fehlern in switch-Anweisungen immer eine default-Verzweigung angeben

    Eine switch-Anweisung sollte immer eine default-Verzweigung enthalten, die zum Abfangen von Fehlern genutzt werden sollte.

    Diese Richtlinie stellt sicher, dass die vorhandene default-Verzweigung einen Fehler abfängt, wenn neue switch-Werte eingeführt und Verzweigungen für die Behandlung der neuen Werte übergangen werden.

    Handsymbol OK Bei erforderlichem Schleifentest vor einer Iteration eine for- oder while-Anweisung verwenden

    Wenn die Iteration und die Beendigung der Schleife anhand eines Schleifenzählers erfolgt, sollten Sie eher eine for-Anweisung als eine while-Anweisung verwenden.

    Handsymbol OK Bei erforderlichem Schleifentest nach einer Iteration eine do-while-Anweisung verwenden

    Handsymbol OK Sprunganweisungen in Schleifen vermeiden

    Vermeiden Sie den Austritt aus einer Schleife mit break, return oder goto. Eine Schleife sollte nur beim Eintritt der Bedingung für die Beendigung der Schleife verlassen werden. Das vorzeitige Springen zur nächsten Iteration mit continue sollte vermieden werden. Sie reduzieren damit die Anzahl der Steuerpfade und machen den Code leichter verständlich.

    Zeigefingersymbol Keine Anweisung goto verwenden

    Dies ist eine allgemein gültige Richtlinie.

    Handsymbol OK Verdecken von Bezeichnern in verschachtelten Bereichen vermeiden

    Dies kann zur Verunsicherung der Leser und zu potenziellen Risiken bei der Wartung führen.


    Kapitel 7

    Spezielle Themen

    Dieses Kapitel enthält Anleitungen für die Speicherverwaltung und die Erstellung von Fehlerberichten.

    Speicherverwaltung

    Zeigefingersymbol Speicheroperationen von C nicht mit Speicheroperationen von C++ mischen

    Die C-Bibliotheksfunktionen malloc, calloc und realloc sollten nicht für die Zuordnung von Objektbereichen verwendet werden. Nutzen Sie für diesen Zweck den C++-Operator new.

    Ordnen Sie Speicher nur dann mit den C-Funktionen zu, wenn Speicher an eine C-Bibliotheksfunktion übergeben werden soll, um wieder frei verfügbar zu sein.

    Löschen Sie mit C-Funktionen zugeordneten Speicher nicht mit delete, und verwenden Sie nicht free für Objekte, die mit new erstellt wurden.

    Zeigefingersymbol Mit new erstellte Array-Objekte immer mit delete[] löschen

    Wenn Sie für Array-Objekte delete ohne die Schreibweise mit eckigen Klammern ("[]") verwenden, wird nur das erste Array-Element gelöscht. Es kommt also zu einem Speicherverlust.

    Fehlerbehandlung und Ausnahmen

    Zum C++-Ausnahmemechanismus liegen noch nicht viele Erfahrungen vor. Es ist daher möglich, dass die hier gegebenen Richtlinien später einer gründlichen Überarbeitung unterzogen werden müssen.

    Der Normentwurf für C++ definiert zwei grobe Fehlerkategorien, logische Fehler und Laufzeitfehler. Logische Fehler sind vermeidbare Programmierfehler. Laufzeitfehler sind als Fehler definiert, die aufgrund von Ereignissen außerhalb des Programmgeltungsbereichs auftreten.

    Als allgemeine Regel für die Verwendung von Ausnahmen gilt, dass das System unter normalen Bedingungen keine Ausnahmen auslösen sollte, wenn keine Überlastung und kein Hardwarefehler vorliegt.

    Handsymbol OK Zusicherungen im Interesse der Fehlererkennung liberal handhaben

    Verwenden Sie während der Entwicklung Zusicherungen für die Vor- und Nachbedingungen von Funktionen, um schwerwiegende Fehler erkennen zu können.

    Zusicherungen sind ein einfacher und sinnvoller vorläufiger Fehlererkennungsmechanismus, der bis zur Implementierung des abschließenden Fehlerbehandlungscodes genutzt werden kann. Zusätzlich bieten Zusicherungen den Vorteil, dass sie mit dem Vorprozessorsymbol "NDEBUG" herauskompiliert werden können (siehe Abschnitt "Symbol NDEBUG mit bestimmtem Wert definieren").

    Traditionell wird zu diesem Zweck das Makro assert verwendet. In der Referenzdokumentation [Stroustrup 1994] wird jedoch eine Alternative mit template vorgestellt (siehe folgendes Beispiel).

    Beispiel
    template<class T, class Exception>
    inline void assert ( T a_boolean_expression, 
    Exception the_exception)
    {
    if (! NDEBUG)
    if (! a_boolean_expression) 
    throw the_exception;
    }      

    Handsymbol OK Ausnahmen nur für echte Ausnahmebedingungen verwenden

    Verwenden Sie Ausnahmen nicht für häufig eintretende oder zu erwartende Ereignisse. Ausnahmen können den normalen Steuerungsfluss im Code unterbrechen, was die Verständlichkeit des Codes verschlechtert und die Wartung erschwert.

    Erwartete Ereignisse sollten im normalen Steuerungsfluss des Codes bearbeitet werden. Verwenden Sie den Rückgabewert einer Funktion oder den Statuscode eines Parameters "out".

    Sie sollten auch keine Steuerstrukturen mit Ausnahmen implementieren. Dies wäre nur eine andere Form einer "goto"-Anweisung.

    Handsymbol OK Projektausnahmen von Standardausnahmen ableiten

    So stellen Sie sicher, dass alle Ausnahmen ein Mindestmaß an gemeinsamen Operationen unterstützen und von einigen wenigen High-Level-Behandlungsroutinen bearbeitet werden können.

    Fehler der Anwendungsdomäne, die Übergabe ungültiger Argumente an Funktionsaufrufe, die Konstruktion von Objekten mit ungültiger Größe und Argumentwerte außerhalb des zulässigen Bereichs sollten als logische Fehler (Domänenfehler, ungültiges Argument, Längenfehler, Bereichsüberschreitung) angezeigt werden.

    Arithmetische Fehler und Konfigurationsfehler, beschädigte Daten oder die Nichtverfügbarkeit von Ressourcen, die erst in der Laufzeit festgestellt werden können, sollten als Laufzeitfehler (Bereichs- oder Überlauffehler) angezeigt werden.

    Handsymbol OK Anzahl der Ausnahmen für eine gegebene Abstraktion minimieren

    Wenn man es bei großen Systemen auf jeder Ebene mit einer Vielzahl von Ausnahmen zu tun hat, ist der Code schwer zu lesen und zu warten. Der Aufwand für die Ausnahmebehandlung kann größer als der für die normale Verarbeitung sein.

    Möglichkeiten, die Anzahl der Ausnahmen zu minimieren:

    Verwenden Sie nur eine kleine Anzahl von Ausnahmekategorien, die von mehreren Abstraktionen gemeinsam genutzt werden können.

    Wenn die Ausnahme eine von den Standardausnahmen abgeleitete spezielle Ausnahme ist, sollte diese ausgelöst werden. Handelt es sich um eine eher allgemeine Ausnahme, sollte diese verwendet und keine andere Ausnahme ausgelöst werden.

    Fügen Sie Ausnahmestatus für die Objekte hinzu und geben Sie Basiselemente (Primitives) an, um explizit die Gültigkeit der Objekte zu überprüfen.

    Handsymbol OK Alle ausgelösten Ausnahmen deklarieren

    Funktionen, die Ausnahmen auslösen (und nicht nur "durchreichen"), sollten in ihrer Ausnahmenspezifikation alle ausgelösten Ausnahmen deklarieren. Es sollten keine "heimlichen" Ausnahmen ohne Vorwarnung an die Clients ausgelöst werden.

    Tippsymbol Ausnahmen beim ersten Auftreten dokumentieren

    Dokumentieren Sie beim Entwickeln Ausnahmen und den Auslösepunkt so früh wie möglich mit dem entsprechenden Protokollierungsmechanismus.

    Handsymbol OK Ausnahmebehandlungsroutinen absteigend nach ihrer Entfernung von der Basisklasse definieren

    Ausnahmebehandlungsroutinen sollten absteigend nach ihrer Entfernung von der Basisklasse definiert werden, um das Codieren nicht erreichbarer Behandlungsroutinen zu vermeiden. Vergleichen Sie hierzu das folgende Beispiel für eine zu vermeidende Definition. Außerdem können Sie so sicherstellen, dass eine Ausnahme von der am besten geeigneten Behandlungsroutine abgefangen wird, denn die Routinen werden in der Reihenfolge abgeglichen, in der sie deklariert sind.

    Beispiel

    Verwenden Sie nicht diese Angabe:

    class base { ... };
    class derived : public base { ... };
    ...
    try 
    {
      ...
    throw derived(...);
      //
    // Abgeleitete Klassenausnahme auslösen
    }
    catch (base& a_base_failure)
      //
    // Wird von der Basisklassenbehandlungsroutine
    // abgefangen, weil diese als erste übereinstimmt!
    {
      ...
    }
    catch (derived& a_derived_failure) 
    //
    // Diese Behandlungsroutine ist nicht erreichbar!
    { 
      ...
    }      

    Handsymbol OK Universelle Ausnahmebehandlungsroutinen vermeiden

    Vermeiden Sie universelle Ausnahmebehandlungsroutinen, sofern die Ausnahme nicht mehrfach ausgelöst wird.

    Universelle Behandlungsroutinen sollten nur für eine lokale Bereinigung verwendet werden. Lösen Sie anschließend die Ausnahme erneut aus, um festzustellen, ob die Ausnahme auf dieser Ebene behandelt werden kann.

    try 
    {
      ...
    }
    catch (...)
    { 
    if (io.is_open(local_file))
      {
    io.close(local_file);
      }
    throw;
    }      

    Zeigefingersymbol Geeigneten Wert für Funktionsstatuscodes sicherstellen

    Wenn Statuscodes als Funktionsparameter zurückgegeben werden, stellen Sie sicher, dass dem Parameter in der ersten ausführbaren Anweisung des Funktionshauptteils ein Wert zugeordnet ist. Machen Sie alle Status standardmäßig zu einem Erfolgs- oder Fehlerstatus. Denken Sie an alle Möglichkeiten, die Funktion zu verlassen, einschließlich der Ausnahmebehandlungsroutinen.

    Handsymbol OK Sicherheitsprüfungen lokal durchführen und nicht dem Client überlassen

    Falls eine Funktion fehlerhafte Ausgaben erzeugen kann, wenn keine korrekte Eingabe erfolgt, installieren Sie in der Funktion Code, um kontrolliert ungültige Eingaben erkennen und dokumentieren zu können. Verlassen Sie sich nicht auf einen Kommentar, in dem der Client angewiesen wird, korrekte Werte zu übergeben. Theoretisch ist garantiert, dass der Kommentar früher oder später ignoriert wird, was zu kaum feststellbaren Fehlern führen kann, sofern die ungültigen Parameter nicht gefunden werden.


    Kapitel 8

    Portierbarkeit

    Dieses Kapitel Abschnitt beschäftigt sich mit Sprachfeatures, die an sich nicht portierbar sind.

    Pfadnamen

    Zeigefingersymbol Keine fest codierten Pfadnamen verwenden

    Pfadnamen werden unter den verschiedenen Betriebssystemen nicht nach einheitlichem Standard dargestellt. Die Verwendung von Pfadnamen schafft somit Plattformabhängigkeiten.

    Beispiel
    #include "somePath/filename.hh" // UNIX
    #include "somePath\filename.hh" // MSDOS      

    Datendarstellung

    Die Darstellung und Ausrichtung von Typen ist sehr stark von der Maschinenarchitektur abhängig. Annahmen hinsichtlich der Darstellung und Ausrichtung können zu bösen Überraschungen führen und die Portierbarkeit beeinträchtigen.

    Zeigefingersymbol Keine Typdarstellung voraussetzen

    Versuchen Sie insbesondere nie, einen Zeiger in einem int, long oder einem anderen numerischen Typ zu speichern, wenn Sie Wert auf Portierbarkeit legen.

    Zeigefingersymbol Keine Typausrichtung voraussetzen

    Zeigefingersymbol Keine Abhängigkeit von einem bestimmten Unterlauf- oder Überlaufverhalten erzeugen

    Handsymbol OK Nach Möglichkeit "dehnbare" Konstanten verwenden

    Dehnbare Konstanten helfen, Probleme mit variierenden Wortgrößen zu vermeiden.

    Beispiel
    const int all_ones = ~0;
    const int last_3_bits = ~0x7;     

    Typkonvertierungen

    Zeigefingersymbol Keine Konvertierung von einem "kurzen" in einen "langen" Typ

    Die Maschinenarchitektur kann die Ausrichtung bestimmter Typen vorgeben. Wenn Typen mit weniger strikten Ausrichtungsanforderungen in Typen mit sehr strikten Ausrichtungsanforderungen konvertiert werden, können Programmfehler auftreten.


    Kapitel 9

    Wiederverwendung

    Dieses Kapitel enthält eine Anleitung für die Wiederverwendung von C++-Code.

    Handsymbol OK Nach Möglichkeit Standardbibliothekskomponenten verwenden

    Falls die Standardbibliotheken nicht verfügbar sind, erstellen Sie Klassen, die auf den Schnittstellen der Standardbibliotheken basieren, um eine spätere Migration zu erleichtern.

    Handsymbol OK Schablonen für die Wiederverwendung von datenunabhängigem Verhalten nutzen

    Nutzen Sie Schablonen für die Wiederverwendung von Verhalten, das nicht von einem bestimmten Datentyp abhängig ist.

    Handsymbol OK Öffentliche Vererbung für die Wiederverwendung von Klassenschnittstellen (Subtyping) nutzen

    Nutzen Sie die öffentliche Vererbung, um die "isa"-Beziehung auszudrücken und Schnittstellen von Basisklassen sowie optional deren Implementierung wiederzuverwenden.

    Handsymbol OK Für die Wiederverwendung von Klassenimplementierungen den Einschluss an Stelle der privaten Vererbung nutzen

    Vermeiden Sie bei der Wiederverwendung von Implementierungen oder beim Modellieren von "Pars-Totum-Beziehungen" die private Vererbung. Eine Implementierung lässt sich ohne Neudefinition am besten durch Einschluss wiederverwenden.

    Nutzen Sie die private Vererbung, wenn Basisklassenoperationen neu definiert werden müssen.

    Handsymbol OK Mehrfachvererbung mit Vorsicht verwenden

    Die Mehrfachvererbung sollte mit Vorsicht angewendet werden, denn sie erhöht in starkem Maße die Komplexität. Die Veröffentlichung [Meyers 1992] beschäftigt sich ausführlich mit der Komplexität aufgrund der potenziellen Mehrdeutigkeit von Namen und der wiederholten Vererbung. Komplexität kann durch Folgendes entstehen:

    Mehrdeutigkeiten, wenn mehrere Klassen dieselben Namen verwendet. Alle nicht qualifizierten Verweise auf die Namen sind inhärent mehrdeutig. Sie können die Mehrdeutigkeit auflösen, indem Sie die Elementnamen mit ihren Klassennamen qualifizieren. Dies hat jedoch den unerfreulichen Effekt, dass keine Polymorphie mehr möglich ist und aus virtuellen Funktionen statisch gebundene Funktionen werden.

    Bei wiederholter Vererbung mehrerer Gruppen von Datenelementen von einer Basis (mehrfaches Vererben einer Basisklasse durch eine abgeleitete Klasse über verschiedene Pfade der Vererbungshierarchie) entsteht die Frage, welche der verschiedenen Gruppen von Datenelementen zu verwenden ist.

    Mehrfach vererbte Datenelemente können Sie mit virtueller Vererbung (Vererbung virtueller Basisklassen) vermeiden. Sollte die virtuelle Vererbung dann nicht generell verwendet werden? Die virtuelle Vererbung hat die negative Auswirkung, dass die zugrunde liegende Objektdarstellung geändert und der Zugriff weniger effizient wird.

    Eine Richtlinie, die generell eine virtuelle Vererbung vorschreibt und gleichzeitig jeglichen Platz- oder Zeitverlust ahndet, wäre viel zu rigide.

    Für die Mehrfachvererbung braucht es vorausschauende Klassendesigner, die eine klare Vorstellung von der künftigen Verwendung der Klassen haben und die Entscheidung für oder gegen die virtuelle Vererbung selbst treffen können.


    Kapitel 10

    Kompilierungsprobleme

    Dieses Kapitel enthält Orientierungshilfen für Kompilierungsprobleme.

    Handsymbol OK Kompilierungsabhängigkeiten minimieren

    Schließen Sie in eine Modulspezifikation keine anderen Header-Dateien ein, die nur für die Implementierung des Moduls erforderlich sind.

    Vermeiden Sie den Einschluss von Header-Dateien in eine Spezifikation mit dem Ziel der Transparenz für andere Klassen, wo nur die Transparenz von Zeigern oder Referenzen erforderlich ist. Verwenden Sie lieber Forward-Deklarationen.

    Beispiel
    // Spezifikation für Modul A in der Datei "A.hh" enthalten
    #include "B.hh" // Nicht einschließen, wenn nur für die
                    // Implementierung erforderlich
    #include "C.hh" // Nicht einschließen, wenn nur als Verweis erforderlich;
                    // stattdessen eine Forward-Deklaration verwenden
    class C;
    class A
    {
    C* a_c_by_reference; // 'has-a'-Beziehung per Referenz
    };
    // Ende von "A.hh"      
    Anmerkungen

    Für die Minimierung der Kompilierungsabhängigkeiten wurden bestimmte Designidiome oder -muster entwickelt, die als Handle oder Envelop (siehe [Meyers 1992]) oder auch als Brückenklassen (siehe [Gamma]) bezeichnet werden. Wenn Sie die Zuständigkeit für eine Klassenabstraktion so auf zwei verknüpfte Klassen verteilen, dass eine Klasse die Schnittstelle und die andere die Implementierung bereitstellt, minimieren Sie die Abhängigkeiten zwischen einer Klasse und ihren Clients, denn die Clients müssen bei Änderungen an der Implementierung (Implementierungsklasse) nicht mehr neu kompiliert werden.

    Beispiel
    // Spezifikation für Modul A in der Datei "A.hh" enthalten
    class A_implementation;
    class A
    {
    A_implementation* the_implementation;
    };
    // Ende von "A.hh"      

    Bei diesem Ansatz können Sie die Schnittstellenklasse und die Implementierungsklasse auch als zwei gesonderte Klassenhierarchien angeben.

    Zeigefingersymbol Symbol NDEBUG Symbol NDEBUG mit bestimmtem Wert definieren

    Das Symbol NDEBUG wird traditionell für das Herauskompilieren von Zusicherungscode verwendet, der mit dem Makro assert implementiert wurde. Nach dem herkömmlichen Verwendungsparadigma wird das Symbol definiert, zum Zusicherungen zu eliminieren. Wenn sich der Entwickler jedoch nicht über das Vorhandensein von Zusicherungen im Klaren ist, wird er das Symbol nicht definieren. Dieser Fall kommt häufig vor.

    Wir favorisieren daher die template-Version von assert, bei der dem Symbol NDEBUG ein expliziter Wert zugeordnet wird. Dieser Wert ist 0, wenn der Zusicherungscode erwünscht ist, und ungleich null, wenn der Code eliminiert werden soll. Wird Zusicherungscode kompiliert, ohne dass dem Symbol NDEBUG ein bestimmter Wert zugeordnet ist, werden Kompilierungsfehler generiert. Diese machen dem Entwickler deutlich, dass Zusicherungscode vorhanden ist.


    Zusammenfassung der Richtlinien

    Nachfolgend sind alle bisher vorgestellten Richtlinien noch einmal zusammengefasst.

    Zeigefingersymbol Anforderungen oder Einschränkungen

    Gesunden Menschenverstand verwenden
    Immer mit #include auf eine Modulspezifikation zugreifen
    Keine Namen deklarieren, die mit Unterstreichungszeichen ('_') beginnen
    Globale Deklarationen ausschließlich auf Namespaces beschränken
    Für Klassen mit explizit deklarierten Konstruktoren immer einen Standardkonstruktor angeben
    Copy-Konstruktoren und Zuordnungsoperatoren für Klassen mit Datenelementen vom Typ Zeiger immer deklarieren
    Konstruktorparameter nie neu deklarieren, um einen Standardwert zu erhalten
    Destruktoren immer als virtuell deklarieren
    Nicht virtuelle Funktionen nie erneut definieren
    Elementfunktionen nie von einer Initialisierungsroutine des Konstruktors aufrufen lassen
    Keine Verweise auf lokale Objekte zurückgeben
    Nie einen mit new initialisierten dereferenzierten Zeiger zurückgeben
    Keinen nicht konstanten Verweis oder Zeiger auf Elementdaten zurückgeben
    Operator operator= einen Verweis auf *this zurückgeben lassen
    Operator operator= auf Selbstzuordnung überprüfen lassen
    Konstanz eines konstanten Objekts immer bewahren
    Keine bestimmte Auswertungsreihenfolge für Ausdrücke voraussetzen
    Keine explizite Typumwandlung im alten Stil
    Für Boolesche Ausdrücke neuen Typ bool verwenden
    Keine direkten Vergleiche mit dem Booleschen Wert true
    Zeiger nie mit Objekten außerhalb desselben Arrays vergleichen
    Einem gelöschten Objektzeiger immer einen Nullzeigerwert zuordnen
    Zum Abfangen von Fehlern in switch-Anweisungen immer eine default-Verzweigung angeben
    Keine Anweisung goto verwenden
    Speicheroperationen von C nicht mit Speicheroperationen von C++ mischen
    Mit new erstellte Array-Objekte immer mit delete[] löschen
    Keine fest codierten Pfadnamen verwenden
    Keine Typdarstellung voraussetzen
    Keine Typausrichtung voraussetzen
    Keine Abhängigkeit von einem bestimmten Unterlauf- oder Überlaufverhalten erzeugen
    Keine Konvertierung von einem "kurzen" in einen "langen" Typ
    Symbol NDEBUG Symbol NDEBUG mit bestimmtem Wert definieren

    Handsymbol OK Empfehlungen

    Modulspezifikationen und -implementierungen in separaten Dateien speichern
    Gruppe von Dateinamenerweiterungen auswählen, um Header von Implementierungsdateien unterscheiden zu können
    Nach Möglichkeit nur eine Klasse pro Modulspezifikation definieren
    Möglichst keine implementierungsinternen Deklarationen in Modulspezifikationen aufnehmen
    Definitionen von Inline-Funktionen eines Moduls in eine gesonderte Datei stellen
    Große Module bei problematischer Programmgröße in mehrere Umsetzungseinheiten unterteilen
    Plattformabhängigkeiten eingrenzen
    Schutz vor wiederholtem Einschließen von Dateien
    Inline-Kompilierung mit dem Symbol "No_Inline" für bedingte Kompilierung unterbinden
    Für verschachtelte Anweisungen Stil mit kleiner konsistenter Einrückung verwenden
    Funktionsparameter unter dem Funktionsnamen oder Namen des Geltungsbereichs einrücken
    Maximale Zeilenlänge an das Standardpapierformat für Druckausgabe anpassen
    Konsistentes Line-Folding anwenden
    Kommentare im C++-Stil angeben
    Kommentare sehr nah am Quellcode platzieren
    Kommentare am Zeilenende vermeiden
    Kommentare mit Kopfdaten vermeiden
    Kommentarabsätze durch eine leere Kommentarzeile trennen
    Redundanz vermeiden
    Selbstdokumentierender Code ist Kommentaren vorzuziehen
    Klassen und Funktionen dokumentieren
    Namenskonvention auswählen und konsistent anwenden
    Typennamen vermeiden, die sich nur durch die Groß-/Kleinschreibung unterscheiden
    Abkürzungen vermeiden
    Suffixe für Sprachkonstrukte vermeiden
    Verständliche, lesbare und aussagekräftige Namen wählen
    Korrekte Rechtschreibung der Namen beachten
    Bejahende Prädikatklausel für Boolesche Objekte, Funktionen und Argumente verwenden
    Namespaces zur Partitionierung potenzieller Globalnamen in Subsysteme oder Bibliotheken verwenden
    Substantive oder substantivische Ausdrücke für Klassennamen verwenden
    Verben für Namen von prozeduralen Funktionen verwenden
    Funktionsüberladung bei identischer allgemeiner Bedeutung verwenden
    Bedeutung von Namen durch grammatikalische Elemente hervorheben
    Für Ausnahmen Namen mit negativer Bedeutung wählen
    Projektdefinierte Adjektive für die Namen von Ausnahmen verwenden
    Großbuchstaben für Gleitkommaexponenten und Hexadezimalziffern verwenden
    Namespace zum Gruppieren von Nichtklassenfunktionen verwenden
    Verwendung von globalen Bereichsdaten und Namespace-Bereichsdaten minimieren
    Für die Implementierung abstrakter Datentypen class statt struct verwenden
    Klassenelemente absteigend nach Zugriffsmöglichkeit deklarieren
    Deklaration öffentlicher oder geschützter Datenelemente für abstrakte Datentypen vermeiden
    Kapselung mit Freunden schützen
    Möglichst keine Funktionsdefinitionen in Klassendeklarationen angeben
    Deklaration zu vieler Umsetzungsoperatoren und nur eines Parameterkonstruktors vermeiden
    Nicht virtuelle Funktionen mit Vernunft verwenden
    Initialisierungsroutine des Konstruktors an Stelle von Zuordnungen im Konstruktor verwenden
    Vorsicht beim Aufruf von Elementfunktionen in Konstruktoren/Destruktoren
    Verwendung von static const für integrale Klassenkonstanten
    Für Funktionen immer einen expliziten Rückgabetyp deklarieren
    In Funktionsdeklarationen immer formale Parameternamen angeben
    Funktionen mit nur einem Rückkehrpunkt anstreben
    Keine Funktionen mit globalen Nebeneffekten erstellen
    Funktionsparameter absteigend nach ihrer Wichtigkeit und Volatilität deklarieren
    Deklaration von Funktionen mit variabler Parameteranzahl vermeiden
    Erneutes Deklarieren von Funktionen mit Standardparametern vermeiden
    Verwendung von const in Funktionsdeklarationen maximieren
    Übergabe von Objekten in Form von Werten vermeiden
    Für Makroerweiterung bevorzugt Inline-Funktionen statt #define verwenden
    Standardparameter anstelle einer Funktionsüberladung nutzen
    Allgemeine Semantik mit Funktionsüberladung ausdrücken
    Keine Funktionen überladen, die Zeiger und Integer verwenden
    Komplexität minimieren
    Verwendung fundamentaler Typen vermeiden
    Verwendung von Literalwerten vermeiden
    Vorprozessordirektive #define nicht zum Definieren von Konstanten verwenden
    Objekte nahe am ersten Verwendungspunkt deklarieren
    Konstante Objekte immer beim Deklarieren initialisieren
    Objekte beim Definieren initialisieren
    Für die Verzweigung Boolescher Ausdrücke eine if-Anweisung verwenden
    Für die Verzweigung diskreter Werte eine switch-Anweisung verwenden
    Bei erforderlichem Schleifentest vor einer Iteration eine for- oder while-Anweisung verwenden
    Bei erforderlichem Schleifentest nach einer Iteration eine do-while-Anweisung verwenden
    Sprunganweisungen in Schleifen vermeiden
    Verdecken von Bezeichnern in verschachtelten Bereichen vermeiden
    Zusicherungen im Interesse der Fehlererkennung liberal handhaben
    Ausnahmen nur für echte Ausnahmebedingungen verwenden
    Projektausnahmen von Standardausnahmen ableiten
    Anzahl der Ausnahmen für eine gegebene Abstraktion minimieren
    Alle ausgelösten Ausnahmen deklarieren
    Ausnahmebehandlungsroutinen absteigend nach ihrer Entfernung von der Basisklasse definieren
    Universelle Ausnahmebehandlungsroutinen vermeiden
    Geeigneten Wert für Funktionsstatuscodes sicherstellen
    Sicherheitsprüfungen lokal durchführen und nicht dem Client überlassen
    Nach Möglichkeit "dehnbare" Konstanten verwenden
    Nach Möglichkeit Standardbibliothekskomponenten verwenden

    TippsymbolTipps

    Globale Systemtypen für das gesamte Projekt definieren
    Synonyme zur Stärkung der lokalen Bedeutung mit typedef erstellen
    Verbundausdrücke mit paarigen runden Klammern verdeutlichen
    Zu tiefe Verschachtelung von Ausdrücken vermeiden
    Für Nullzeiger 0 statt NULL verwenden
    Ausnahmen beim ersten Auftreten dokumentieren


    Bibliographie

    [Cargill 92] Cargill, Tom, 1992, C++ Programming Styles, Addison-Wesley

    [Coplien 92] Coplien, James O., 1992, Advanced C++ Programming Styles and Idioms, Addison-Wesley

    [Ellemtel 93] Ellemtel Telecommunications Systems Laboratories, Juni 1993, Programming in C++ Rules and Recommendations

    [Ellis 90] Ellis, Margaret A. und Stroustrup, Bjarne, 1990, The Annotated C++ Reference Manual, Addison-Wesley

    [Kruchten 94] Kruchten, P., Mai 1994, Ada Programming Guidelines for the Canadian Automated Air Traffic System

    [Lippman 96] Lippman, Stanley B., 1996, Inside the C++ Object Model, Addison-Wesley

    [Meyers 92] Meyers, Scott, 1992, Effective C++, Addison-Wesley

    [Meyers 96] Meyers, Scott, 1996, More Effective C++, Addison-Wesley

    [Plauger 95] Plauger, P. J., 1995, The Draft Standard C++ Library, Prentice Hall, Inc.

    [Plum 91] Plum, Thomas und Saks, Dan, 1991, C++ Programming Guidelines, Plum Hall Inc.

    [Stroustrup 94] Stroustrup, Bjarne, 1994, The Design and Evolution of C++, Addison-Wesley

    [X3J16 95] X3J16/95-0087 | WG21/N0687, April 1995, Working Paper for Draft Proposed International Standard for Information Systems-Programming Language C++