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

Quellcode

#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.