C++ Core Guidelines: Überraschung inklusive mit der Spezialisierung von Funktions-Templates

Modernes C++  –  6 Kommentare

Heute schließe ich die Regeln der C++ Core Guidelines zu Templates mit einer großen Überraschung für viele C++-Entwickler ab. Ich schreibe über die Spezialisierung von Funktions-Templates.

Erst mal beginnt dieser Artikel sehr einfach mit einer Einführung in Template-Spezialisierung aus der Vogel-Perspektive.

Template-Spezialisierung

Templates definieren das Verhalten von Familien von Klassen oder Funktionen. Oft ist es notwendig, dass spezielle Typen oder Nichttypen besonders behandelt werden. Um diese Anwendungsfälle umzusetzen, bietet sich die vollständige Spezialisierung von Templates an. Klassen-Templates können auch teilweise spezialisiert werden.

Hier ist ein erster, einfacher Codeschnipsel:

template <typename T, int Line, int Column>     // (1)
class Matrix;

template <typename T> // (2)
class Matrix<T, 3, 3>{};

template <> // (3)
class Matrix<int, 3, 3>{};

Zeile 1 ist das primäre oder allgemeine Template. Dies muss vor den teilweisen und vollständigen Spezialisierungen zumindest deklariert werden. Mit der Zeile 2 folgt die teilweise Spezialisierung und mit der Zeile 3 die vollständige Spezialisierung.

Um die teilweise und vollständige Spezialisierung besser zu verstehen, möchte ich eine visuelle Erklärung vorstellen. Stelle dir einen n-dimensionalen Raum von Template-Parametern vor. Im Fall des primären Templates (Zeile 1) kannst du einen beliebigen Datentyp und zwei beliebige ints einsetzen. Im Fall der teilweisen Spezialisierung in der Zeile 2 kannst du nur noch einen Datentypen auswählen. Das heißt, der dreidimensionale Raum wird auf eine Gerade reduziert. Vollständige Spezialisierung bedeutet, dass der dreidimensionale Raum auf einen Punkt reduziert wird.

Was passiert nun, wenn die Templates aufgerufen werden?

Matrix<int, 3, 3> m1;          // class Matrix<int, 3, 3>

Matrix<double, 3, 3> m2; // class Matrix<T, 3, 3>

Matrix<std::string, 4, 3> m3; // class Matrix<T, Line, Column> => ERROR

m1 verwendet die vollständige Spezialisierung, m2 die teilweise Spezialisierung und m3 das primäre Template. Das primäre Template führt zu einem Fehler, da dieses nicht definiert ist.

Hier sind die Regeln, die der Compiler anwendet, um die passende Spezialisierung auszuwählen.

  1. Der Compiler findet nur eine Spezialisierung. In diesem Fall wendet er sie an.
  2. Der Compiler findet mehr als eine Spezialisierung. Der Compiler verwendet die am meisten spezialisierte Variante. Falls dieser Prozess in mehr als einer Spezialisierung endet, führt das zu einem Compilerfehler.
  3. Der Compiler findet keine Spezialisierung und verwendet daher das primäre Template.

Okay, jetzt muss ich erklären, was es heißt, dass A ein mehr spezialisiertes Templates als B ist. Hier ist die Definition von cppreference.com: "A accepts a subset of the types that B accepts."

Nach diesem ersten Überblick möchte ich ein wenig tiefer in Funktions-Templates eintauchen.

Spezialisierung und Überladen von Funktions-Templates

Funktions-Templates machen einerseits den Umgang mit Template-Spezialisierung einfacher, aber auch andererseits anspruchsvoller.

  • einfacher, da Funktions-Templates nur die vollständige Spezialisierung unterstützen.
  • anspruchsvoller, da das Überladen von Funktionen ins Spiel kommt.

Aus der Entwurfssicht lässt sich ein Funktions-Template mit Template-Spezialisierung oder Überladen anpassen:

// functionTemplateSpecialisation.cpp

#include <iostream>
#include <string>

template <typename T> // (1)
std::string getTypeName(T){
return "unknown type";
}

template <> // (2)
std::string getTypeName<int>(int){
return "int";
}

std::string getTypeName(double){ // (3)
return "double";
}

int main(){

std::cout << std::endl;

std::cout << "getTypeName(true): " << getTypeName(true) << std::endl;
std::cout << "getTypeName(4711): " << getTypeName(4711) << std::endl;
std::cout << "getTypeName(3.14): " << getTypeName(3.14) << std::endl;

std::cout << std::endl;

}

Die Zeile 1 ist das primäre Template. Die Zeile 2 enthält die vollständige Spezialisierung für int und die Zeile 3 die Überladung für double. Da ich nicht an den Werten für die Funktionen oder Funktions-Templates interessiert bin, habe ich sie ignoriert: std::string getTypeName(double) zum Beispiel. Die Anwendung der verschiedenen Varianten ist sehr einfach. Der Compiler ermittelt den Datentyp, und die passende Funktion oder das passende Funktions-Template wird aufgerufen. Im Fall der überladenen Funktion zieht der Compiler die Funktion dem Funktions-Template vor, wenn diese perfekt passt.

Aber halt. Wo ist die große Überraschung, die ich versprochen habe? Hier kommt sie.

T.144: Don’t specialize function templates

Die Begründung für die Regel ist kurz und bündig: Spezialisierung von Funktions-Template wird beim Überladen nicht berücksichtigt. Das will ich gerne an einem Beispiel zeigen. Es basiert auf dem Programmschnipsel von Demiov/Abrahams:

// dimovAbrahams.cpp

#include <iostream>
#include <string>

// getTypeName

template<typename T> // (1) primary template
std::string getTypeName(T){
return "unknown";
}

template<typename T> // (2) primary template that overloads (1)
std::string getTypeName(T*){
return "pointer";
}

template<> // (3) explicit specialization of (2)
std::string getTypeName(int*){
return "int pointer";
}

// getTypeName2

template<typename T> // (4) primary template
std::string getTypeName2(T){
return "unknown";
}

template<> // (5) explicit specialization of (4)
std::string getTypeName2(int*){
return "int pointer";
}

template<typename T> // (6) primary template that overloads (4)
std::string getTypeName2(T*){
return "pointer";
}

int main(){

std::cout << std::endl;

int *p;

std::cout << "getTypeName(p): " << getTypeName(p) << std::endl;
std::cout << "getTypeName2(p): " << getTypeName2(p) << std::endl;

std::cout << std::endl;

}

Zugegeben, der Sourcecode schaut recht langweilig aus. Habe Geduld! In der Zeile 1 definiere ich das primäre Template getTypeName. Zeile 2 stellt eine Überladung für Zeiger und Zeile 3 eine vollständige Spezialisierung für int-Zeiger dar. Im Fall von getTypeName2 mache ich eine kleine Variation. Ich platziere die vollständige Spezialisierung (Zeile 5) vor der Überladung von Zeigern (Zeile 6).

Dieses Umsortieren besitzt überraschende Konsequenzen.

Im ersten Fall wird die vollständige Spezialisierung für int-Zeiger aufgerufen und im zweiten Fall die Überladung für Zeiger. Was?

Der Grund für dieses, nicht so intuitive Verhalten ist, dass Funktionsüberladung Template-Spezialisierung ignoriert. Funktionsüberladung berücksichtigt Funktionen und primäre Templates. In beiden Fällen ermittelte die Funktionsüberladung die primären Templates. Im ersten Fall (getTypeName) ist die Zeiger-Variante der bessere Treffer und daher kommt die vollständige Spezialisierung für int-Zeiger zum Einsatz. Im zweiten Fall (getTypeName2) wird auch die Zeiger-Variante verwendet, aber die vollständige Spezialisierung gehört zum primären Template (Zeile 4). Daher wird sie ignoriert.

Wie geht's weiter?

Während ich die Zeilen korrekturlese, habe ich eine Idee. Templates sind für die ein oder andere Überraschung gut. Daher mache ich einen kleinen Ausflug von den C++ Core Guidelines und werde ein paar der Überraschungen vorstellen. Meine Hoffnung ist es, dass du dich an diese Zeilen erinnerst, wenn du den Überraschungen begegnest.

Die Zukunft von C++ spricht Templates. Es ist somit eine gute Idee, ihre Sprache besser zu verstehen.