Funktions-Templates
Ein Funktions-Template ist eine Familie von Funktionen. In diesem Beitrag möchte ich tiefer in das Thema eintauchen.
- Rainer Grimm
Ein Funktions-Template ist eine Familie von Funktionen. In diesem Beitrag möchte ich tiefer in das Thema eintauchen.
Hier ist eine kurze Erinnerung, um alle auf den gleichen Wissensstand zu bringen.
Wenn ein Funktions-Template wie max fĂĽr int und double instanziiert wird,
template <typename T>
T max(T lhs,T rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max(10, 5);
max(10,5, 5,5);
}
erzeugt der Compiler ein vollständig spezialisiertes Funktions-Template für int und double: max<int> und max<double>. Der generische Teil ist in beiden Fällen leer: template<>. Dank C++ Insights kann ich tiefere Einsichten anbieten.
Jetzt möchte ich in die Details eintauchen. Was passiert, wenn ich Funktions-Templates und gewöhnliche Funktionen (kurz: Funktionen) verwende?
Ăśberladen von Funktions-Templates und Funktionen
Dazu verwende ich erneut die Funktion max. Diesmal instanziiere ich sie fĂĽr float und double. Dazu stelle ich eine Funktion max bereit, die auch doubles nimmt.
Hier ist mein nächstes Beispiel:
template <typename T>
T max(T lhs,T rhs) {
return (lhs > rhs)? lhs : rhs;
}
double max(double lhs, double rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max(10.5f, 5.5f); // (1)
max(10.5, 5.5); // (2)
}
Dabei drängt sich eine Frage auf: Was passiert in den mit (1) und (2) markierten Zeilen? Hier sind meine konkreten Fragen:
- (1): Wählt der Compiler das Funktions-Template oder die Funktion aus und erweitert den
floatzudouble? - (2): Sowohl die Funktion als auch das Funktions-Template sind ideale Kandidaten. Dies ist mehrdeutig. Verursachen diese Zeilen einen Compilerfehler?
Die Antworten auf diese Fragen sind intuitiv und folgen der allgemeinen Regel in C++. Der Compiler wählt den am besten passenden Kandidaten aus.
- (1): Das Funktions-Template ist ein besserer Kandidat, weil die Funktion eine Erweiterung von
floatnachdoubleerfordern würde. - (2): Das Funktions-Template und die Funktion sind ideale Kandidaten. In diesem Fall tritt eine zusätzliche Regel in Kraft. Wenn beide gleich gut passen, bevorzugt der Compiler die Funktion.
Wie zuvor hilft C++ Insights dabei, diesen Prozess zu visualisieren.
Der Screenshot zeigt es explizit. Nur der Aufruf von max für double (2) löst die Instanziierungen des Funktions-Templates aus.
Beim weiteren Text gilt eine Einschränkung zu beachten: Ich ignoriere Concepts in diesem Artikel.
Unterschiedliche Template Argumente
Nun verwende ich das Funktions-Template max mit zwei Werten unterschiedlichen Datetyps.
template <Typname T>
T max(T lhs,T rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max(10.5f, 5.5);
}
Dieses Programm möchte ich gerne mit C++ Insights ausprobieren.
Wow! Was passiert hier? Warum wird der float nicht zu einem double erweitert? Ehrlich gesagt, der Compiler denkt anders:
- Der Compiler bestimmt die Template-Argumente mithilfe der Funktions-Argumente, falls dies möglich ist. In diesem Fall ist es möglich.
- Der Compiler fĂĽhrt diesen Prozess der Bestimmung der Template-Argumente fĂĽr jedes Funktions-Argument durch.
- FĂĽr
10,5fbestimmt der CompilerfloatfĂĽrT,fĂĽr5,5bestimmt der CompilerdoublefĂĽrT. - NatĂĽrlich kann
Tnicht gleichzeitigfloatunddoublesein. Wegen dieser Zweideutigkeit schlägt die Kompilierung fehl.
Zweite Einschränkung: Ich habe den Prozess der Ableitung von Template-Argumenten vereinfacht. Ich werde in einem zukünftigen Post über Template-Argument-Deduktion für Funktions- und Klassen-Templates schreiben.
NatĂĽrlich wollen wir Werte unterschiedlichen Typs vergleichen.
Zwei Typ-Parameter
Die Lösung scheint ganz einfach zu sein. Ich führe einfach einen zweiten Typ-Parameter ein.
template <Typname T, Typname T2>
max(T lhs,T2 rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max(10.5f, 5.5);
}
Einfacher geht es nicht! Oder? Es gibt eine ernsthafte Herausforderung in diesem Beispiel. Man beachte die drei Fragezeichen als Rückgabetyp. Dieses Problem tritt typischerweise dann auf, wenn das Funktions-Template mehr als einen Typ-Parameter besitzt. Welchen Rückgabetyp benötigt das Funktions-Template? Soll in diesem konkreten Fall, sollte der Rückgabetyp T, T2, oder ein von T und T2 abgeleiteter Typ R sein? Das war vor C++11 eine herausfordernde Aufgabe, aber mit C++11 ist es ziemlich einfach.
Hier sind ein paar Lösungen, die ich im Kopf habe:
// automaticReturnTypeDeduction.cpp
#include <type_traits>
template <typename T1, typename T2> // (1)
typename std::conditional<(sizeof(T1) > sizeof(T2)),
T1, T2>::type max1(T1 lhs,T2 rhs) {
return (lhs > rhs)? lhs : rhs;
}
template <typename T1, typename T2> // (2)
typename std::common_type<T1, T2>::type max2(T1 lhs,T2 rhs) {
return (lhs > rhs)? lhs : rhs;
}
template <typename T1, typename T2> // (3)
auto max3(T1 lhs,T2 rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max1(10.5f, 5.5);
max2(10.5f, 5.5);
max3(10.5f, 5.5);
}
Die ersten beiden Versionen max1 (1) und max2 (2) basieren auf der type-traits Bibliothek. Die dritte Version max3 (3) nutzt die automatische Typableitung von auto.
max1(1):typename std::conditional<(sizeof(T1) > sizeof(T2)), T1, T2>::typegibt den TypT1oderT2zurück, der größer ist. std::conditional ist eine Art ternärer Operator zur Kompilierzeit.
max2(2):typename td::common_type<T1, T2>::typegibt den gemeinsamen Typ der TypenT1undT2zurück. std::common_type kann eine beliebige Anzahl von Argumenten akzeptieren.max3(3):autosollte selbsterklärend sein.
Der typename vor dem Rückgabetyp der Funktions-Templates max1 und max2 mag irritieren: T1 und T2 sind abhängige Namen. Ein abhängiger Name ist ein Name, der von einem Template-Parameter abhängt. In diesem Fall müssen wir dem Compiler einen Hinweis geben, dass T1 und T2 Typen sind. T1 und T2 könnten auch Nicht-Typen oder Templates sein.
Dritte Einschränkung: Ich schreibe in einem weiteren Artikel über abhängige Typen.
Schauen wir uns an, was C++ Insights bietet. Ich zeige nur die Template-Instanziierungen. Wer das gesamte Programm analysieren willt, folge diesem Link: C++ Insights.
max1(1): Hier lässt sich nur raten, welchen Rückgabetyp das Funktions-Template besitzt. In der Rückgabeanweisung wird der kleinere Typ (float) zudoubleumgewandelt.
max2(2): Wie beimax1lässt die Rückgabeanweisung den Rückgabetyp erahnen: derfloat-Wert wird zudoublegewandelt.
max3(3): Jetzt können wir den Rückgabetyp explizit sehen: Es ist eindouble.
Wie geht es weiter?
In diesem Artikel habe ich die Herausforderung unterschiedlicher Typen der Funktions-Argumente gelöst, indem ich mehrere Typ-Parameter verwendet habe. In meinem nächsten Artikel wähle ich einen anderen Ansatz und gebe die Template-Argumente explizit an.