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
Code
C++98-Variante mit partieller Templatespezialisierung
#include<iostream>#include<stdexcept>#include<type_traits>template<bool>inlinevoid check(int value, int min, int max);
template<>inlinevoid 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<>inlinevoid check<false>(int, int, int)
{
}
template<int min, int max>classRangeInt
{
int mValue;
RangeInt(int value, bool)
{
mValue = value;
}
template<int otherMin, int otherMax>friendclassRangeInt;
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);
}
operatorint() const
{
return mValue;
}
intgetValue() const
{
return mValue;
}
};
intmain()
{
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…