Programming with C++

Inhalt

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

  • KISS (keep it simple software):
    korrekt ist besser als schnell,
    einfach ist besser als komplex.
    Anwendung der einfachsten Technik, die eine effektive Lösung ermöglicht.
  • Keine vorzeitige Optimierung: Zuerst zweimal messen, dann einmal lokal optimieren!

    "It is far, far easier to make a correct program fast
    than it is to make a fast program correct."

  • Vermeidung vorzeitiger "Verlangsamung":
    • pass by reference für komplexe Typen
    • prefix operator ++/-- verwenden (postfix operator muss zusätzlich alten Wert zwischenspeichern und zurückgeben)
    • Verwendung der Constructor-Initializer-List anstatt Assignment innerhalb des Konstruktors
    • Bevorzugung von Algorithmen anstatt handgeschriebener Loops, Verwendung von STL-Containern und -Algorithmen
  • Nicht anwenden: inlining by default,
    erst nach Analyse mit Profiler einzelne Funktionen inline deklarieren, sonst zuviele unnötige Abhängigkeiten

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:

  • a @= b entspricht a = a @ b
  • @ sollte über @= definiert werden
    T& T::operator@= (const T &)
    {
        //...
        return *this;
    }
    
    T operator@ (T const & lhs, T const & rhs)
    {
        T temp (lhs);
        return temp @= rhs;
    }
    
  • operator@ ist eine non member function => für linke und rechte Seite gelten die gleichen impliziten Konversionen
  • falls möglich auch operatror@= als non member function realisieren
  • nonmember operators im gleichen namespace wie den Typ T definieren, um lookup-Probleme zu vermeiden

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:

  • Der Client-Code ruft nach new eine virtuelle Funktion Init auf
  • Die Klasse bietet eine Factorymethode Create an, die diese Schritte beinhaltet:
    class Base
    {
    protected:
        Base(){};
        virtual void Init();
    
    public
        template<class T>
        static shared_ptr<T> Create()
        {
            shared_ptr<T> p (new T);
            p->Init();;
            return p;
        }
    };
    

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

  • nur zur Verifikation interner Annahmen, Abweichung bedeutet Programmierfehler, insbesondere sinnvoll zum Aufdecken von Fehlern in der Entwicklungsphase
  • nicht zum Prüfen von Runtime-Fehlern verwenden (z.B. out of memory)
  • Syntax
    assert(!"Some description to display in case of error");
    
    assert (    (someValue==43)
             && (!"Some description to display in case of"
                  "preceding condition is not fulfilled"));
    

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:
    • Fangen als Wert-Typ könnte zu Slicing führen
    • Fangen per Referenz erhält den Polymorphismus des Exception- Objektes
    • analog sollte rethrow über throw; statt über throw e; ausgeführt werden, damit der Polymorphismus erhalten bleibt.
      catch (MyException & e)  // catch by reference to non-const when changes
      {                        // of the exception object are needed 
      
          e.DoSomethingToChangeObject(); // e.g. add context info to error chain
      
          throw;               // rethrow changed exception object
      }
      

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

  • Verwendung einer "checked" STL-Implementierung ermöglicht das Abfangen folgender "beliebter" Fehlersituationen:
    • Verwendung eines ungültigen Iterator-Bereiches (itRangeBegin kommt nicht vor itRangeEnd)
    • Iterator bezieht sich auf anderen Container
  • In STL-Algorithmen verwendete Prädikate (Function-Objekte mit einem Argument und Returntyp bool) sollten pure functions sein, d.h. ihr Ergebnis darf nur von den Argumenten abhängen und nicht von globalen oder (dynamisch sich ändernden) Memberdaten abhängen. Der zugehörige operator() sollte daher als const definiert werden.

    Begründung:

    • STL-Algorithmen kopieren die Function-Objekte und gehen davon aus, dass jede Kopie gleichwertig ist
    • die Aufrufreihenfolge ist beliebig und nicht garantiert, so kann z.B. das Auswählen jedes 3. Elementes nicht über eine mitzählendes Prädikat realisiert werden, da z.B. eine andere STL-Implementierung die Prädikate in anderer Reihenfolge aufrufen könnte.

      Negativbeispiel: Löschen jedes dritten Elementes nicht so realisieren:
      v.erase(remove_if(v.begin(),v.end(),CheckIfCalledNumTimes(3)));

  • Bevorzugung von Function Objects gegenüber Funktionen
    Motivation
    • Kombination mit Adaptern ist möglich (z.B. std::not1)
    • Compiler kann schnelleren Code erzeugen
    • Verwendung als Comparer für assoziative Container (z.B. set<SomeThing, MyCompare>)
    Beispiele:
    // predicate function object
    struct HasSomeProperty : public unary_function
    {
        bool operator() (SomeThing const &) const {...}
    }
    
    // compare function object
    struct MyCompare : public binary_function
    {
        bool operator() (SomeThing const &, SomeThing const &) const {...}
    }
    
  • Functions objects werden i.d.R. by value übergeben. Das Kopieren sollte daher performant sein (ggf. über das Pimpl-Idiom Bezug zu größerem Implementation-Objekt auslagern).

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

  • unsortierter Bereich
    find/findif, count/count_if: Prüfung ob und wo ein Element existiert
  • sortierter Bereich
    lower_bound: Iterator auf ersten Treffer bzw. die Position zum Einfügen
    upper_bound: Iterator auf Position hinter dem letzten Treffer bzw. die Position zum Einfügen
    equal_range: liefert Ergebnisse von lower_bound und upper_bound

    Hinweis: binary_search liefert lediglich die Info, ob ein Element existiert oder nicht und ist meist nutzlos.

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