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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#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.