Good to know | C++

ODR – One Definition Rule

Irgendwann stolpert jeder über die One Definition Rule in C++. Sie in der Tiefe zu verstehen ist komplex. Aber an der Oberfläche sind folgende Dinge wichtig.

„No translation unit shall contain more than one definition of any variable, function, class type, enumeration type, or template.“

Eine Übersetzungseinheit (translation unit) ist eine .cpp Datei in der alle Header (#include) eingebunden wurden, Makros aufgelöst wurden etc. Also eine Quellcodedatei aus welcher der Compiler ein object file generiert (Objektcode). Innerhalb dieser Quelldatei darf keine Funktion, Variable, Klasse, Enumeration oder Template zweimal definiert sein. Achtung: Es geht um die Definition. Eine mehrfache Deklaration ohne (vollständige) Definition ist möglich.
Das sollte generell kein Problem sein. Nutzt man Include-Guards in den Headerdateien, werden diese stets nur einmal eingebunden (auch wenn man unbemerkt eine Mehrfachinkludierung erzeugt hat). Ohne Include-Guards braucht man nur einen Header einer Klasse direkt zweimal in einer .cpp Datei einbinden und sofort hat man eine Mehrfachdefinition der Klasse in einer Übersetzungseinheit, was mit einem Compilerfehler endet.

Soweit bezieht sich die ODR nur auf eine Übersetzungseinheit. Für das gesamte Program (also über mehrere Einheiten hinweg) gibt die ODR folgendes an:

There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14), non-static function template (14.5.6), static data member of a class template (14.5.1.3), member function of a class template (14.5.1.1), or template specialization for which some template parameters are not specified (14.7, 14.5.5) in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements.“

Es gibt also einige Ausnahmen, die im kompletten Program mehrfach definiert sein dürfen, solange sie eine Anforderung erfüllen: Die Definitionen müssen gleich sein!
Das ist die Erklärung warum ein Header mit einer Klasse im gesamten Programm an verschiedenen Stellen eingebunden und genutzt werden kann. Ohne diese Ausnahme der ODR wäre das nicht möglich, da die Klasse in jeder einbindenden Einheit neu definiert wird.
Auf der anderen Seite gehören (non-inline) Funktionen nicht zu den Ausnahmen. Definiere ich also eine Funktion in einem Header und binde diesen in verschiedenen Quellcodedateien ein, um die Funktion zu nutzen verletze ich die ODR:

// Header.h
#ifndef HEADER_H
#define HEADER_H

int foo(int i) { return i; }

#endif

// Quelle.cpp
#include "Header.h"

int main(int argc, char* argv[])
{
	int i = -1;
	i = foo(i);
	return 0;
}

// Quelle1.cpp
#include "Header.h" // ODR verletzt

In Header.h wird eine Funktion foo definiert. Der Header wird in zwei Übersetzungseinheiten (Quelle.cpp und Quelle1.cpp) eingebunden. Dieser Code wird vom Linker mit einem Error quittiert (VS2012): Mindestens ein mehrfach definiertes Symbol gefunden.

Für Templates ist das ODR ebenfalls interessant. Laut Ausnahmen kann man Funktions-Templates (non-static) ohne Probleme im Header definieren. Nur explizite Spezialisierungen von diesen Templates verletzen schließlich das ODR.
Member-Methoden von Klassen-Templates dürfen ebenfalls im Header (außerhalb der Klassen-Template) definiert werden. Vermutlich funktioniert das, weil ein Template an sich noch nicht zu Objektcode kompiliert werden kann, sondern erst die tatsächliche Ausprägung durch Angabe aller Template-Argumente.

POD – Plain Old Datatype

Inline Keyword

Inlining bedeutet, dass ein Funktionsaufruf nicht wie üblich erfolgt sondern der gesamte Code der Funktion an die Stelle des Aufrufs geschrieben wird (und der Aufruf somit überflüssig wird). Bei kurzen Funktionen wird das Program dadurch (meist) schneller laufen.
Setzt man das inline keyword vor eine Funktion sagt man dem Compiler damit, dass es sich lohnen kann, diese Funktion inline einzubinden. Aktuelle Compiler sind so optimiert, dass sie das jedoch selbst herausfinden und zuverlässiger entscheiden können. Fakt ist, dass ein Compiler nur inlinen kann wenn er die Definition einer Funktion sieht, die Deklaration reicht nicht, da der Funktionscode ja komplett an die Stelle des Aufrufs geschrieben werden soll. Deshalb können Funktionen zwischen Übersetzungseinheiten nur inline geschrieben werden wenn ihre Definition bereits im Header erfolgt.

Fazit: Inline kann man sich in den meisten Fällen sparen. Hat man eine kurze Funktion (one-liner) kann man diese im Header direkt definieren und der Compiler erledigt den Rest.

Extern Keyword

Das extern Keyword sollte nicht mehr benutzt werden. Es deklariert eine Variable/Funktion, die in einer anderen Übersetzungseinheit definiert wird. Das läuft oft auf globale Variablen hinaus, die zwischen Übersetzungseinheiten genutzt werden sollen. Da globale Variablen jedoch überall geändert werden können und niemand einen bestimmten Zustand garantieren kann, sind sie bad practice.

Header Files

C++ Programme werden in einzelnen Übersetzungseinheiten organisiert. Jede CPP Datei ist eine Übersetzungseinheit. Statt ein Programm in nur einer einzigen Datei zu implementieren hat man so eine Gliederung, die den Code lesbarer (und damit auch wartbar) macht und zudem noch eine logische Gliederung in einzelne Bausteine ermöglicht. Übersetzungseinheiten werden getrennt voneinander kompiliert, die eine weiß nix von der anderen.
Headerdateien werden genutzt, um Deklarationen einer Übersetzungseinheit in einer anderen bekannt zu machen. Damit kann man z.Bsp eine Funktion oder eine Klasse aus einer anderen Einheit verwenden. Damit hat man zusätzlich noch den Effekt der Kapselung. Fremde Übersetzungseinheiten bekommmen nur die Headerdatei mit Deklarationen zu sehen. Die Definitionen bleiben (meist) in der zugehörigen CPP Datei verborgen. Headerdatei sind somit eine Schnittstelle. Will man eine fremde Bibliothek im eigenen Code verwenden muss man sie durch die ausgelieferten Headerdateien im eigenen Code bekannt machen.

Notizen zur #include Anweisung: Wird die Headerdatei in Anführungszeichen angegeben, sagt man dem Compiler damit sie liegt höchstwahrscheinlich in dem Verzeichnis in welchem auch auch das aktuelle Projekt liegt bzw. das Verzeichnis in welchem die einbindende Headerdatei liegt. Header die anderswo liegen, in den zusätzlichen Include-Verzeichnissen, nimmt man die eckigen Klammern <> in der Include Anweisung.

Achtung: Using Direktiven sollten in Headern nicht verwendet werden, da der angegebene Namensbereich dann in allen einbindenden Headern benutzt wird. Folglich Bezeichner in Headern immer voll qualifiziert angeben.

Achtung: Klassen können in Headern via forward declaration eingebunden werden (statt den zugehörigen header direkt einzubinden). Geht aber nur wenn in dem einbindenden Header keine Methode der Klasse aufgerufen wird und lediglich Referenzen und Zeiger auf Objekte der Klasse verwendet werden. Ansonsten müsste der Compiler die Größe von Objekten der Klasse kennen und die Methodensignatur prüfen können, was nur mit dem Header geht. Pointer und Referenzen haben dagegen auf einem System die gleiche Größe. In der zugehörigen CPP Datei kann der Header dann problemlos eingebunden werden.
Generell empfiehlt es sich forward declarations einzusetzen sooft es geht um die Kaskade bei der Header Inkludierung zu vermeiden.

Konstante Methoden

Werden die Methoden einer Klasse mit const deklariert will man damit anzeigen, dass von der Methode keine Member des aufrufenden Objektes geändert werden. Das bezieht sich jedoch nur auf die direkten Member nicht auf die Daten auf die sie verweisen.
Beispiel: Man hat einen Pointer auf einen String in einer Klasse. Eine const Methode dürfte die Zeichen im String verändern, den String jedoch nicht neu zuweisen (also den Pointer als tatsächlichen Member ändern).

Operator overload

Operatoren für eigene Klassen können auf zwei Wegen realisiert werden: Als Methode oder als Funktion. Soll heißen: Als Memberfunktion oder als (freie) Funktion. Die Deklaration der Methode erfolgt innerhalb des Namespace der Klasse, wie die übrigen Methoden auch. Die Funktion kann außerhalb definiert werden.
Beispiel Operator+ als Methode von MyClass:

const MyClass operator+(const MyClass& other) const
{
    // erzeuge temporäres objekt für ergebnis
    MyClass tmp(*this);
    // ... addition mit other
    return tmp;
}

Für Methoden ist das aufrufende Objekt innerhalb des Operators durch this immer verfügbar und kann als erster Operand genommen werden. Bei binären Operatoren kommt der zweite Operand als Parameter rein. Bei Methoden ist der Vorteil, dass private Member der Klasse direkt zugreifbar sind. Ein Problem sind jedoch binäre Operatoren, die kommutativ sind und beide Operanden unterschiedliche Typen sind.
Beispiel skalare Vektormultiplikation: s * v und v * s. Die zweite Variante ist als Methode direkt umsetzbar. Für die erste Variante müsste jedoch das Skalar den Operator * überladen haben. Hier muss der Operator als freistehende Funktion implementiert werden.
Achtung: Implementierung als Methode setzt immer einen linken Operanden vom Typ der implementierenden Klasse vorraus.

Beispiel Operator+ als Funktion:

const MyClass operator+(const MyClass& m1, const MyClass& m2)
{
    // do something
}

Ein weiterer Vorteil der Funktion ist, dass beide Operanden als Parameter übergeben werden und somit implizit umgewandelt werden können. Wenn der Aufruf von + mit einem Literal erfolgt aus welchem ein Objekt vom Typ MyClass erzeugt werden kann (Konstruktor vorhanden), ersetzt der Compiler den Aufruf automatisch, was im Code sehr praktisch sein kann.
Nachteil der Funktion ist, dass sie nur die öffentliche Schnittstelle von MyClass nutzen kann. Es sei denn sie wird als friend innerhalb der Klasse deklariert:

class MyClass {
    friend const MyClass operator+(const MyClass& m1, const MyClass& m2);
    // rest of the class
}

Außerdem: Bei Zuweisungsoperatoren sollte eine Kopie des aktuellen Objekts zurückgeliefert werden: return (*this). Bei arithmetischen unären Operatoren wie +=, *= ist das ebenfalls interessant.

Außerdem: Der Rückgabetyp von Operatoren ist oft als const deklariert. Auch wenn ein Operator eine Kopie zurückgibt (und dadurch die Gefahr einer Änderung einer wichtigen Variable ausgeschlossen ist) muss für den Compiler das Schlüsselwort const mit rein um solche Aufrufe schon beim Kompilieren zu verbieten: (a + b) = 3;

const Pointer to const

const int* foo;

foo zeigt auf ein konstantes Objekt vom Typ int, ist selbst aber nicht konstant. foo kann auch auf andere Objekte vom Typ int zugewiesen werden.
Das Objekt auf welches es gerade zeigt kann nicht geändert werden.

int* const foo = NULL;

foo ist selbst als Zeiger konstant. Es muss initialisiert werden da es nur einmal zugewiesen werden kann.
foo zeigt auf ein veränderbares Objekt.

const int* const foo;

foo ist als Zeiger konstant. foo muss initialisiert werden.
Das Objekt auf welches es zeigt kann ebenfalls nicht verändert werden.

Vektor Skalarprodukt in abstrakter Basisklasse (ABC abstract base class) oder template basisklasse?

virtual double Dot(const VectorT& v) const
{
double sum = 0.0;
for (int i = 0; i < N; i++)
{
sum += this->elements[i] * v.elements[i];
}
return sum;
}

virtual double Dot(const IVector* v) const
{

}

Der zweite Ansatz zeigt die ABC. Leite ich Vector2, Vector3 von IVector ab kann ich alle Vektoren in diese Funktion als Argument packen, auch wenn sie länger/kürzer sind als der aufrufende Vector (this). Dadurch wäre das Skalarprodukt nicht definiert, weshalb ich die Definition in den einzelnen Klassen vornehmen wollte. Dort hätte ich dann allerdings doppelten und dreifachen Code der jeweils nur die n-elemente im jeweiligen Vektor mehr dazurechnet.
Nimmt man die Template Klasse, die ein festes Template Argument N für die Größe definiert, welches vom Kompilier dann als Konstante eingesetzt wird, kann man den Code in der Template-Basisklasse lassen (d.h. keine Wiederholung des Codes) und für jeden Vektor kann nur der passende Typ mit der gleichen Anzahl Elemente aufgerufen werden. Für den Vector3 generiert der Compiler die Funktionsversion mit dem Argument VectorT<double, 3> und nur genau solch ein Argument (oder eine abgeleitete Klasse) darf als Argument übergeben werden.

Exceptions

Ausnahmen (Exceptions) sind ein wichtiges Mittel zur Fehlerbehandlung. Früher (als es nur C gab) hat man Fehler in Funktionen auf zwei verschiedene Arten kenntlich gemacht:

  1. Rückgabe eines „Fehlerwertes“ der im Wertebereich des Rückgabewertes liegt, aber symbolisch für einen Fehler steht
  2. Übergabe eines Fehlerparameters des Aufrufenden an die Funktion als Referenz oder Pointer

Die erste Variante birgt das Problem, dass sich nicht immer ein Fehlerwert finden lässt. Typischer Fehlerwert für eine Funktion, die einen String zurückliefern soll ist der leere String „“. Je nach Funktion kann der Wert aber schon wieder zum sinnvollen Rückgabebereich zählen und steht nicht als Fehlerwert zur Verfügung.
Bei beiden Varianten muss der Aufrufer, entweder das Ergebnis prüfen (und den Fehlerwert kennen) oder die übergebene Fehlervariable wie bool success oder bool error prüfen. Das Problem: Keiner zwingt den Aufrufer der Funktion dazu. Wird es vergessen wird fehlerhaft weitergearbeitet. Ausnahmen stellen ein gutes Mittel dar, diese Art der Fehlerbehandlung abzulösen.

Ausnahmen werden geworfen (throw) um in einem try/catch Block gefangen zu werden:

try {
  // code ausführen
} catch (...) {
  // geworfene ausnahmen auffangen und fehler behandeln
}

Eine catch Klausel fängt nur den in ihr spezifierten Typ einer Ausnahme. Mit catch (…) hat man die Möglichkeit alle Typen von Ausnahmen zu fangen. Erfolgt nur ein Teil der Fehlerbehandlung im lokalen catch-Block kann durch ein erneutes throw die gefangene Ausnahme wiederum geworfen werden, um von umgebenden catch-Blöcken (sofern vorhanden) wiederum gefangen zu werden.

try {
  // code ausführen
} catch (Integer i) {
  // geworfene ausnahmen auffangen und fehler behandeln
  throw;
}

Es können mehrere catch-Blöcke in einer try/catch Konstellation verwendet werden. Es wird dabei stets der erste catch-Block genommen der passt. Dabei findet keine Typumwandlung statt! Angenommen eine Ausnahmen vom Typ int wird geworfen: throw(15). Dann kann ein catch-Block wie im obigen Beispiel catch (Integer i) die Ausnahme nicht fangen obwohl die Klasse Integer einen Konstruktor für int besitzt und daraus konstruiert werden könnte. Bei vererbten Typen sieht es wie folgt aus:

class Base {
};

class Child : Base
{
};

try {
  throw(Child());
} catch (Base b) {
  // Child Objekte werden hier ebenfalls gefangen
}

Der catch-Block benötigt ein Objekt vom Typ Base. Aufgrund der Vererbung ist jedes Objekt vom Typ Child auch vom Typ Base weswegen es hier ebenfalls gefangen werden kann. Andersherum funktioniert es nicht. Ein catch-Block der ein Child Objekt erwartet, fängt kein Objekt vom Typ Base.

Achtung: In Destruktoren sollten generell Nie Ausnahmen geworfen werden!

Assignment, Copy Constructors und Conversion

Legt man in C++ eine Klasse an erzeugt der Compiler automatisch (implizit) einen Copy Constructor und einen Copy-Assignment Operator (kurz: assignment operator):

MyClass(const MyClass& other); // Signatur copy constructor
MyClass& operator=(const MyClass& other); // Signatur copy assignment

Der implizite Kopier-Konstruktor erstell lediglich eine flache Kopie des Objektes, wobei alle Datenmember einfach kopiert werden. Deswegen wird man für Klassen, die Zeiger nutzen einen eigenen Kopier-Konstruktor anlegen, der eine tiefe Kopie erzeugt und die Daten auf die der Zeiger zeigt ebenfalls kopiert. Tut man das nicht zeigen nach dem Kopieren zwei Objekte der Klasse auf die selben Daten. Das ist meist ungewollt, da eine Änderung der Daten, die Daten für beide Objekte ändert. Wird außerdem ein Objekt zerstört und seine Daten über den Destruktor freigegeben, ist der Zeiger ungültig. Das andere Objekt würde jetzt einen NULL-Pointer verwenden, was zum Absturz führen kann.
Die Qualifizierung des MyClass& other Parameters als const ist essentiell. Nur so kann der Kopier-Konstruktor mit einem temporären Objekt aufgerufen werden. Der C++ Standard verbietet die Übergabe von temporären Objekten als nicht konstante Referenzen!

Der Zuweisungsoperator ist dem Kopier-Konstruktor generell sehr ähnlich. Das Argument wird ebenfalls const-qualiviziert übergeben. Die oben aufgeführte Variante ist jedoch nur eine mögliche (wenn auch typische) Signatur. Nutzt ein Objekt Zeiger auf Daten, ist es analog zum Kopier-Konstruktor ebenfalls sehr wahrscheinlich, dass ein eigener Zuweisungsoperator geschrieben werden muss, um eine tiefe Kopie zu erzeugen. Die Rule-Of-Three besagt, dass in diesem Fall meistens ein eigener Kopier-Konstruktor, Zuweisungsoperator und Destruktor zu implementieren ist.

Wann verwendet der C++ Compiler den Zuweisungsoperator bzw. Kopier-Konstruktor?

  • Der Kopier-Konstruktor konstruiert ein völlig neues Objekt. Er wird aufgerufen in: Initialisierung, Parameterübergabe per Wert, Funktionsrückgabetyp per Wert
  • Der Zuweisungsoperator weist einem bereits existierenden Objekt die Eigenschaften eines anderen Objektes zu. Er wird nur aufgerufen wenn beide Objekte bereits angelegt (konstruiert) sind.

Die Implementierung beider ist ähnlich, jedoch gibt es wichtige Unterschiede: Im Zuweisungsoperator findet im besten Fall eine Prüfung gegen Selbst-Zuweisung statt (Selbstzuweisung ist vom Compiler her in C++ möglich):

MyClass& operator=(MyClass& other)
{
	if (this == &other)
		return *this;  // self assignment, abort here

	// copy code
}

Um mehrere Zuweisungen im Code verketten zu können (chaining) gibt der Zuweisungsoperator stets das aktuelle Objekt (*this) als Referenz (oder konstante Referenz) zurück. Ein weiterer Unterschied zum Kopier-Konstruktor ist das aufräumen von bereits bestehendem Speicher. Nutzt ein Klassenmember dynamischen Speicher (new) muss dieser im Zuweisungsoperator zunächst mit delete freigegeben werden! Weist man schlicht einen neuen Speicherbereich zu, wird der bisher zugewiesene Speicher zu einem ordentlichen Leck!
Der letzte Unterschied zum Kopier-Konstruktor muss bei abgeleiteten Klassen beachtet werden. Ist eine Klasse von einer anderen Klassen abgeleitet worden, muss im Zuweisungsoperator der Zuweisungsoperator der Super-Klasse aufgerufen werden: MyClassSuper::operator=(other); . Im Kopier-Konstruktor ist das egal, da der eh den Kopier-Konstruktor der Super-Klasse aufruft.

Noch einmal alle 4 Unterschiede des Zuweisungsoperators:

  • Selbstzuweisung abfragen
  • Das aktuelle Objekt zurückliefern (chaining)
  • Aktuellen Speicher freigeben (Aufräumen) bevor neuer Speicher zugewiesen wird
  • Zuweisungsoperator der Superklasse aufrufen (falls abgeleitet)

Mehr lesen: Stackoverflow – Assignment operator and copy constructor
Mehr lesen: Stackoverflow – Conversion constructor vs. overloaded assignment operatorMehr lesen: The Anatomy of the Assignment Operator

Wie mache ich einen Kopier-Konstruktor ausnahmefest?

Wird während des Konstruierens eines Objektes eine Ausnahme geworfen kann schnell ein Speicherleck entstehen, da der Destruktor dieser Klasse nur für fertig konstruierte Objekte aufgerufen wird. Um das zu verhindern, sollten Konstruktoren und Zuweisungoperator ausnahmefest sein. Ausnahmefest bedeutet nicht, dass im Code keine Ausnahme auftreten darf sondern, dass auftretende Ausnahmen kein Speicherleck erzeugen.
Das Problem tritt vor allem bei Allokation von dynamischem Speicher (new/delete) auf. Delete wirft generell keine Ausnahme! New wirft die Ausnahme bad_alloc wenn der angeforderte Speicherbereich nicht reserviert werden konnte. Fordert man Speicher in einem Konstruktor via new an besteht die potentielle Gefahr, dass bad_alloc auftritt und der Konstruktor verlassen wird ohne, dass der Destruktor aufgerufen wird. Das ist kein Problem wenn nur ein Klassenmember dynamischen Speicher braucht. Schlägt die Allokation fehl, konnte eh kein Speicher reserviert werden und muss folglich auch nicht freigegeben werden. Allokiert man mehrere Klassenmember dynamisch und die Allokation misslingt nachdem bereits andere Member erfolgreich mit new Speicher erhalten haben, muss der Speicher für diese Member auch im Fall einer Ausnahme wieder freigegeben werden!
Folgende Klasse zeigt das Beispiel:

ZweiZeiger {
int* data1;
int* data2;

public: 
	ZweiZeiger() 
	{
		this->data1 = new data[10];
		this->data2 = new data[5];  // Ausnahme?!
	}
	
	~ZweiZeiger()
	{
		delete[](data1);
		delete[](data2);
	}
};

Tritt bei der Initialisierung von this->data2 im Konstruktor eine Ausnhame auf, wird data1 zum Speicherleck. Eine Möglichkeit den Konstruktor ausnahmefest zu machen sieht so aus:

ZweiZeiger() 
{
	this->data1 = new data[10];
	
	try {
		this->data2 = new data[5];
	} catch (bad_alloc)  {
		delete[](this->data1);
		throw;
	}		
}

Generell empfiehlt sich für ausnahmefestes Kopieren/Zuweisen von Objekten folgendes Vorgehen: Zuerst führt man die kritischen Operationen durch, die Ausnahmen auslösen können. Erst wenn die geglückt sind aktualisiert man den Status des Objektes mit Operationen die keine Ausnahmen auslösen. So kann man bei der Zuweisung sicherstellen, dass wenn etwas schiefgeht, das Objekt den selben Zustand hat wie davor. Primitive Datentypen stellen generell kein Problem dar. Im Kopier-Konstruktor kann man sie schon in der Initialisierungsliste zuweisen, da dort die Anforderung des unveränderten Zustandes nicht eingehalten werden muss (Man legt ein Objekt ja neu an).
Innerhalb des Zuweisungsoperators kann man sich einen ausnahmefesten Kopier-Konstruktor zu nutze machen. Man erzeuge ein temporäres Objekt als Kopie des zu kopierenden Objektes mit Aufruf des Kopier-Konstruktors. Dabei wurde sämtlicher Speicher gefahrlos allokiiert. Nun tauscht man einfach alle Datenelemente dieses temporären Objektes mit dem aktuellen Objekt. Dabei kann das Funktions-Template std::swap genutzt werden. Für primitive Typen gefahrlos. Für Zeiger bzw. dynamischen Speicher findet die Vertauschung ebenfalls statt. Dadurch zeigt das aktuelle Objekt auf den Speicher des temporären Objektes und umgekehrt. Beim Verlassen des Zuweisungsoperators wird die temporäre Kopie automatisch abgebaut. Es baut dabei automatisch (im Destruktor) den dynamischen Speicher des aktuellen Objektes ab (der im beim Tauschen zugewiesen wurde und jetzt nicht mehr benötigt wird).
Dieses Vorgehen ist als Copy-Swap-Idiom in C++ bekannt.