Neue Typen generieren
Mittels Template Metaprogramming kann der Compiler beim Übersetzen eines
Programms nicht nur zwischen verschiedenen Templatespezialisierungen
unterscheiden, sondern auch noch komplett neue Varianten eines Templates
instanziieren, wenn nötig. Das wird diesmal ausgenutzt, um eine Lücke in unserem
RangeInt-Template zu schließen: wir brauchen ja auch noch ein paar geprüfte
Rechenoperationen.
Video
Code
#include <iostream>
#include <stdexcept>
#include <typeinfo>
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;
RangeInt(int value, bool)
{
mValue = value;
}
template <int otherMin, int otherMax> friend class RangeInt;
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();
return *this;
}
template <int otherMin, int otherMax> RangeInt<min+otherMin, max+otherMax>
operator+(RangeInt<otherMin, otherMax> const &other)
{
return RangeInt<min+otherMin, max+otherMin>(other.getValue()+mValue, true);
}
operator int() const
{
return mValue;
}
int getValue() const
{
return mValue;
}
};
int main()
{
RangeInt<5, 20> r1(10);
RangeInt<15, 30> r2(25);
RangeInt<0, 70> r3 = r1 + r2;
std::cerr << r3 << std::endl;
std::cerr << typeid(r1 + r2).name() << std::endl;
}
Erklärung
Bisher konnten RangeInts nur initialisiert und an andere RangeInt zugewiesen
werden. Das allein ist natürlich auf die Dauer unbefriedigend. Wollten wir damit
rechnen, dann mussten wir sie jedesmal in einen normalen int konvertieren,
rechnen und wieder zurückkonvertieren. Das kostet natürlich. Aber: der Compiler
und speziell die C++-Templates haben noch weitere Tricks auf Lager.
Die Lösung ist eigentlich simpel: wir implementieren die notwendigen numerischen
Operatoren, um rechnen zu können (hier mal am Beispiel des operator+). Die
implementieren wir allerdings mit einem Trick: statt nur RangeInts des
gleichen Typs zu nehmen, wenden wir wieder das gleiche Prinzip, wie beim Copy
Assignment an, und machen aus operator+ ein Funktionstemplate. Als Parameter
bekommt das die Grenzen des übergebenen RangeInt (womit es die sich gleich mal
selbst ableiten kann, damit wir die Arbeit damit nicht haben). Wenn wir nun wie
in Zeile 74 eine Addition zweier RangeInt schreiben, so wird in der Klasse des
ersten (r1 in dem Fall) ein Operator mit den Grenzen des zweiten (r2) aus
dem gegebenen Template instanziiert. Mit den bekannten Grenzen können wir nun –
da sie Compile Time Constants sind – zur Übersetzungszeit rechnen. In unserem
Fall bilden wir einen neuen Typ mit den summierten Grenzen.
Warum funktioniert das? Der Wert von r1 muss mindestens der unteren Grenzes
des Typs entsprechen. Der von r2 analog dessen unterer Grenze. Die Summe der
beiden Werte kann daher niemals unter der Summe der beiden Grenzen liegen. Mit
den oberen Grenzen verhält es sich analog. Wir können also vom Compiler
vorausberechnen lassen, dass die Grenzen des resultierenden Wertes die Summen
der Grenzen der Eingangswerte sind. Genau das tun wir in Zeile 52 und liefern
einen entsprechenden neuen RangeInt zurück. Um bei dessen Initialisierung auch
wirklich den Check zu überspringen, müssen wir schnell noch einen Konstruktor
hinzufügen, der das tut und diesen zugreifbar machen. Aus naheliegenden Gründen
darf der Konstruktor nicht public sein (sonst könnte ja jeder einfach die
Garantien unseres Typs verletzen). Dummerweise kommen die RangeInt-Instanzen
mit unterschiedlichen Grenzen nicht an den private-Konstruktor des jeweils
anderen ran. Deswegen müssen sie hier noch zu friends erklärt werden, um diese
Zugriffsbeschränkung aufzuheben (siehe Zeile 28).
Damit haben wir alles zusammen: wir liefern als Summe zweier RangeInts einen
entsprechenden Typen zurück, der die geeigneten Grenzen hat, um die Summe auch
wirklich zu enthalten. Mit diesem können wir nun wieder normal arbeiten:
initialisieren, zuweisen, etc., alles mit ggf. geprüften Aktionen.
Benutzungstechnisch sieht das ganze aus wie ein normaler int. Ist nur ein
wenig mächtiger.