Template-Spezialisierung: Mehr Details zu Klassen-Templates
Nachdem ich in meinem letzten Beitrag die Grundlagen zur Template-Spezialisierung vorgestellt habe, tauche ich heute tiefer ein. Ich möchte die partielle und vollständige Spezialisierung eines Klassen-Templates als Compilezeit if vorstellen.
- Rainer Grimm
Nachdem ich in meinem letzten Beitrag die Grundlagen zur Template-Spezialisierung vorgestellt habe, tauche ich heute tiefer ein. Ich möchte die partielle und vollständige Spezialisierung eines Klassen-Templates als Compilezeit if vorstellen.
Spezialisierung von Klassen-Templates als Compilezeit if
Zu meinen Einführungen in die Template-Spezialisierung habe ich ein paar ähnliche Fragen erhalten, darunter: Wie kann man entscheiden, ob ein Datentyp einen gegebenen Typ besitzt oder zwei Typen gleich sind? Die Beantwortung dieser Fragen ist einfacher als es scheinen mag und hilft mir, weitere Theorien über die Spezialisierung von Klassen-Templates zu präsentieren. Für meine Antwort implementiere ich vereinfachte Versionen von std::is_same
und std::remove_reference
. Die in diesem Post vorgestellten Techniken sind eine Anwendung der Spezialisierung von Klassen-Templates und stellen ein Compilezeit if dar.
std::is_same
std::is_same
ist eine Funktion aus der type-traits-Bibliothek. Sie gibt std::true_type zurück, wenn beide Typen gleich sind, andernfalls gibt sie std::false_type
zurück. Der Einfachheit halber gebe ich in meiner Implementierung true
oder false
zurück.
// isSame.cpp
#include <iostream>
template<typename T, typename U> // (1)
struct isSame {
static constexpr bool value = false;
};
template<typename T> // (2)
struct isSame<T, T> {
static constexpr bool value = true;
};
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
// (3)
std::cout << "isSame<int, int>::value: " << isSame<int, int>::value << '\n';
std::cout << "isSame<int, int&>::value: " << isSame<int, int&>::value << '\n';
int a(2011);
int& b(a); // (4)
std::cout << "isSame<decltype(a), decltype(b)>::value " <<
isSame<decltype(a), decltype(b)>::value << '\n';
std::cout << '\n';
}
Das primäre Template (1) gibt als Default false
zurück, wenn du nach dem Wert fragst. Im Gegensatz dazu gibt die partielle Spezialisierung (2), die verwendet wird, wenn beide Typen gleich sind, true
zurück. Das Klassen-Template isSame
lässt sich auf Datentypen (3) und, dank decltype
, auf Werte (4) anwenden. Der folgende Screenshot zeigt die Ausgabe des Programms:
Du ahnst es wohl schon? Das Klassen-Template isSame
ist ein Beispiel für Template-Metaprogrammierung. Nun muss ich einen kleinen Umweg machen und ein paar Worte über Meta verlieren.
Metafunktionen und Metadaten
Zur Laufzeit verwenden wir Daten und Funktionen. Zur Compilezeit verwenden wir Metadaten und Metafunktionen. Ganz einfach, es heißt Meta, da wir Metaprogrammierung umsetzen. Was sind Metadaten oder Metafunktionen? Hier ist die erste Definition:
- Metadaten: Typen und Ganzzahlen, die in Metafunktionen verwendet werden.
- Metafunktion: Funktionen, die zur Compilezeit ausgeführt werden.
Lass mich die Begriffe Metadaten und Metafunktion näher erläutern.
Metadaten beinhalten drei Entitäten:
- Datentypen wie
int
,double
oderstd::string
- Nicht-Typen wie Ganzzahlen, Enumeratoren, Zeiger, lvalue-Referenzen und Fließkommawerte mit C++20
- Templates
In der Metafunktion isSame
habe ich nur Datentypen verwendet.
Datentypen wie das Klassen-Template isSame
werden in der Template-Metaprogrammierung verwendet, um Funktionen zu simulieren. Basierend auf meiner Definition von Metafunktionen, können constexpr
-Funktionen auch zur Compile Time ausgeführt werden und sind somit auch Metafunktionen.
Eine Metafunktion kann nicht nur einen Wert, sie kann auch einen Datentyp zurückgeben. Per Konvention gibt eine Metafunktion einen Wert mittels ::value
und einen Datentyp mittels ::type
zurück. Die folgende Metafunktion removeReference
gibt einen Datentyp als Ergebnis zurück.
// removeReference.cpp
#include <iostream>
#include <utility>
template<typename T, typename U>
struct isSame {
static constexpr bool value = false;
};
template<typename T>
struct isSame<T, T> {
static constexpr bool value = true;
};
template<typename T> // (1)
struct removeReference {
using type = T;
};
template<typename T> // (2)
struct removeReference<T&> {
using type = T;
};
template<typename T> // (3)
struct removeReference<T&&> {
using type = T;
};
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
// (4)
std::cout << "isSame<int, removeReference<int>::type>::value: " <<
isSame<int, removeReference<int>::type>::value << '\n';
std::cout << "isSame<int, removeReference<int&>::type>::value: " <<
isSame<int, removeReference<int&>::type>::value << '\n';
std::cout << "isSame<int, removeReference<int&&>::type>::value: " <<
isSame<int, removeReference<int&&>::type>::value << '\n';
// (5)
int a(2011);
int& b(a);
std::cout << "isSame<int, removeReference<decltype(a)>::type>::value: " <<
isSame<int, removeReference<decltype(a)>::type>::value << '\n';
std::cout << "isSame<int, removeReference<decltype(b)>::type>::value: " <<
isSame<int, removeReference<decltype(b)>::type>::value << '\n';
std::cout << "isSame<int, removeReference<decltype(std::move(a))>::type>::value: " <<
isSame<int, removeReference<decltype(std::move(a))>::type>::value << '\n';
std::cout << '\n';
}
In diesem Beispiel wende ich die zuvor definierte Metafunktion isSame
und die Metafunktion removeReference
an. Das primäre Template removeReference
(1) gibt T mittels des Bezeichners type
zurück. Die partiellen Spezialisierungen für die lvalue-Referenz (2) und die rvalue-Referenz geben ebenfalls T
zurück, indem sie die Referenzen von ihrem Template-Parameter entfernen. Wie zuvor lässt sich die Metafunktion removeReference
mit Typen (4) und, dank decltype
, mit Werten (5) verwenden. decltype(a)
gibt einen Wert, decltype(b
) gibt eine lvalue-Referenz und decltype(std::move(a))
gibt eine rvalue-Referenz zurück.
Zum Abschluss folgt hier die Ausgabe des Programms:
Es gibt eine Falle, in die ich bereits getappt bin. Wenn eine Memberfunktion eines voll spezialisierten Klassen-Templates außerhalb der Klasse definiert wird, darf nicht template<>
verwendet werden.
Memberfunktionen einer Spezialisierung, die außerhalb des Klassenkörpers definiert werden
Das folgende Programm zeigt das Klassen-Template Matrix
, das eine partielle und eine vollständige Spezialisierung besitzt.
// specializationExtern.cpp
#include <cstddef>
#include <iostream>
template <typename T, std::size_t Line, std::size_t Column> // (1)
struct Matrix;
template <typename T> // (2)
struct Matrix<T, 3, 3>{
int numberOfElements() const;
};
template <typename T>
int Matrix<T, 3, 3>::numberOfElements() const {
return 3 * 3;
};
template <> // (3)
struct Matrix<int, 4, 4>{
int numberOfElements() const;
};
// template <> // (4)
int Matrix<int, 4, 4>::numberOfElements() const {
return 4 * 4;
};
int main() {
std::cout << '\n';
Matrix<double, 3, 3> mat1; // (5)
std::cout << "mat1.numberOfElements(): " << mat1.numberOfElements() << '\n';
Matrix<int, 4, 4> mat2; // (6)
std::cout << "mat2.numberOfElements(): " << mat2.numberOfElements() << '\n';
std::cout << '\n';
}
(1) deklariert das primäre Template, (2) definiert die partielle Spezialisierung und (3) die vollständige Spezialisierung von Matrix
. Die Memberfunktionen numberOfElements
werden außerhalb des Klassenkörpers definiert. Zeile (4) ist wohl die nicht-intuitive Zeile. Wenn die Memberfunktion numberOfElements
außerhalb des Klassenkörpers definiert wird, darf kein template <>
verwendet werden. Zeile (5) bewirkt die Instanziierung der partiellen und Zeile (6) die Instanziierung der vollständigen Spezialisierung.
Wie geht's weiter?
In meinem nächsten Beitrag schreibe ich über die vollständige Spezialisierung von Funktions-Templates und deren überraschendes Zusammenspiel mit Funktionen. Um es kurz zu machen, gemäß den C++ Core Guidelines gilt: T.144: Don't specialize function templates.