Automatisch umgewandelt
Unser Optional vom letzten Mal fühlt sich irgendwie noch etwas unrund an. Wir können ihn nicht wirklich als einfachen Ersatz für den Basisdatentyp verwenden, obwohl ja eigentlich das genau das Ziel ist. Daher bauen wir das Template diesmal noch ein wenig um, damit die üblichen Dinge wie Zuweisung vom Basisdatentyp etc. funktionieren. C++ bietet mit Operatorüberladung und automatischen Konvertierungsfunktionen dafür praktische Hilfsmittel.
Video
Code
#include <string>
#include <iostream>
class NoValueException
{
};
template <typename T> class Optional
{
T mValue;
bool mHasValue;
public:
Optional() : mHasValue(false)
{
std::cerr << "Standardkonstruktor\n";
}
Optional(T const &v) : mValue(v), mHasValue(true)
{
std::cerr << "Initialisiert mit Parameter\n";
}
void set(T const &v)
{
mValue = v;
mHasValue = true;
}
void clear()
{
mHasValue = false;
}
T const &get() const
{
if (!mHasValue) {
throw NoValueException();
}
return mValue;
}
bool hasValue() const
{
return mHasValue;
}
operator T() const
{
return get();
}
Optional &operator=(T const &v)
{
set(v);
return *this;
}
};
void test(int x)
{
std::cerr << "Wert des Parameters: " << x << std::endl;
}
void test2(Optional<int> const &o)
{
if (o.hasValue()) {
std::cerr << "Wert von o: " << o << std::endl;
}
}
int main()
{
Optional<int> i;
i = 10;
std::cerr << "Wert von i: " << i.get() << std::endl;
int x = i;
std::cerr << "Wert von x: " << x << std::endl;
test(i);
test2(5);
}
Erklärung
Diesmal spielen zwei unterschiedliche Techniken zusammen, um uns unsere
gewünschte Funktionalität zu liefern. Wir überladen einerseits den Operator =
um eine Zuweisung vom int aus zu ermöglichen und bieten andererseits zwei
Konvertierungsfunktionen an um aus einem int in ein Optional und umgekehrt
umzuwandeln. Streng genommen ist nicht unbedingt beides notwendig, aber es passt
hier eben zusammen.
Der erste Schritt ist die Operatorüberladung für =. Operatoren in C++ sind
grundsätzlich auch nur Funktionen, deren Name mit operator beginnt und mit dem
eigentlichen Operator endet. Je nach Art des Operators nehmen sie keinen, einen
oder zwei Parameter. In unserem Fall hier der operator= nimmt als Parameter
seine rechte Seite (also die Quelle der Zuweisung) und gehört zu seiner linken
Seite (das Ziel der Zuweisung). Der Operator fungiert hier im Prinzip nur als
Alias für den Setter, von daher ist der eigentliche Code nicht so wahnsinnig
umfangreich.
Die zweite Technik, die die Klasse verwendet sind Konvertierungsfunktionen.
Konvertierungsfunktionen sind spezielle Klassenmember, die vom Compiler
aufgerufen werden können, wenn ein Typ benötigt wird, aber ein anderer zur
Verfügung steht. Die beiden Funktionen hier im Quelltext stehen in Zeile 20
und 49. Der Konstruktor mit einem Parameter fungiert als automatische
Konvertierung von dem angegebenen Typ (in unserem Fall der unterliegende Typ des
Optional) zur Klasse. Wenn also ein Optional benötigt wird (bspw. als
Parameter der Funktion test2), aber nur ein int zur Verfügung steht (siehe
der Aufruf in Zeile 84), dann wird vom Compiler automatisch der
Konvertierungskonstruktor verwendet. Aus test2(5) wird also
test2(Optional<int>(5)). Die Gegenrichtung ist genauso möglich. Wir haben
einen Optional zur Verfügung, wollen aber einen einfachen int verwenden. Zu
diesem Zweck gibt es die Konvertierungsfunktion in Zeile 49. Die Schreibweise
ist etwas ungewöhnlich (scheinbar kein Rückgabetyp), aber das ist Absicht: der
Rückgabetyp steckt im "Namen" der Funktion (in Wirklichkeit ist das der
Rückgabetyp und die Funktion hat in dem Sinne keinen Namen, da sie nicht direkt
aufgerufen werden kann). Immer dann, wenn wir (in unserem
Optional<int>-Beispiel) einen int brauchen, aber nur einen Optional zur
Verfügung haben (bspw. in den Zeilen 80 und 83) wird automatisch diese Funktion
aufgerufen. Aus test(i) in Zeile 83 wird also test(i.int()) (ACHTUNG: kein
gültiger C++-Code. Die Konvertierungsfunktion wird nicht so aufgerufen, sondern
automatisch vom Compiler eingesetzt!).
Ich hatte eingangs erwähnt, dass nicht zwingend beide (die Operatorüberladung
und die Konvertierungsfunktionen) notwendig sind. Wir könnten hier im Beispiel
auf auf die Überladung von operator= verzichten. Aus dem i = 10 in Zeile 77
würde der Compiler dann folgendes machen: i.operator=(Optional<int>(10)).
Während in unserem Beispiel hier direkt die überladene Variante von =
aufgerufen wird, würde in dem Fall durch den Compiler festgestellt, dass es
keine Variante mit int als Parameter gibt. Daraufhin würde er versuchen, den
int passend zu konvertieren, was ihm über den Konvertierungskonstruktor gelingt.
Zuguterletzt könnte dann die automatisch immer vorhandene Variante des
Zuweisungsoperators vom eigenen Typ angewandt werden. Wenn das so geht, wieso
gibt es dann überhaupt beide Möglichkeiten? Erstens ist das Thema
Operatorüberladung wesentlich komplexer, als hier dargestellt und zweitens kann
man automatische Konvertierungen ausschließen wollen (da sie an vielen Stellen
verwendet werden, die man vielleicht nicht immer mag), aber trotzdem explizit
die Zuweisung ermöglichen (was dann über den überladenen Operator immernoch
geht).
Sowohl die Konvertierungsfunktionen, als auch der Zuweisungsoperator können im
Übrigen auch mehrfach überladen sein. So könnte es zum Beispiel Konstruktoren
geben, die von int oder std::string umwandeln (wenn das semantisch Sinn
ergibt). Der Compiler setzt dann das für die Verwendungsstelle passende ein.