C++

C++ ist eine multiparadigmatische Programmiersprache, die einerseits C um objektorientierte syntaktische Elemente erweitert und andererseits funktionale Programmierung erlaubt. Die Sprache versucht, alles für jeden zu sein. Ich rate dringend davon ab, C++ zu verwenden.

Versionen

Datum Version Neuerungen
1979 C with classes Klassen, starke Typisierung, Default Argumente
1983 C++ 1.0 Virtuelle Funktionen, Überladene Operatoren
1989 C++ 2.0 Mehrfachvererbung, Abstrakte Klassen
1994 STL
1998 C++98 Templates, Ausnahmen, Namensräume
2011 C++11 Threads, Lambda, Regex
2014 C++14 Typinferenz, Move

Beispiel

hello.cc
#include <iostream> int main (int argc, char * argv[]) { if (argc == 1) { std::cout << "Hello, World!\n"; } else for (int i = 1; i < argc; i++) { std::cout << "Hello, " << argv[i] << "!\n"; } return 0; }

Übersetzen und ausführen mit:

make hello && ./hello
Hello, World!

Syntax

Abstrakte Datentypen

Die Philosophie besteht darin, eigene Typen, sogenannte Klassen, zu definieren, die sich genauso einfach wie primitive Typen verwenden lassen. Die Klasse kapselt ihre internen Details von der Außenwelt ab, exponiert die gewünschten Operatoren und implementiert die gewünschte Funktionalität.

Beispiel: Deklaration einer zuweisbaren und (de)serialisierbaren Klasse. Alle selbst definierten Klassen sollten mindestens diese Methoden aufweisen, um sie als Element in einem Container verwenden zu können.

class Foo {
	friend std::istream & operator>> (std::istream & is,       Foo & c);
	friend std::ostream & operator<< (std::ostream & os, const Foo & c);
public:
	virtual ~Foo     ();                    // Destruktor
	Foo              ();                    // Standard-Konstruktor
	Foo              (const Foo & c);       // Kopier-Konstruktor
	Foo & operator=  (const Foo & c);       // Zuweisungs-Operator
	bool  operator== (const Foo & a) const; // Identitäts-Vergleich
	bool  operator<  (const Foo & a) const; // Ordnungs-Vergleich
private:
	int value;                              // Attribut
};

Zuweisungen

Eine Klasse heißt Assignable, wenn Sie den Kopier-Konstruktur, den Zuweisungs-Operator und die Tausch-Operation anbietet.

	Foo              (const Foo & c);            // Kopier-Konstruktor
	Foo & operator=  (const Foo & c);            // Zuweisungs-Operator
	void  swap       (const Foo &, const Foo &); // Tausch-Operation

Funktoren

Ein Funktor (function object), ist ein Objekt, das sich wie eine Funktion verhält, indem es den Operator () überlädt.

Man unterscheidet:

Generator
Erhält keinen Parameter
Unärer Operator
Erhält einen Parameter
Binärer Operator
Erhält zwei Parameter
Prädikat
Liefert einen Wahrheitswert

Beispiel: Lexikalischer Vergleichsoperator für Zeichenketten ohne Beachtung der Groß-/Kleinschreibung.

#include <functional>

struct icaseless : std::less<std::string>
{
	bool operator() (const std::string & s1, const std::string & s2) const
	{
		return s1.compare (s2, true) < 0;
	}
};

std::map<std::string, std::string, icaseless> foo;

Objektorientierte Programmierung

Das objektorientierte Paradigma umfasst die Konzepte Kapselung, Vererbung und Polymorphismus.

Kapselung: Klassen exponieren eine einfache und klare Schnttstelle und verbergen die Details ihrer Implementierung. In C++ geschiet dies mit den Schlüsselworten public für exponierte Attribute und Methoden, private für geschützte Elemente und protected für ebenfalls geschützte aber weitervererbte Elemente.

class Foo {
	// Private Elemente
public:
	// Exponierte Elemente
protected:
	// Geschützte aber weitervererbte Elemente
private:
	// Interne Details
};

Vererbung: Klassen bilden kategorische Hierarchien, indem sie Attribute und Methoden von übergeordneten Klassen erben. Vererbung kann durch Aggregation und Delegation ersetzt werden. Bei der Öffentlichen Vererbung erbt die Kindklasse alle öffentlichen und geschützten Elemente der Vaterklasse, wobei diese ihren Status behalten.

class Bar : public Foo {
	// Alle öffentlichen Elemente aus Foo werden exponiert.
};

Bei der Privaten Vererbung erbt die Kindklasse ebenfalls alle öffentlichen und geschützten Elemente der Vaterklasse, allerdings werden diese weder exponiert noch weiter vererbt.

class Bla : private Foo {
	// Alle öffentlichen und geschützten Elemente aus Foo sind privat.
};

Polymorphismus: Vielgestaltigkeit bedeutet, dass eine Klasse den selben Methodennamen mit unterschiedlichen Signaturen definieren kann und der Compiler im Kontext entscheidet, welche Methode aufgerufen wird.

Dynamischen Polymorphismus realisiert C++ mit virtuellen Methoden. Eine Klasse mit virtuellen Methoden heißt abstrakt. Es können keine Exemplare von abstrakten Klassen erzeugt werden.

virtual void f() = 0;

Statischer Polymorphismus wird in C++ mit Templates und Namenskonventionen realisiert. Als Beispiel dienen die Konzeptklassen der STL.

Templates

Templates erlauben Generische Programmierung durch die Parametriesierung von Typen.

Ein Funktions-Template definiert eine Familie von semantisch gleichartigen Funktionen, die auf Typen mit einer gemeinsamen Schnittstelle operieren.

// Definition
template <class T> print (const T & param)
{
	std::cout << param << "\n";
}

// Aufruf
print<int>(3);
print<std::string>("Hello, World!");

Ein Klassen-Template definiert eine Familie von Klassen.

// Definition
template <class T, int MAX = 10> class myclass
{
public:
	myclass (T & value);
};

// Instanziierung
myclass<int> m(3);
myclass<string, 50> m("Hello, World!");

Template-Spezialisierung erlaubt die optimierte Implementierung eines bestimmten Familienmitglieds.

template<> class myclass<int>
{
public:
	myclass(int value);
};

Ausnahmen

Das Schlüsselwort throw wird nicht verwendet. Methoden deklarieren keine Ausnahmen. Die Semantik in C++ ist anders als in Java: falls eine Ausnahmeliste deklariert wurde, wird das Programm abgebrochen, wenn eine andere Ausnahme geworfen wurde, ohne die Möglichkeit, die Ausnahme zu protokollieren.

void exceptional ()
{
	throw std::exception ("This does what it says.");
}

try exceptional ()
catch (const my::exception & ex) { ... }

Methoden werfen Ausnahmen nur im Fehlerfall, Destruktoren niemals.

Wer Ausnahmen für den Kontrollfluss mißbraucht, dem ist nicht mehr zu helfen. Konstruktoren dürfen zwar (im Gegensatz zu Destruktoren) Ausnahmen auslösen, müssen sich aber selbst darum kümmern, angeforderte Ressourcen aufzuräumen.

Todsünden

Zeiger oder Referenzen auf lokale Variablen zurückliefern.
Lokale Variablen liegen auf dem Stack und werden mit grosser Wahrscheinlichkeit bald wieder überschrieben. Verlasse Dich auf NRVO und liefere ein neues Objekt!
Zeiger oder Referenzen auf Klassenvariablen zurückliefern.
Dies durchbricht die Kapselung und die dadurch gewonnene Abstraktion. Wenn es gar nicht anders geht, liefere const-Referenzen!
Komplexe Objekte wie Container by value übergeben.
Dies erzeugt bei jedem Aufruf eine Kopie aller enthaltenen Elemente auf dem Stack. Objekte immer by reference übergeben!
Ressourcen außerhalb eines Konstruktors anfordern.
Führt zu unnötig komplexer Ausnahme-Behandlung oder verursacht Speicherlöcher und andere Ressourcen-Lecks. Verwende das RAII-Muster!

Entwurfsmuster

Ressource Acquisition Is Initialisation (RAII)

Betriebsmittel (Speicher, Dateien, Mutexe, Handles) werden ausschließlich im Konstruktur angefordert und im zugehörigen Destruktur freigegeben. Dies entbindet den Anwender von der fehlerträchtigen manuellen Freigabe. Der Code wird Ausnahme-sicher (exception safe). Zum Beispiel:

{
	std::ifstream is ("filename.txt");
	is.read (...);
} // Eingabestrom wird automatisch geschlossen.

statt (in Java):

{
	FileInputStream is = null;
	try {
		is.open ("filename.txt");
		is.read (...);
	} finally {
		// Eingabestrom muss explizit geschlossen werden.
		if (is != null) is.close ();
	}
}

Pointer to Implementation (PIMPL)

Die Deklaration eines Klasse enthält nur einen Zeiger auf eine Struktur mit den Attributen. Auf diese Weise kann man die interne Repräsentation eines Objekts anpassen, ohne die Schnittstelle zu ändern.

foo.h

class Foo {
	struct pimpl;
	struct pimpl * p;
public:
	virtual ~Foo ();
	Foo ();
	Foo (const Foo & f);
	Foo & operator= (const Foo & f);
};

foo.cc

struct Foo::pimpl { // anything... };

Foo::~Foo ()
{
	delete p;
}

Foo::Foo () : p (new pimpl())
{
}

Foo::Foo (const Foo & f) : p (new pimpl (*(f.p)))
{
}

Foo &Foo::operator= (Foo f)
{
	std::swap (p, f.p);
	return *this;
}

Named Return Value Optimization (NRVO)

Der Compiler erzeugt bei der Rückgabe von komplexen Objekten keine unnötigen Kopien, der Code wird lesbarer.

Curiously Reoccuring Template Pattern (CRTP)

@todo: Braucht man für Template Meta-Programmierung.

Literatur