Programming with C++
Top1 Coding Standards
Quellen:
C++ Coding Standards, Sutter/Alexandrescu
C++ Coding Standards, Sutter/Alexandrescu (Safari-Books)
Eigene Anmerkungen und Beispiele sind ohne weitere Kennzeichnung enthalten
Top1.1 Grundregeln
Top1.1.1 Lesbarkeit, Verständlichkeit
- Hauptziel: Programme müssen so geschrieben werden, dass Menschen sie leicht verstehen
können.
- Lesbarkeit des Quellcodes
- keine zu langen Zeilen (höchstens 10 Wörter)
- falls Tabs erlaubt sind, dafür sorgen, dass Code in allen verwendeten Tools
lesbar bleibt (unterschiedliche Editoren/Viewer stellen Tabs unterschiedlich dar)
einfache Alternative: Editor fügt bei Betätigung der TAB-Taste Leerzeichen ein
- niemals "underscore" als Wortanfang verwenden, niemals doppeltes "underscore" als
Wortbestandteil (siehe Sutter Item 0)
- Nicht verwenden: Ungarische Notation (Typinformnation ist Teil des Namens),
bietet in modernen Entwicklungsumgebungen keine Vorteile mehr, ist
nicht zu realisieren in generischer Programmierung (Templates)
- magische Zahlen im Code vermeiden, benannte Konstanten verwenden:
// MyClass.h
// Important domain specific constants at namespace level
const size_t MY_MAX_LEN = 2345;
// Class specific constants
class MyClass
{
// constant integral value can be directly provided within header
static const int MY_INT = 333;
// constant non integral value is provided within cpp file
static const double MY_DOUBLE;
// providing non integral value directly through function,
// no additional entry within cpp file needed
static const char* MyName() {return "AnyString";}
};
// MyClass.cpp
const int MyClass::MY_INT; // definition here is required!
// for const value see header
const double MyClass::MY_DOUBLE = 2.34;
Top1.1.2 Sicheres Programmieren
- compile with high warning level
Third-Party-Libs mit vielen Warnungen können folgendermaßen eingebunden werden:
// File: MyThirdPartyInclude.h
// always include this file and not the third party include directly
#pragma warning(push) // disbale for this header only
#pragma warning(disable:4512) // disable specific warning
#include "ThirdPartyInclude.h"
#pragma warning(pop) // restore original warning level
- const "überall" einsetzen
Als Default sollten Funktionen und Funktions-Inputparameter als const deklariert werden.
Ausnahme: Funktionsparameter vom Typ pass-by-value sind immer const. Die Angabe "const"
sollte hier entfallen.
Neben Funktionsparametern sollten auch lokale Variablen, die einmalig
ermittelt werden, als const definiert werden:
void DoSomething()
{
int const LENGTH = CalculateLength();
// ... now we know: length will remain unchanged
// throughout the whole function
}
- RAII (resource acquisition is initialization)
Einsatz der Constructor/Destruktor-Symmetrie für den Erwerb und die Freigabe von
Ressourcen (fopen/fclose, lock/unlock, new/delete).
class MyResource // may be file, port, ...
{
// constructor allocates resource (e.g. open file)
MyResource(const string & in_identification);
// destructor frees resource (e.g. close file)
~MyResource();};
// Example: using as automated stack object
void DoSomething()
{
MyResource myResource("NameOfResource");
//...
// use resource
//...
} // at end of scope resource is freed automatically
// Refcounted resource handling
shared_ptr<MyResource> resource = new MyResource("SomeResource");
// Resource is freed when the last shared pointer referring to it goes away
Zur Vermeidung von Problemen bei
möglicherweise auftretenden Exceptions in jeder Zeile Code / in jedem Statement
nur maximal eine Resource allokieren und dem Verwaltungsobjekt zuweisen
(sonst könnten mehrere Resourcen erzeugt werden und noch bevor sie an die zugehörigen
Resourcemanagementobjekte übergeben werden, könnte eine Exception das korrekte Verwalten
und abschliessende Aufräumen unterbinden)
Oft macht das Kopieren der Verwaltungsobjekte keinen Sinn, für diese Fälle ist der
copy-Constructor /Assignment operator über eine Deklaration als private zu deaktivieren.
- Einsatz von assertions zur Aufdeckung interner Programmierfehler
- Variablen so lokal wie möglich definieren, stets initialisieren
- Switch-Konstrukte sollten immer den Default-Zweig enthalten, z.B. assert(0);
(bessere Vorbereitung auf spätere Erweiterung des Wertebereiches)
- varargs/ellipsis (variable Argumentliste) wegen der damit verbundenen Typunsicherheit
(und trotz ihrer oft besseren Lesbarkeit) nicht verwenden!
Eine typsichere Alternative ist die Boost format Library
- auf implizite Umwandlungen (implicit conversions) eher verzichten,
um unerwartete und tw. schwer durchschaubare Typumwandlungen zu vermeiden
mögliche Probleme: Aufwand zur Erzeugung temporärer Objekte,
versehentlicher Aufruf anderer Funktionen mit evtl. unerwarteter Signatur
Empfehlung: Konstruktoren, die ein einzelnes Argument
akzeptieren als explicit definieren,
erforderliche Umwandlungen besser über Konvertierungsfunktionen
zur Verfügung stellen (asChar(), toLong())
- Im Konstruktor alle Daten stets in der Initializerliste und in
der Reihenfolge ihrer Definition initialisieren
- Code-Reviews durchführen: Probleme erkennen, Reviews sind eine sehr
gute und billige Methode eines in-house-trainings
Top1.1.3 Performance
Top1.1.4 Mögliche Klassentypen
- Value class
- wird als konkrete Klasse in der Rolle eines Wertes verwendet, nicht
als Basisklasse, Beispiele: std::pair, std::vector
- public constructor und destructor
- keine virtuellen Funktionen
- Base class
- public virtual destructor oder nonpublic non virtual destructor
- non public copy constructor and assignment operator (Kopieren ist i.d.R. nicht erwünscht)
- oft auf dem Heap erzeugt, Zugriff über (smart) Pointer
- Traits class
- template mit Informationen zu Typen
- nur typedefs und statische Funktionen
- kein Zustand, keine virtuellen Methoden
- normalerweise nicht instanziiert
- Policy class
- normalerweise Template-Klasse, die Verhalten zur Verfügung stellt,
das anderswo "eingesteckt" wird
- kann Zustand und virtuelle Methoden besitzen
- wird normalerweise als Basisklasse oder Member instanziiert
- Exception class
- thrown by value, catched by reference
- public destructor, no-fail constructor
- sollte sich von std::exception ableiten
Top1.1.5 Module und Dateien - Extern und static in Headerfiles
Ein Headerfile darf/soll
keine Definitionen mit "external
linkage" enthalten:
// within global namespace or some named namespace dont do that:
int someNumber;
string someString ("AnyContent");
void SomeFunction(){...};
Stattdessen sollen
nur die Deklarationen aufgeführt sein:
extern int someNumber;
extern string someString; // definition (string contents) see cpp file
void SomeFunction(); // extern not needed, definition (implementation body)
// see cpp file
Static-Definitionen beseitigen zwar Linker-Meldungen zu doppelt-definierten
Symbolen, führen aber zur Vervielfachung der entsprechenden CodeTeile und
ggf. auch zu gar nicht beabsichtigten logischen Effekten (gleichnamige statische Variable existiert für jedes Cpp-Modul als eigene Instanz und erlaubt keinen Zugriff von mehreren Modulen aus).
Negativbeispiel
static int someNumber;
static string someString("AnyContent");
static void SomeFunction(){...};
Empfehlung:
Im Headerfile keine static-Elemente auf Namespace-Level definieren.
Ausnahme: Inline-Funktionen haben external linkage, werden aber nur einmal angelegt.
Top1.2 Entwurfsziele
Top1.2.1 Erweiterung bestehender Funktionalitäten
Beziehung: is-implemented-in-terms-of
- nonmember Funktionen, die im gleichen namespace
wie die zu erweiternde Klasse definiert werden, Funktionalität muss dann über
die public Members der Klasse realisierbar sein,
geringfügig veränderte Syntax für Anwender:
statt
object.NewFunction
dann
NewFunction(object)
- Composition, d.h. die zu erweiternde Klasse wird als Attribut in
NewClass ergänzt, NewClass kann dann neue Funktionalität durch
Memberfunktionen anbieten, die intern das Attribut verwenden, bereits existierende
Funktionalität der zu erweiternden Klasse muss über forwarding Memberfunktionen
zur Verfügung gestellt werden.
- nonpublic inheritance, falls Zugriff auf protected members
der Basisklasse oder ein Überschreiben von Basisfunktionen erforderlich ist
Die ersten beiden Möglichkeiten bieten sich insbesondere für Klassen an, die nicht als
Basis vorgesehen sind.
Ziel:
neue Anforderungen sollen durch neuen Code realisiert werden,
der bestehende Code soll dabei möglichst nicht verändert werden
Top1.2.2 Unterstützung polymorpher Verwendung (public inheritance)
Beziehung: is-a, works-like-a, usable-as-a
Ziel:
Für den Anwender kann die neue Klasse wie die zugehörige Basisklasse angesteuert
werden. Üblicherweise erfolgt die Interaktion über einen Zeiger auf die Basisklasse.
Public inheritance muss das
Liskov Substitution Prinzip erfüllen, d.h. alle wesentlichen Verhaltensweisen der Basisklasse müssen erfüllt werden.
Jede überschriebene Basisfunktion
- darf nicht mehr Anforderungen/Preconditions stellen als die Basisklasse
- muss mindestens die Funktionalität der Basisklasse erfüllen
Top1.2.3 Überschreiben von Basisfunktionen (override, hide)
Sichtbarkeit beachten: Wird ein Name in einem Scope definiert, so verbirgt
er alle gleichlautenden Namen in allen untergeordneten Scopes.
Scope ist z.B namespace oder class. Die Parameterliste bei Funktionsnamen
spielt dabei keine Rolle.
class Base
{
virtual void SomeFunction(int);
virtual void SomeFunction(int,int);
void SomeFunction(int,int,int);
};
class Derived : public Base
{
virtual void SomeFunction(int); // overrides Base::SomeFunction
// and hides the other two base functions
};
Hiding durch eine using-Deklaration vermeiden:
class Derived : public Base
{
virtual void SomeFunction(int); // overrides Base::SomeFunction
using Base::SomeFunction; // make the other two base functions visible
};
Default-Argumente unverändert übernehmen (sonst werden je nach statischem Typ der
Verwendung unterschiedliche Werte eingesetzt).
Top1.2.4 Komposition ist besser als Vererbung (weniger Abhängigkeiten)
Funktionalität nicht über Basisklassen, sondern über Attribute zur Verfügung
zu stellen, bietet folgende Vorteile:
- beliebige Änderungsmöglichkeit, ohne den Client-Code zu beeinflussen (Client hat keinen
Zugriff auf private Datenmember)
- weniger Header-Abhängigkeiten, schnellere Compilezeit durch (shared) pointer Attribute,
da Typ der Attribute nur forward deklariert werden muss
- bei Anwendung des Pimpl-Idioms (siehe
Pimpl) ist
der Client-Code vollständig unabhängig von der Implementierung der Funktionalität
- Komposition erlaubt Erweiterung von Klassen, die nicht als Basisklasse vorgesehen
sind: zu erweiternde Klasse als Attribut verwenden und Methodenaufrufe an die
einbettende Klasse je nach Bedarf an das Attribut forwarden.
Top1.2.5 Reduktion von logischen Abhängigkeiten - information hiding
- alle Datenmember private machen
auf public und protected Datenmembers sollte verzichtet werden, da auf beide von
anderen Klassen "unkontrolliert" zugegriffen werden kann,
Mindestforderung: Get/Set-Methoden einführen
Ausnahme: reine Datenklassen (structs) ohne nennenswertes eigenes
Verhalten sollen weiterhin public Daten haben.
- keinen ändernden Zugriff auf interne Daten, z.B. über Herausgabe
von Handles/Zeigern auf Interna ermöglichen
- Funktionale Erweiterungen können bevorzugt auch über nonmember
Funktionen realisiert werden, wenn dazu sowieso nur der
Zugriff auf public-Elemente notwendig ist. Die nonmember Funktion kann
dann nicht vom private Anteil der Klasse abhängen.
Andere Motivationen für nonmember-Funktionen: linkes Argument
ist ungleich dem Klassentyp (z.B. bei Streamoperastoren) oder
soll implizite Typ-Umwandlungen unterstützen.
Bei Bedarf kann die Funktion zum nonmember friend der Klasse gemacht werden.
Virtuelles Verhalten durch Aufruf einer virtuellen Memberfunktion
aus der nonmember Funktion:
class MyBaseClass
{
public:
// pure base method to write contents to stream
virtual std::ostream& WriteToStream (std::ostream &) const = 0;
};
// non member and non virtual function,
// virtual behaviour comes from the second argument
std::ostream& operator<<(std::ostream & in_rStream, MyBaseClass const & in_rBase)
{
return in_rBase.WriteToStream (in_rStream);
}
Top1.2.6 Dependency Management - Minimierung der Abhängigkeiten zwischen Modulen
- Information hiding: interne Daten und Implementierungen verbergen,
Abhängigkeiten minimieren
- Include vermeiden, wenn forward-Deklaration ausreicht
- Jeder Header muss seinerseits alle erforderlichen Header enthalten, damit er alleine compiliert
- Beziehungen zwischen Klassen / SW-Modulen können oft durch abstrakte Beziehungen / Interfaces ersetzt werden
Top1.2.7 Pimpl - Vollständige Trennung Schnittstelle / Implementierung
Problem:
Durch die Deklaration von Daten und Methoden als private verhindert man
zwar den Zugriff durch Client-Code, es gibt jedoch folgende (oft
unerwünschten) Abhängigkeiten:
- Neucompilierung des Clientcodes ist erforderlich bei Änderungen an den
private Daten bzw. den inline-Implementierungen
- Der Client-Code benötigt alle für die private-Daten erforderlichen
Includes (für als Wert gehaltene Daten-Member und by-value-Funktionsparameter)
- C++ name lookup berücksichtigt auch nicht zugreifbare Methoden aus dem
private-Bereich, was zu Compilefehlern führen kann
(overload resolution wird for dem access check durchgeführt)
Lösung:
vollständige Trennung zwischen öffentlicher Schnittstelle und privater
Implementierung:
class MyClass
{
// public methods forwarding to implementation part
// ...
private:
struct MyImpl;
shared_ptr<MyImpl> m_pMyImpl;
};
Aufwand für Pimpl jedoch nur dann betreiben, wenn durch die Verkapselung ein tatsächlicher Vorteil entsteht.
Top1.3 Funktionen und Operatoren
Top1.3.1 Unsichere Evaluierungsreihenfolge für Argumente
Die
Reihenfolge der Evaluierung von Funktionsargumenten
ist nicht garantiert.
variable Ausdrücke, die als Argument übergeben werden, müssen unabhängig von der
Ausführungsreihenfolge das gleiche Ergebnis liefern.
Negativbeispiel:
SomeFunction (++n, ++n);
Top1.3.2 Geeignete Parametertypen
Input-Parameter
- primitive Typen (int, float, double, enum, einfache structs) "by value"
int in_numValues
- user defined types as "reference to const"
MyUserType const & in_rMyData
Output/InOut/(In)-Parameter
- als (smart) pointer falls das Argument optional ist
(Aufrufer kann dann null übergeben)
oder falls die Funktion eine Kopie des Zeigers speichert oder sonst die
Ownership beeinflußt
- als Referenz falls das Argument erforderlich ist
(Aufruf muss stets gültiges Objekt übergeben) und die Funktion
keinen Einfluss auf die Ownership ausübt
Top1.3.3 Operatoren
Im Zweifelsfall sind benannte Funktionen klarer in der Anwendung als benutzerdefinierte Operatoren (Im Falle von Operatoren stellt sich der Anwender sofort folgende Fragen: Können die Argumente vertauscht werden?
Gibt es verwandte oder inverse Operatoren?)
Kanonische Form für Arithmetische/Assignment Operatoren
Ein Operator @ (Platzhalter für +,-,*,/) sollte stets folgende Eigenschaften erfüllen:
Top1.4 Constructor, Destruktor, Copy
Top1.4.1 Klassenspezifisches new
Wird ein klassenspezifischer operator new realisiert, so müssen
dabei alle new-Formen realisiert werden, um ein hiding der
Defaultimplementierungen zu vermeiden.
// plain new
void* operator new (std::size_t);
// nothrow new
void* operator new (std::size_t, std::nothrow_t) throw();
// in-place new
// (is often used by STL containers)
void* operator new (std::size_t, void*)
Hat bereits die Basisklasse einen spezifischen new-Operator, so müssen
abgeleitete Klassen diesen verfügbar machen:
class Derived : public Base
{
public:
using Base::operator new;
};
Top1.4.2 Virtuelle Funktionen in Konstruktoren und Destruktoren
Innerhalb von Konstruktoren und Destruktoren ist der dynamische
Klassentyp auf die unmittelbare Klasse beschränkt, d.h. ein Aufruf
einer virtuellen Methode Base::SomeVirtualFunction() wird immer
die Methode der Base aufrufen. Ist die Methode pure virtual, so ist
das Resultat undefiniert!
Will man bei der Konstruktion einer Klasse virtuelles Verhalten so gibt
es folgende Möglichkeiten:
Top1.4.3 Destruktor-Definition für Basisklassen
Es gibt folgende Möglichkeiten
- delete von Basisklassen-Zeigern ist erlaubt (polymorphic deletion)
Destruktor ist public und virtual
- delete auf Basisklassen-Zeiger ist nicht erlaubt
Destruktor ist protected und nonvirtual
Da die Defaultimplementierung (public nonvirtual) für Basisklassen
nicht erwünscht ist, sollte der Destruktor stets explizit definiert
werden.
Top1.4.4 Keine Exceptions im Destructor
Destruktoren dürfen keine Exceptions werfen (sonst droht
Programmende während des stack unwinding im Rahmen einer
Exception). Destruktoren sollten deshalb alle Exceptions
auffangen.
Top1.4.5 Copy und Assignment
- Alternative 1: Disable Copying
z.B. für Hilfsklassen, die den Zugriff auf Ressourcen verwalten
und im Destruktor die Ressource freigeben.
class A
{
private:
// disable copying
A (const A&); // not implemented
A& operator=(A const &); // not implemented
};
Derartige Klassen können dann nicht in Standard-Containern verwaltet
werden. Bei Bedarf ist jedoch Verwaltung über SmartPointer möglich.
- Alternative 2: explizite Realisierung des Copy-Constructors
und des Assignment-Operators
- Alternative 3: Compiler-generierte Copy-Versionen sind ausreichend
Kommentar im Klassenheader ergänzen, um dies klar zu stellen.
- Hinweis: In der Regel müssen copy-Constructor und Assignment-Operator
gleich behandelt werden.
- Mögliche Signaturen für den Assignment Operator einer Klasse A:
A& operator= (A const &); // traditional signature
A& operator= (A); // optimizer friendly
Ziele: error-safe, möglichst "strong guarantee", safe for self-assignment
Zu empfehlen ist: Realisierung über swap idiom:
A& A::operator= (A const & rhs) // traditional signature
{
A temp (rhs);
swap (temp);
return *this;
}
A& A::operator= (A rhs) // rhs passed by value, optimizer friendly
{
swap (rhs);
return *this;
}
Mögliche Probleme in Klassenhierarchien:
Normalerweise ist polymorphes Verhalten erwünscht. Je nach beabsichtigter/
versehentlicher Wahl der Funktionssignaturen besteht die Gefahr ungewollter
Beschränkung auf die Basisfunktionalität:
class Derived : Base {...};
DoSomething (Base in_base); // value param
DoSomethingElse (Base& in_base); // reference param
Derived aDerivedObject;
DoSomething (aDerivedObject); // object is sliced to Base type!
DoSomethingElse (aDerivedObject); // polymorphic access to Derived type
Lösung über Clone-Funktion:
direkte (unbeabsichtigte) Kopie verbieten, "deep copy"-Funktionalität
über spezifische
Clone-Funktion anbieten:
class Base
{
public:
Base* Clone() const // nonvirtual interface function to be
{ // used by all client code
Base* pBase = DoClone();
// Ensure that derived class has correctly defined DoClone
// and does not return a wrong type
assert (typeid(*pBase) == typeid(*this)) &&
"DoClone incorrectly overridden"
return pBase;
}
protected:
// disable copying
Base (Base const &):
private:
// Every derived (and also more derived) class has to
// implement this method
virtual Base* DoClone() const = 0;
};
Mit diesem Ansatz führt der (versehentliche) Aufruf von
DoSomething (aDerivedObject);
zu einem Compile-Error. Unvollständig realisierte abgeleitete Klassen
haben zumindest einen Laufzeitfehler mit assert.
Hinweis: typeid und polymorphic classes
typeid liefert für polymorphe Klassen den Laufzeit-Typ zurück, auch wenn ein Basisklassenzeiger
übergeben wird. Damit eine Klasse polymorphes Verhalten zeigt, muss sie mindestens eine
virtuelle Methode besitzen (sonst liefert typeid nur den statischen Typ des übergebenen Parameters):
class Base
{
// polymorphic behaviour is activated if
// at least one virtual function exists
virtual void SomeFunction() {};
};
class Derived : public Base
{
};
...
Base base;
Derived derived;
Base* pBase = &derived;
Derived* pDerived = &derived;
std::cout << "typeid(pBase) = " << typeid(*pBase).name() << std::endl;
std::cout << "typeId(pDerived) = " << typeid(*pDerived).name() << std::endl;
Output:
typeid(pBase) = class MyNamespace::Derived
typeId(pDerived) = class MyNamespace::Derived
When the polymorphic behaviour is removed (e.g. by
removing virtual method Base::SomeFunction) the output changes to:
typeid(pBase) = class MyNamespace::Base
typeId(pDerived) = class MyNamespace::Derived
Top1.4.6 Swap-Funktionalität (no-fail guarantee)
Für das Ziel "strongly error safe code" kann eine Swap-Funktion das effiziente
und garantierte Austauschen von Objektinhalten garantieren.
class A
{
public:
void swap (A& rhs)
{
m_dataSpecial.MySpecificSwap (rhs.dataSpecial);
std::swap (m_dataPrimitive, rhs.m_dataPrimitive);
std::swap (m_dataStdContainer, rhs.m_dataStdContainer);
}
private:
SpecialClass m_dataSpecial; // class providing specific swap function
int m_dataPrimitive;
std::vector<double> m_dataStdContainer;
};
Darauf aufbauend kann z.B. auch der Assignment-Operator realisiert werden
(siehe
Swap Idiom).
Wenn SpecialClass keine sichere Swap-Funktion anbietet, jedoch no fail copy construction und assignment, so kann std::swap verwendet werden.
Existiert kein sicherer Copy Constructor, so kann SpecialClass als SmartPointer
in der Klasse aufgenommen werden, da Zeiger sicher ausgetauscht werden können.
(Das Kopieren der zugehörigen Zeigerinhalte ist nicht erforderlich, da Swap
lediglich das Austauschen des Bezugs erfordert.)
Empfehlung:
Spezialisierung von std::swap (nur möglich für Nicht-Template-Klassen)
namespace std
{
template<> void swap (MyClass& lhs, MyClass& rhs)
{
lhs.swap(rhs);
}
}
Allgemein: nonmember Funktion im gleichen namespace.
Anwendungsbereich: Value-Klassen, nicht sinnvoll für
Basisklassen/Klassenhierarchien, die über Zeiger verwendet werden.
Top1.5 Namespaces
Top1.5.1 Nonmember-Funktionen und das Interface-Prinzip
Interface-Prinzip
Das logische Interface einer Klasse X besteht sowohl aus den
public member functions (z.B. void X::MyFunction1 (void)
)
als auch aus den
nonmember functions (z.B. void MyFunction2(X in_x)
),
die den Typ X verwenden und im gleichen namespace wie X definiert
sind.
Zur Unterstützung dieses Interfaceprinzips wurde in C++ das
"argument dependent lookup (ADL)" oder
"Koenig lookup" eingeführt, das dafür sorgt,
dass die passenden nonmember-Funktionen genauso einfach gefunden werden
wie reguläre member functions.
namespace MyNamespace
{
class X
{
public:
void MyFunction1 (void);
};
void MyFunction2 (X in_x);
}
Ähnliche Verwendung im Client-Code:
X x;
x.MyFunction1();
MyFunction2(x);
Insbesondere bei Operator Funktionen (z.B. auch Stream-Operatoren)
sollten nonmember functions bevorzugt werden:
namespace MyNamespace
{
class X {};
X operator+ (X const&, X const &);
// => enables simple client syntax: xSum = x1 + x2;
}
Empfehlung
Bevorzugt sollten Operatoren und Funktionen als nonmember nonfriend
Funktionen realisiert werden (also nicht als reguläre Memberfunktionen).
Motivation: Minimierung der Abhängigkeiten, der Functionbody kann dann nicht
vom nicht öffentlichen Teil der Klasse abhängen.
Vorsicht
Nonmember-Funktionen, die nicht zum Interface von X gehören, dürfen
nicht im gleichen Namespace definiert werden. Insbesondere gilt das
für alle Templatefunktionen. Sonst besteht die Gefahr, dass sie durch das ADL
für nicht vorgesehene Zwecke (z.B. von X abgeleitete Typen wie
std::vector<X>
) verwendet werden.
Top1.5.2 Using Deklarationen
- using Deklarationen können und sollen ohne schlechtes Gewissen
verwendet werden.
- Einschränkung
In Header-Files oder vor include-Statements dürfen keine
using Deklarationen eingesetzt werden. Alle Namen müssen dort
vollständig qualifiziert werden. Dies gilt auch für vermeintlich
sichere Statements vom Typ "using Class::Function"
Negativbeispiel
Mögliche Reihenfolgenprobleme, die z.B. durch unterschiedliche Includereihenfolgen
ausgelöst werden können:
// code section 1
namespace A
{
int f (double);
}
...
// code section 2
namespace B
{
using A::f; // only considers f(double)!
void g();
}
...
// code section 3
namespace A
{
int f (int); // is not considered by the preceding using declaration!
}
...
// code section 4
void B::g{)
{
f(1); // calls A::f(double)
// if code section 3 would preced section 2
// it would call the better matching function A::f(int)
}
Top1.6 Generische Programmierung und Templates
Top1.6.1 Statischer und dynamischer Polymorphismus
dynamischer Polymorphismus - Klassen mit virtuellen Funktionen
- geeignet für: einheitlichen indirekten Zugriff auf eine Klassenhierarchie über
Basisklassen-Zeiger
- dynamisches Binding, binär kompatibel: Anwendungscode kann unabhängig vom Code
erzeugt werden, der die Hierarchie (Basisklassen oder einzelne
abgeleitete Klassen) beinhaltet
statischer Polymorphismus - Template-Klassen und Template-Funktionen
- geeignet für: einheitliche Behandlung aller Typen, die dem
gleichen syntaktischem und semantischem Schema genügen
- derartige Typen erfüllen ein "implizites Interface", d.h. für sie
erzeugt das Template (und die darin verwendete Syntax) compilierbaren
Code.
- statisches Binding, separate Compilierung nicht möglich
- ggf. bessere Effizienz/Optimierung durch compile-time Evaluation und static binding
Top1.6.2 Customizing explizit anbieten
Vermeidung von unbeabsichtigtem Customizing
- Genaue Dokumentation der "points of customization" im Rahmen der
Template-Dokumentation
- Verhinderung unbeabsichtigter Customizations durch Deaktivierung
des ADL (argument dependent lookup):
- intern verwendete Hilfsfunktionen, die nicht variiert werden
sollen, in eigenem "nested namespace" realisieren und mit expliziter
Qualifizierung aufrufen:
template<typename T>
void MyTemplateFunction (T in_t)
{
MyHelperNamespace::MyFunction(in_t);
// alternative: parenthesis also disables ADL
(MyFunction)(in_t);
}
- Abhängigkeit von parametrierbarer Basisklasse Base(T) explizit formulieren:
template<typename T>
class C : Base<T>
{
// safe access to dependent type within base
typedef typename Base<T>::SomeType MyType;
void Function ()
{
// safe access to base member function
Base<T>::SomeBaseFunction();
// same effect via this pointer, but allowing virtual behaviour
this->SomeBaseFunction();
}
};
Methoden zum Anbieten von Customization points:
- implizites Interface für Memberfunktionen
// (within template code)
// SomeFunction and value_type are customization points:
t.SomeFunction(); // regular member function syntax
typedef typename T::value_type MyType;
- implizites Interface für Nonmemberfunktionen
// (within template code)
SomeFunction(t); // unqualified call to non member function
typedef typename T::value_type MyType;
- Spezialisierung einer Traits-Klasse
Eine Traits-Template-Klasse wird im Namespace des Templates zur Verfügung gestellt.
Bei Bedarf kann der Anwender dieses Traits-Template für seinen Typ spezialisieren,
d.h. er kann spezifische Typen und (i.d.R. statische) Funktionen spezifisch
implementieren:
// (within template code)
SomeTraits<T>::SomeFunction(t);
typedef typename SomeTraits<T>::value_type MyType;
Top1.7 ErrorHandling und Exceptions
Top1.7.1 Assert
Top1.7.2 Fehlerstrategie
- innerhalb eines Moduls nur eine einzige Fehler-Strategie (error handling policy)
einsetzen
- die Strategie muss folgende Aspekte umfassen
- Identification: Welche Bedingungen sind Fehler?
- Severity: Wie wichtig ist jeder einzelne Fehler?
- Detection and handling: Wo wird der Fehler aufgedeckt, wo behandelt?
- Propagation: Wie wird der Fehler innerhalb des Moduls weitergemeldet?
- Reporting: Aufzeichnung des Fehlers (Logfile) und Art der Benachrichtigung
an den Bediener.
- Fehlerstrategie nur an den Modulgrenzen ändern, die Schnittstellenfunktionen
des Moduls müssen dann von der internen Strategie (z.B. Exceptions) auf die externe
Strategie (z.B. Returnwerte) umwandeln
Top1.7.3 Definition eines Fehlers
Ein Fehler betrifft immer eine Funktionsausführung, bei der mindetens eine der
folgenden Bedingungen nicht erreicht wird:
- eine Precondition ist nicht erfüllt, bzw. kann nicht hergestellt werden
- eine PostCondition kann nicht hergestellt werden
- eine Invariante (z.B. gültiger Zustand für alle Datenmember einer Klasse)
ist verletzt
Alle anderen Situationen sollten nicht als Fehler gemeldet werden.
Hinweis:
Die Verletzung einer Precondition bei Modul-/Projektinternem Aufruf
der Funktion kann auch als Programmierfehler gesehen werden,
der mit assert() überwacht werden kann.
Top1.7.4 Empfehlung: Bevorzugter Einsatz von Exceptions
Vorteile:
- Exceptions können im Gegensatz zu Returncodes nicht
versehentlich / per Default ignoriert werden.
Dazu ist mindestens ein try-catch-Block erforderlich.
- Exceptions werden automatisch an higher-level
Code propagiert
- Das Fehlerhandling kann besser vom normalen Kontrollfluss
getrennt werden. Die Fehleranalyse und Behandlung findet
in catch-Blöcken und evtl. erst in übergeordneten Funktionen
statt.
Empfehlung: die nächstliegende Ebene, die genügend Kontextwissen
hat, führt die Behandlung des Fehlers durch.
- Konstruktor- und Operator-Aufrufe haben keine wirklich
gute Alternative zu Excpetions für das Weitermelden von Fehlern.
Nachteile/Konsequenzen
- Kontrollfluss im Falle von Exceptions ist z.T. schwieriger
zu erkennen.
- Als Folge dürfen Destruktoren und Deallokierungsfunktionen
nicht scheitern (andernfalls kommt es zur Terminierung
während des Exception-Unwindings)
Empfehlung: Verzicht auf Exception-Specifications:
- zusätzlicher Overhead durch implizit vom Compiler eingefügte try/catch-Blöcke,
die das Einhalten der Spezifikation prüfen.
- im Falle des Nichteinhaltens der Spezifikation kommt es zur
sofortigen Programmterminierung (normalerweise nicht erwünscht,
Möglichkeit eines zentralen unexpectded_handlers ist oft nicht
ausreichend)
Top1.7.5 Exceptions: Throw by value, catch by reference
- throw by value:
- Risiko bei Weitergabe einen Zeigers: das zugehörige Objekt könnte zum
Zeitpunkt des Fangens bereits zerstört sein.
- der Compiler erledigt das Speichermanagement für value-Objekte
- Anforderung: der copy-Konstruktor des Exception-Objektes
muss die no-throw-Guarantee erfüllen
- Alternative: Werfen eines Smartpointers als Value, d.h.
die Exception hält eine eigene Referenz auf das interessierende
Objekt
- catch by (const) reference/rethrow:
Top1.7.6 Exceptions nur innerhalb eines Moduls
Empfehlung: Exceptions sollten nicht über Modulgrenzen hinweg propagiert werden.
Typischerweise wird das modul-intern bevorzugt einzusetzende C++-Exceptionhandling an den Aussenschnittstellen des Moduls z.B. auf Fehlerreturnwerte konvertiert.
Mögliche Motivation: In manchen Anwendungsfällen hat man keine Kontrolle
über die Compiler-Optionen mit denen bestimmte Module generiert werden. Damit sind die ExceptionHandling-Mechanismen evtl. gar nicht binär kompatibel.
Ein try-catch(...)-Block sollte deshalb in folgenden Situationen eingesetzt werden:
- in main()
- in jeder Thread-Main-Funktion
- in eigenen Callback-Funktionen, insbesondere wenn der rufende Code nicht vollständig
kontrolliert wird
- in Interface-Funktionen
- innerhalb von Destruktoren, sofern sie Funktionen rufen, die Exceptions
werfen können
Diskussion: völlig unerwartete Fehler sollten evtl. nicht vollständig aufgefangen werden,
sondern z.B. über rethrow bewusst zum Abbruch des Programms führen, da sonst das Fehlersymptom nur verschleppt wird und an anderer Stelle zu schwer analysierbaren Fehlern führt. Neben einem Loggen des Fehlers kann dabei im catch-Block evtl. auch ein automatischer Exception-Dump für die Offline-Analyse erstellt werden.
Top1.7.7 Error-safe Code - basic/strong/no fail guarantee
Mögliche "Sicherheitsstufen" bei Ausführung einer Funktion:
-
Basic Guarantee
nach Auftreten eines beliebigen Fehlers während der Ausführung
einer Funktion hat das Programm weiterhin einen gültigen
Zustand und ist auch weiter lauffähig.
-
Strong Guarantee
nach Auftreten eines beliebigen Fehlers hat das Programm
entweder wieder den ursprünglichen Zustand vor Aufruf der
Funktion oder den beabsichtigten Zielzustand eingenommen.
-
No-Fail/No-Throw Guarantee
die Funktion kann immer erfolgreich durchgeführt werden,
wichtig z.B. für Destruktoren, Funktionen zur Deallokierung
von Ressourcen und Swap-Funktionen
Bewertungen:
- Empfehlung:
Jede Funktion sollte die stärkste "Sicherheits-Garantie" umsetzen, die
ohne (Performance-)Nachteile für Aufrufer möglich ist, die die Garantie
nicht benötigen.
- Kann nicht einmal die basic guarantee eingehalten werden, so
muss das als Programmierfehler betrachtet werden.
- Sicherster Lösungsansatz über Transaktionsmodell: Ursprünglichen Zustand
nur verändern, wenn alle Teilschritte der Funktion erfolgreich waren.
Beispiele:
- Retry-Möglichkeit nach Fehler beim Speichern
Nach dem Scheitern einer Safe-Operation, sollte
der Bediener die Möglichkeit eines Retrys haben, d.h.
der ursprüngliche Zustand sollte erhalten bleiben.
- Irreversibler Raketenabschuss
Wird in einem Teilschritt eine irreversible Aktion
ausgeführt (z.B. Abschuss eines Satelliten in die
Umlaufbahn), so kann keine Strong Guarantee mehr
zugesichert werden. Evtl. kann die Funktion
aufgesplittet werden in einzelne Teilfunktionen,
die bis auf die kritische Aktion die Strong-Guarantee
erfüllen.
Top1.8 STL-Container
- Default-Entscheidung: Einsatz von std::vector
vector ist z.B. auch für Listen optimal, solange die Anzahl der
Elemente nicht sehr groß wird
- C-APIs,
die Arrays erwarten, können auch über std::vector und
std::string versorgt werden
Zeiger auf Arraybereich: &*myVector.begin() oder &myVector.front()
char*-Lesezugriff: myString.c_str() liefert null-terminierten C-String,
char*-Schreibzugriff: myString.data() liefert nicht null-terminierten String
- ein herkömmliches Array ist akzeptabel, wenn die
Grösse bereits zur Compilezeit feststeht
(Alternative: boost::array)
- in STL-Containern dürfen nur values, smart pointer und Iteratoren gespeichert werden
z.B. auto_ptr<T> ist nicht geeignet, da beim Kopieren
der Objekt-Bezug evtl. an temporäre Container-Elemente weitergegeben wird
Nicht-Value-Typen oder polymorphe Objekte können über smart pointer verwaltet werden
- Hinzufügen von Elementen bevorzugt über
push_back (falls nicht an bestimmter Position
eingefügt werden muss), in Algorithmen back_inserter einsetzen
gute Performanz: constant time Garantie, Kapazität wird exponentiell erweitert
- Bevorzugung von Range-Operationen anstelle von Einzelelement-Operationen,
z.B. myVector.insert(position, itFirst, itLast)
- swap trick idiom zur Reduzierung des Containers auf die tatsächlich benötigte Grösse
// reduce capacity to minimum
container<T>(myContainer).swap(myContainer);
// clear all contents and reduce capacity to minimum
container<T>().swap(myContainer);
- erase-remove idiom zum Löschen von Elementen aus einem Container
myContainer.erase(std::remove(myContainer.begin(), myContainer.end(),
someValue), myContainer.end());
Hinweis: der remove-Algorithmus arbeitet nur mit dem Iterator und hat keinen Zugriff auf den Container,
kann also nichts daraus löschen.
Top1.9 STL-Algorithmen
Top1.9.1 Allgemein
Top1.9.2 Vermeidung handgeschriebener Schleifen
Algorithmen sind oft besser als handgeschriebene Schleifen
Hinweis: Für sehr einfache Fälle sind herkömlich geschriebene
Schleifen weiterhin die einfachste und lesbarste Lösung.
(Negativ-)Beispiel für handgeschriebene Schleife zur Ermittlung des ersten
Elementes, das zwischen minVal und maxVal liegt:
vector<int>::iterator it = v.begin();
for(;it != v.end(); ++it)
{
if (*it > minVal && *it < maxVal) break;
}
Negativbeispiel für schwer lesbare Lösung über Standard-Binders:
vector<int>::iterator it = find_if(v.begin(),v.end(),
compose2 ( std::logical_and<bool>(),
std::bind2nd(greater<int>(),minVal),
std::bind2nd(less<int>(),maxVal)));
Negativbeispiel für selbstgeschriebenes Function-Objekt, das weit entfernt von der Schleife definiert ist:
template<typename T>
class Between : public unary_function<T,bool>
{
public:
Between (T const & in_minVal, T const & in_maxVal)
: m_minVal (in_minVal), m_maxVal (in_maxVal) {}
bool operator()(T const & in_val) const // compare logic far away from loop
{return in_val > m_minVal && in_val < m_maxVal;}
private:
T m_minVal;
T m_maxVal;
};
...
vector<int>::iterator it = find_if(v.begin(),v.end(), Between<int>(minVal, maxVal);
Empfehlung: Verwendung von (Boost) Lambda-Funktionen:
vector<int>::iterator it = find_if(v.begin(),v.end(), _1 > minVal && _1 < maxVal);
siehe auch
BOOST_FOREACH
Top1.9.3 Auswahl eines passenden Suchalgorithmus
Top1.9.4 Auswahl eines passenden Sortieralgorithmus
Reihenfolge nach steigendem Aufwand:
- partition
teilt die Menge in zwei Hälften, liefert Iterator-Position für
erstes Element, das die Bedingung nicht mehr erfüllt
Beispiel: Zahlen größer als 7 nach vorne sortieren:
partition (numbers.begin(), numbers.end(), std::bind2nd(std::greater<int>(),7);
- nth_element
das N-te Element wird korrekt einsortiert und die vorausgehenden bzw.
nachfolgenden Elemente sind relativ zum N-ten Element richtig angeordnet
(untereinander ist jedoch noch keine Sortierung garantiert).
Beispiel 1: die 5 größten Zahlen nach vorne sortieren
nth_element (numbers.begin(), numbers.begin() + 4, numbers.end(), std::greater<int>());
Beispiel 2: die Person mit dem mittleren Gewicht (= Median) bestimmen,
links davon die leichteren, rechts die schwereren Personen
nth_element (people.begin(), people.begin() + people.size()/2, people.end(), WeightIsLess);
- partial_sort
Sortierung wie bei nth_element(), zusätzlich ist der Bereich vor dem
N-ten Element vollständig sortiert
Beispiel: die 3 kleinsten Zahlen in aufsteigender Reihenfolge nach vorne sortieren
partial_sort (numbers.begin(), numbers.begin() + 3, numbers.end(), std::less<int>());
- sort
vollständige Sortierung des Bereiches
Hinweise:
- die stable-Versionen garantieren zusätzlich, dass beim Sortieren die Reihenfolge gleichwertiger
Elemente nicht verändert wird
-
index container idiom
nth_element, partial_sort, sort benötigen random access Iteratoren. Stehen diese
im zu sortierenden Container (z.B. std::list) nicht zur Verfügung, so legt man einen
Container (z.B. std::vector) an, der diese Iteratoren unterstützt, speichert
darin die Iteratoren zu den Elementen im Ausgangscontainer und wendet den
Sortieralgorithmus auf den "Iterator"-Container an.
Top2 Loops
Top2.1 BOOST_FOREACH
Instead of writing:
for (MyContainer::iterator itElement = myContainer.begin();
itElement != myContainer.end(); ++itElement)
{
it->ChangeState();
}
you can simply write:
BOOST_FOREACH (MyElement & element, myContainer)
{
element.ChangeState();
}
- bietet einfache Notation, Iteratoren nicht notwendig
- direkte Einbettung des Elementzugriffs in die Schleife, keine ausgelagerten
Hilfsfunktionen wie bei Verwendung von std::foreach
- ermöglicht gleichartiges Iterieren über C-Arrays,
STL-Container und Strings
- Regeln
- zum schreibenden Zugriff Referenz verwenden (MyElement & element)
- innerhalb der Schleife kann break und continue verwendet werden
- Schleifen können verschachtelt werden
- Achtung: nicht verwenden, wenn innerhalb der Schleife der Container verändert wird
- weitere Infos siehe
Boost Documentation
Top2.1.1 Beispiel: bedingte Summenbildung
std::list<int> numberList;
// Build sum of all positive numbers within list";
int sum (0);
BOOST_FOREACH (int curInt, numberList)
{
if (curInt > 0)
sum+=curInt;
}
cout << "nSum=" << sum << endl;
Top2.1.2 Beispiel: Zugriff auf Map
// Increment all numbers within the map
// Remark: As BOOST_FOREACH is a macro you cannot use types
// containing commas
// instead use typedefs to get simpler and more readable names
typedef std::map<std::string,int> NumberMap;
BOOST_FOREACH (NumberMap::value_type & curPair, numberMap)
{
++curPair.second;
}
Top2.1.3 Beispiel: Zugriff auf Pointer to const structs
struct MyDataStruct
{
int m_id;
double m_val;
std::string m_name;
SpIDemoInterface m_spIDemoInterface;
void WriteName();
void WriteNameConst() const;
};
typedef boost::shared_ptr<MyDataStruct const> SpMyDataStructConst;
std::vector<SpMyDataStructConst> myDatas;
//Calling const member functions and accessing an interface pointer
// stored in the struct
BOOST_FOREACH (SpMyDataStructConst spCurStruct, myDatas)
{
// access data member and interface method
cout << "nStructname=" << spCurStruct->m_name << "n";
spCurStruct->m_spIDemoInterface->DoSomethingConst();
// call const member function
spCurStruct->WriteNameConst();
// Cannot call non const member function
// spCurStruct->WriteName();
// Remark: Calling non const method within interface member is possible
spCurStruct->m_spIDemoInterface->DoSomething();
}
Top2.2 Gründe für explizites Iterieren
Manche Schleifen führen über einen längeren Zeitraum Aktionen aus. Hier ist es nicht so wichtig, möglichst performant das Iterieren und den Container-Zugriff durchzuführen, sondern es kommt vielmehr darauf an, den Ablauf schrittweise debuggen oder tracen zu können. Dies kann erreicht werden durch:
- altmodische for-i-Schleife, Ausgabe von i als Trace/Fortschritts-Information
- explizites Iterieren mit Iteratoren, z.B. von container.begin() bis container.end(),
zum Tracen kann die Position 1..N innerhalb der Schleife ermittelt werden durch
unsigned int numCurElement = distance(myContainer.begin(),itCurrentElement)
+ 1;
unsigned int numElementsLeft = distance(itCurrentElement, myContainer.end());
Top3 Fortgeschrittene Konzepte (TR1)
Top3.1 Lambda
Lambdaausdrücke sind "unbenannte", i.d.R. kleinere Funktionen, die direkt am Ort ihres Aufrufes definiert werden. Der Aufwand (an anderer Stelle) eine normale Funktion oder ein Funktionsobjekt zu definieren, wird dabei vermieden. Der relevante Code bleibt an einer Stelle konzentriert.
Siehe MSDN
Beispiel
std::list values;
int sum;
std::for_each(values.begin(), values.end(),
[&sum](int const & in_value){if (in_value > 0) sum+=in_value;});
std::cout "nSum= " sum << std::endl;
Top3.2 Auto
Insbesondere Variablen mit kompliziertem Typ (z.B. Template-Ausdrücke) können durch Angabe von "auto" auf einfache Weise formuliert werden.
Der Compiler schliesst durch die Angabe eines Initialisierungsausdruckes auf den erforderlichen Typ.
Siehe MSDN
Beispiel
SomeComplexContainerType values;
BOOST_FOREACH(auto element, values)
{
// do something with element
}
Top4 Unicode: charset and encoding
References:
The Absolute Minimum Every Software Developer Must Know About Unicode
How to write BOM into file using fstream
UTF-8 Everywhere
Top4.1 Outdated: ASCII, ANSII
Traditional charsets like
ASCII or
ANSII
assumed that each letter corresponds to a
specific bit combination in memory or on disk. Given a fixed size (e.g. one byte per char
for European languages)
only a limited set of chars could be represented. To satisfy country specific needs there
were introduced country specific charsets, so called
code pages
which usually have the regular
ASCII chars within range 0-127 and
differing chars in range 128-255.
The big problems:
- you have to know the specific code page for each text you want to read/display
- you cannot mix arbitrary chars from different countrys within one text
Top4.2 The new world: Unicode
Unicode distinguishes between the charset and its encoding.
Top4.2.1 Charset
Unicode is a charset given by
globally defined hexadecimal numbers, the
"code points".
Each existing character is uniquely associated to a specific code point.
Example: "ABC" is given by the code points U+0041 U+0042 U+0043
As the numeric range of hexadecimal numbers is unlimited also
the number of representable characters is conceptually unlimited.
Top4.2.2 Encoding
Unicode can be encoded in different ways:
- UTF-8
each character needs a variable number of bytes (1-6). Simple ASCII chars are stored as
one byte (so old fashioned ASCII-programs (unaware of the existence of Unicode) will successfully
interprete Unicode texts containing only simple english text)
- UTF-16 / UCS2
each character is stored within 16 bits / 2 bytes
- UTF-32 / UCS4
each character is stored within 32 bits / 4 bytes
- any other encodings are possible
Endianness
Assuming an encoding as UTF-16 (as used by Unicode-enabled functions on Windows) there still remains the question about the order in which the
2 bytes are stored. Some computer systems first write the most significant byte (
big endian)
other computer systems do the reverse and write the least siginficant byte as first (
little endian).
To store UTF-16 text safely within a portable text file a special "
Byte Order Mark" (
BOM) must be
written at the head of the file.
The BOM to be used for UTF-16 on any computer system is always 0xFEFF. Depending on the endianess of your computer system the
bytes will be stored in order FE FF (big endian) or FF FE (little endian). The compuer system reading
an file (possibly received from an external system) can now read the BOM and deduce about
the stored byte order of the UTF-16 chars.
Top4.2.3 Sample: Generating an UTF16 text file
Assumption: you already have some UTF16 text stored within a std::wstring.
std::wstring fileContents = GetFromSomeWhere();
// Build file path as wstring
std::wstring fileDir = L"C:/SomeDir";
std::wstring fileName = L"MyUTF16Sample.txt";
std::wstring fullFilePath = fileDir + L"/" + fileName;
// Open/create UNICODE file
FILE* targetFile = _wfopen(fullFilePath .c_str(), L"wb, ccs=UNICODE");
std::wofstream ofFile(targetFile);
// Write byte order mark
const wchar_t BOM_UTF_16 = 0xFEFF;
ofFile << BOM_UTF_16;
// Write text contents
ofFile.write(fileContents.c_str(), fileContents.length());
ofFile.close();