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.

In Pocket speichern vorlesen Druckansicht 160 Kommentare lesen
Lesezeit: 7 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

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 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.

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 oder std::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.

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.

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.


()