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.
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:
Die hier vorgestellten Richtlinien gehen von einigen wenigen Grundvoraussetzungen aus:
Die Richtlinien sind nicht alle gleich wichtig, sondern können auf der folgenden Skala eingestuft werden:
Eine mit dem obigen Symbol gekennzeichnete Richtlinie ist ein Tipp oder ein Ratschlag, den Sie nach Belieben annehmen oder ausschlagen können.
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.
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.
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.
Dieses Kapitel enthält eine Anleitung zur Programmstruktur und zum Layout.
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.
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:
File_Name::=
<Modulname> [<Trennzeichen> <Abschnittsname>] '.' <Dateierweiterung>
Beispielschema für Modulpartitionierung und -benennung:
inlines
aufnehmen
(sieh "Stellen Sie Definitionen für Inline-Funktionen von Modulen in gesonderte Dateien.").Modul.Test
genannt werden. Das Deklarieren des Testcodes als Freundklasse vereinfacht die unabhängige Entwicklung von Modul und Testcode und ermöglicht den Ausschluss des Testcodes aus dem endgültigen
Modulobjektcode ohne Quellcodeänderungen. SymaNetwork.hh // Enthält eine Deklaration für eine // Klasse mit dem Namen "SymaNetwork". SymaNetwork.Inlines.cc // Untergeordnete Einheit mit integrierten Definitionen SymaNetwork.cc // Hauptimplementierungseinheit des Moduls SymaNetwork.Private.cc // Private untergeordnete Implementierungseinheit SymaNetwork.Test.cc // Untergeordnete Einheit mit Testcode
Die Trennung der Modulspezifikation von der Implementierung des Moduls vereinfacht eine unabhängige Entwicklung von Benutzer- und Supplier-Code.
Wenn die Implementierung des Moduls in mehrere Umsetzungseinheiten unterteilt wird, kann Objektcode leichter entfernt werden, was zu kleineren ausführbaren Einheiten führt.
Bei Verwendung einer regulären und vorhersagbaren Namens- und Partitionierungskonvention lassen sich Inhalt und Organisation eines Moduls ohne Prüfung des eigentlichen Inhalts erkennen.
Das Aufgreifen der Namen im Code für Dateinamen erhöht die Voraussagbarkeit und vereinfacht die Erstellung dateibasierter Tools, da keine komplexe Namenszuordnung erforderlich ist [Ellemtel 1993].
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.
SymaNetwork.hh // Die Erweiterung ".hh" bezeichnet einen // Header des Moduls "SymaNetwork". SymaNetwork.cc // Die Erweiterung ".cc" bezeichnet eine // Implementierung des Moduls "SymaNetwork".
Im Arbeitspapier zum C++-Normentwurf ist für Header, die in einem Namespace gekapselt sind, außerdem die Erweiterung ".ns" angegeben.
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.
Die Schnittstelle eines Moduls wird verkleinert und die Abhängigkeiten von dieser Schnittstelle werden reduziert.
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:
// 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"
#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").
// 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.
#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
Durch diese Regel wird Folgendes sichergestellt:
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".
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.
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].
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].
Es könnte lohnend sein, zuerst darüber nachzudenken, ob das Modul nicht in kleinere Abstraktionen unterteilt werden sollte.
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.
SymaLowLevelStuff.hh // Spezifikation // "LowLevelStuff" SymaLowLevelStuff.SunOS54.cc // SunOS-5.4-Implementierung SymaLowLevelStuff.HPUX.cc // HP-UX-Implementierung SymaLowLevelStuff.AIX.cc // AIX-Implementierung
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:
1. Schutz vor wiederholtem Einschluss (nur Spezifikation)
2. Optionale Angabe der Datei- und Versionssteuerung
3. Für diese Einheit erforderliche Dateieinschlüsse
4. Moduldokumentation (nur Spezifikation)
5. Deklarationen (Klasse, Typ, Konstanten und Funktionen) und zusätzliche Textspezifikationen (Vor- und Nachbedingungen, Invarianten)
6. Einschließen der Definitionen für die Inline-Funktionen dieses Moduls
7. Definitionen (Objekte und Funktionen) und implementierungsinterne Deklarationen
8. Copyrightvermerk
9. Optionales Verlaufsprotokoll der Versionssteuerung
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.
Möglicherweise muss der Copyrightvermerk am Anfang der Datei stehen, wenn es eine entsprechende unternehmensinterne Richtlinie gibt.
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.
No_Inline
" für bedingte Kompilierung unterbindenVerwenden 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.
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.
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.
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.
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: //... }
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.
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.
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.
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.
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.
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.
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").
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);
Dieses Kapitel enthält Anleitungen für die Verwendung von Kommentaren im Code.
Kommentare sollten den Quellcode ergänzen und nicht umschreiben:
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.
Verwenden Sie anstelle des C-Begrenzers für Kommentare "/*...*/
" den Begrenzer von C++ "//
".
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.
/* 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();
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.
// Kommentar mit Vorbemerkungen, der für // mehrere nachfolgende Anweisungen gilt // ... void function(); // // Ein Kommentar unter der Anweisung, der sich // auf die vorherige Anweisung bezieht.
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.
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].
Trennen Sie die einzelnen Absätze eines Kommentarblocks nicht durch Leerzeilen, sondern durch leere Kommentarzeilen.
// 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.
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.
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.
do { ... } while (string_utility.locate(ch, str) != 0); // Suchschleife nach erfolgreicher Suche verlassenVerwenden Sie stattdessen diesen Code:
do { ... found_it = (string_utility.locate(ch, str) == 0); } while (!found_it);
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:
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.
Dieses Kapitel enthält Anleitungen für die Auswahl von Namen für verschiedene C++-Entitäten.
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.
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.
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.
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.
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.
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).
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:
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.
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.
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.
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.
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
.
Prädikate können verneint werden, so dass eine doppelte Verneinung entstehen würde, die schwerer verständlich ist.
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.
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.
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:
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.
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.
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);
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.
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.
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);
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
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.
Die Buchstaben 'E' in Gleitkommaliteralen und die Hexadezimalziffern 'A' bis 'F' sollten immer Großbuchstaben sein.
Dieses Kapitel enthält Anleitungen für die Verwendung und Form verschiedener C++-Deklarationsarten.
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.
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.
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.
namespace transport_layer_interface { /* ... */ }; namespace math_definitions { /* ... */ };
Die Verwendung von globalen Bereichsdaten und Daten aus dem Namespace-Geltungsbereich läuft dem Kapselungsprinzip zuwider.
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.
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.
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.
Die Zugriffskennungen in einer Klassendeklaration sollten in der Reihenfolge public, protected, private (öffentlich, geschützt, privat) erscheinen.
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.
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.
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.
Klassendeklarationen sollten nur Funktionsdeklarationen und keine Funktionsdefinitionen (Implementierungen) enthalten.
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").
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.
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.
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.
// 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.
// 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.
// 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) { ... }
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].
Der Destruktor einer Klasse sollte immer als virtuell deklariert werden, solange die Klasse nicht explizit als nicht ableitbar konzipiert ist.
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.
// 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 }
Die Verwendung eines einzelnen Parameterkonstruktors für die implizite Umsetzung können Sie auch vermeiden, indem Sie den Konstruktor mit dem Bezeichner
explicit
deklarieren.
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.
// 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 }
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.
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.
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
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.
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].
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].
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.
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
.
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]; };
Dadurch werden Unklarheiten verhindert, wenn der Compiler das Fehlen eines Rückgabetyps bei Funktionen moniert, die nicht mit einem expliziten Rückgabetyp deklariert sind.
Verwenden Sie dieselben Namen in Funktionsdeklarationen und Funktionsdefinitionen, um Überraschungen zu vermeiden. Die Angabe von Parameternamen verbessert die Codedokumentation und die Lesbarkeit des Codes.
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.
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.
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.
Argumente für Funktionen mit einer variablen Anzahl von Parametern können nicht auf ihren Typ überprüft werden.
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.
Ü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.
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.
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);
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.
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!
Lokale Objekte werden beim Verlassen des Geltungsbereichs der Funktion zerstört. Bei Verwendung zerstörter Objekte ist die Katastrophe vorprogrammiert.
Die Nichteinhaltung dieser Richtlinie führt zu Speicherlecks.
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.
Die Nichteinhaltung dieser Richtlinie führt zu einer Verletzung der Datenkapselung und kann böse Überraschungen nach sich ziehen.
#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:
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
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.
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.
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*)
.
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].
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.
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.
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").
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.
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 ... }
Die Darstellung fundamentaler Typen ist von der Implementierung abhängig.
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.
// 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.
Geben Sie benannten Konstanten den Vorzug.
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 };
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.
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.
Konstante Objekte können im Nur-Lese-Speicher vorhanden sein.
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.
Dieses Kapitel enthält Anleitungen für die Verwendung und Form verschiedener C++-Ausdrücke und -Anweisungen.
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.
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.
foo(i, i++); array[i] = i--;
NULL
verwendenDie 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.
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:
dynamic_cast
- Umwandlung von Elementen derselben Klassenhierarchie (Subtypen) mit Laufzeitinformationen (die für Klassen mit virtuellen Funktionen verfügbar sind). Die explizite Typumwandlung zwischen
solchen Klassen ist garantiert sicher. static_cast
- Umwandlung von Elementen derselben Klassenhierarchie ohne Laufzeitinformationen. Die Sicherheit dieser expliziten Typumwandlung ist nicht garantiert. Wenn der Programmierer
die Typsicherheit nicht garantieren kann, sollte dynamic_cast
verwendet werden. reinterpret_cast
- Umwandlung von nicht zusammengehörigen Zeigertypen und Integertypen (integral). Diese explizite Typumwandlung ist nicht sicher und sollte nur zwischen den genannten
Typen ausgeführt werden. const_cast
- Aufhebung der Konstantheit eines Funktionsarguments, dass als
const
-Parameter angegeben ist. Mit const_cast
soll nicht die Konstanz eines Objekts aufgehoben werden, dass wirklich als konstantes Objekt definiert ist (und im Nur-Lese-Speicher enthalten sein könnte). 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.)
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 () { } }
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.
Verwenden Sie nicht die alte Form Boolescher Makros oder Konstanten. Es gibt keinen Booleschen Standardwert true.
Verwenden Sie den neuen Typ bool
.
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.
Vermeiden Sie Angaben wie diese:
if (someNonZeroExpression == true) // Wird möglicherweise nicht als true ausgewertetVerwenden Sie besser diese Angabe:
if (someNonZeroExpression) // Wird immer als wahre Bedingung (true) ausgewertet
Das Ergebnis solcher Operationen ist in den seltensten Fällen aussagekräftig.
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.
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.
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.
Wenn die Iteration und die Beendigung der Schleife anhand eines Schleifenzählers erfolgt, sollten Sie eher eine for-Anweisung als eine while-Anweisung verwenden.
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.
goto
verwenden Dies ist eine allgemein gültige Richtlinie.
Dies kann zur Verunsicherung der Leser und zu potenziellen Risiken bei der Wartung führen.
Dieses Kapitel enthält Anleitungen für die Speicherverwaltung und die Erstellung von Fehlerberichten.
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.
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.
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.
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).
template<class T, class Exception> inline void assert ( T a_boolean_expression, Exception the_exception) { if (! NDEBUG) if (! a_boolean_expression) throw the_exception; }
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.
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.
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.
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.
Dokumentieren Sie beim Entwickeln Ausnahmen und den Auslösepunkt so früh wie möglich mit dem entsprechenden Protokollierungsmechanismus.
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.
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! { ... }
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; }
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.
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.
Dieses Kapitel Abschnitt beschäftigt sich mit Sprachfeatures, die an sich nicht portierbar sind.
Pfadnamen werden unter den verschiedenen Betriebssystemen nicht nach einheitlichem Standard dargestellt. Die Verwendung von Pfadnamen schafft somit Plattformabhängigkeiten.
#include "somePath/filename.hh" // UNIX #include "somePath\filename.hh" // MSDOS
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.
Versuchen Sie insbesondere nie, einen Zeiger in einem int
,
long oder einem anderen numerischen Typ zu speichern, wenn Sie Wert auf Portierbarkeit legen.
Keine Abhängigkeit
von einem bestimmten Unterlauf- oder Überlaufverhalten erzeugen
Nach Möglichkeit "dehnbare" Konstanten verwenden
Dehnbare Konstanten helfen, Probleme mit variierenden Wortgrößen zu vermeiden.
const int all_ones = ~0; const int last_3_bits = ~0x7;
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.
Dieses Kapitel enthält eine Anleitung für die Wiederverwendung von C++-Code.
Falls die Standardbibliotheken nicht verfügbar sind, erstellen Sie Klassen, die auf den Schnittstellen der Standardbibliotheken basieren, um eine spätere Migration zu erleichtern.
Nutzen Sie Schablonen für die Wiederverwendung von Verhalten, das nicht von einem bestimmten Datentyp abhängig ist.
Nutzen Sie die öffentliche Vererbung, um die "isa"-Beziehung auszudrücken und Schnittstellen von Basisklassen sowie optional deren Implementierung wiederzuverwenden.
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.
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.
Dieses Kapitel enthält Orientierungshilfen für Kompilierungsprobleme.
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.
// 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"
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.
// 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.
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.
Nachfolgend sind alle bisher vorgestellten Richtlinien noch einmal zusammengefasst.
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
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
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