Teilweise ziemlich speziell

Templatespezialisierung als Möglichkeit, absichtlich Fehler hervorzurufen – die Sprache überrascht einen doch immer wieder. Diesmal geht es um die Fähigkeit der partiellen Templatespezialisierung, bei der ein Template nur für einen Teil der möglichen Werte seiner parameter spezialisiert wird. Ansonsten bleibt der Templatetyp ohne Definition unvollständig und zwingt den Compiler zu einer Fehlermeldung, wenn eine fehlende Spezialisierung gebraucht wird.

Video

Quellcode

C++98-Variante mit partieller Templatespezialisierung

#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 <bool> struct staticAssert;

template <> struct staticAssert<true>
{
};
 
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)
  {
    staticAssert< (otherMin <= max && otherMax >= min) >();
    check< (otherMin < min || otherMax > max) >(other.getValue(), min, max);
    mValue = other.getValue();
  }
   
  template <int otherMin, int otherMax>
    RangeInt &operator=(RangeInt<otherMin, otherMax> const &other)
  {
    staticAssert< (otherMin <= max && otherMax >= min) >();
    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);
  
  r1 = r2;
}

C++-11-Variante mit static_assert

#include <iostream>
#include <stdexcept>
#include <type_traits>
 
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)
  {
    static_assert(otherMin <= max && otherMax >= min, "Initialisierung kann niemals erfolgreich sein!");
    check< (otherMin < min || otherMax > max) >(other.getValue(), min, max);
    mValue = other.getValue();
  }
   
  template <int otherMin, int otherMax>
    RangeInt &operator=(RangeInt<otherMin, otherMax> const &other)
  {
    static_assert(otherMin <= max && otherMax >= min, "Zuweisung kann niemals erfolgreich sein!");
    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<25, 30> r2(25);
  
  r1 = r2;
}

Erklärung

Der ganze Trick versteckt sich im staticAssert-Template. In Zeile 19 wird dieses nur deklariert, aber ohne Definition gelassen. Für den Wert true wird es dann direkt darunter spezialisiert und mit einer passenden Definition versehen. Die ist leer, denn sie tut zur Laufzeit eigentlich gar nichts. Wichtig ist nur, dass sie existiert. 

Die Verwendung findet sich dann in Zeile 54. Ähnlich wie beim check-Template wird die Bedingung, die geprüft werden soll, als Ausdruck für den Template-Parameter spezifiziert. Trifft der Compiler auf diesen Ausdruck, so muss er ihn natürlich berechnen, um die passende Spezialisierung für das Template auswählen zu können. Aus diesem Grund dürfen im Ausdruck natürlich wieder nur Compiler-Time-Konstanten vorkommen. Der Ausdruck, den wir hier gewählt haben, gibt an, dass sich die Wertebereiche von Quelle und Ziel mindestens auf einer Seite überlappen müssen. Das ist notwendig, damit es überhaupt einen gemeinsamen Wertebereich gibt, in dem eine Zuweisung klappen könnte.

Wertet der angegebene Ausdruck zu true aus, dann wird die – vorhandene – true-Spezialisierung ausgewählt, das Template instanziiert und der Compilerlauf fortgesetzt. Wertet der Ausdruck hingegen zu false aus, müsste der Compiler das Template eben dafür instanziieren. Das kann er aber nicht, da wir weder eine allgemeine Definition, noch eine Spezialisierung für false für das Template angegeben haben. Der gewünschte Typ ist also unvollständig, da ihm die Definition fehlt, genau die Fehlermeldung, die uns der Compiler auch um die Ohren wirft.

Der Trick funktioniert, weil der Compiler erst bei der Instanziierung eines Templates auf mögliche Fehler prüft. Solange keine unserer Zuweisungen im Programm die false-Spezialisierung instanziiert, stört deren Fehlen den Compiler kein Stück.

Die Fähigkeit, die Übersetzung abhängig von bestimmten Bedingungen abbrechen zu können, ist in so vielen Fällen praktisch, dass das Standardisierungskommitee ein entsprechendes Sprachmittel in C++11 aufgenommen hat. Mittels static_assert kann genauso ein Compile-Time-Ausdruck geprüft und ggf. ein Fehler ausgelöst werden. Etwas praktischer hier noch: man kann die Fehlermeldung des Compilers bestimmen. static_assert nimmt einen String als zweiten Parameter und gibt diesen im Fehlerfall aus. Das hilft dem geneigten Programmierer vielleicht schneller auf die Sprünge, als eine Meldung über einen unvollständigen Typ...