Template Metaprogramming

Einfach alle Objekte mit anderen Grenzen für eine Zuweisung abweisen ist nicht ganz das richtige. Eigentlich möchten wir abhängig von den Grenzen des anderen Objektes lieber eine Prüfung durchführen oder eben auch nicht. Template Metaprogramming macht genau das möglich.

Video

Quellcode

#include <iostream>
#include <stdexcept>

template <bool> inline void check(int value, int min, int max);
  
template <> inline void check<true>(int value, int min, int max)
{
  std::cerr << "Check!\n";
  if (value < min || value > max) {
    throw std::out_of_range("Ungueltiger Wert");
  }
}

template <> inline void check<false>(int, int, int)
{
}

template <int min, int max> class RangeInt
{
  int mValue;

public:
  RangeInt(int value)
  {
    check<true>(value, min, max);
    mValue = value;
  }
  
  template <int otherMin, int otherMax>
    RangeInt(RangeInt<otherMin, otherMax> const &other)
  {
    check< (otherMin < min || otherMax > max) >(other.getValue(), min, max);
    mValue = other.getValue();
  }
  
  template <int otherMin, int otherMax>
    RangeInt &operator=(RangeInt<otherMin, otherMax> const &other)
  {
    check< (otherMin < min || otherMax > max) >(other.getValue(), min, max);
    mValue = other.getValue();
  }
  
  operator int() const
  {
    return mValue;
  }
  
  int getValue() const
  {
    return mValue;
  }
};

int main()
{
  RangeInt<5, 20> r1(10);
  RangeInt<15, 30> r2(25);
  RangeInt<6, 10> r3(8);
  RangeInt<5, 20> r4(9);
  
  std::cerr << r1 << std::endl;
  r1 = r3;
  std::cerr << r1 << std::endl;
  r1 = r4;
  std::cerr << r1 << std::endl;
  r1 = r2;
  std::cerr << r1 << std::endl;
}

Erkärung

Unser RangeInt-Template vom letzten Mal ist etwas über's Ziel hinaus geschossen: wir können nun nur noch Objekte gleicher Typen aufeinander zuweisen. Eigentlich könnten wir die Regel etwas lockerer gestalten: Sind die Grenzen der Quelle enger oder gleich den Grenzen des Ziels, dann können wir uns die Bereichsprüfung sparen. Sonst müssen wir sie durchführen. Das Problem ist hier: wir müssen diese Prüfung bei der Übersetzung durchführen.

Die Technik, die uns das erlaubt, nennt sich Template Metaprogramming. So richtig Absicht war das nicht, dass man die in C++ eingebaut hat. Sie folgt vielmehr aus den Regeln, nach denen Templates zur Instanziierung ausgewählt werden und wie Compile-Time-Konstanten funktionieren. In unserem Fall steckt die Hauptarbeit im Funktionstemplate check und seinen beiden Spezialisierungen in Zeile 6 und 14. Dieses Template hat einen einzelnen bool-Parameter. Der ist unsere Bedingung: ist er true (Spezialisierung in Zeile 6), dann müssen wir die Prüfung durchführen, ist er false (Spezialisierung in Zeile 14), dann nicht. Im Grunde genommen zeigt dieser Parameter also die Überprüfung der Grenzen durch den Compiler an.

Wie werden diese Grenzen nun überprüft? Dazu müssen wir uns zum Beispiel den Copy-Assignment-Operator anschauen. Dort sehen wir in Zeile 39 eine Instanziierung des check-Templates mit einem komplexen Ausdruck in der Parameterliste. Sämtliche Variablen in diesem Ausdruck sind Compile-Time-Konstanten (die beiden Grenzen der eigenen Templateinstanz, wie auch die Grenzen der anderen Instanz). Damit muss der Compiler diesen Ausdruck auswerten können (sagt der Standard) und auf einen Wert zusammenführen (true oder false in unserem Falle). Dieser Wert wird dann für die Instanziierung des Templates genutzt, womit wir die passende Variante auswählen. Da unsere beiden Template-Funktionen inline sind, wird ihr Inhalt direkt an dieser Stelle eingesetzt (statt einen Funktionsaufruf zu generieren). Wenn unser Check also nicht durchgeführt werden muss (die false-Spezialisierung), dann wird auch tatsächlich überhaupt kein Code an dieser Stelle ausgeführt.

Möglich ist die Instanziierung des passenden check-Templates, weil der Copy-Assignment-Operator seinerseits auch ein Template ist, welches seine beiden Template-Parameter aus dem übergebenen Objekt ableitet. Jedesmal, wenn wir so eine Zuweisung brauchen, wird also der passende Copy-Assignment-Operator instanziiert, der wiederum das passende check-Template instanziiert, welches dann eben prüft oder nicht.

Was wir hier mittels Template Metaprogramming implementiert haben ist eine zur Compile-Zeit geprüfte if-Anweisung (oder ein switch mit nur zwei Pfaden). Insgesamt geht da noch deutlich Komplexeres, aber dazu in einem anderen Video mehr.