Funktions-Templates

Modernes C++ Rainer Grimm  –  23 Kommentare

Ein Funktions-Template ist eine Familie von Funktionen. In diesem Beitrag möchte ich tiefer in das Thema eintauchen.

Function-Templates

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.

Function-Templates

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 float zu double?
  • (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 float nach double erfordern 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.

Function-Templates

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.

Function-Templates

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,5f bestimmt der Compiler float für T, für 5,5 bestimmt der Compiler double für T.
  • Natürlich kann T nicht gleichzeitig float und double sein. 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>::type gibt den Typ T1 oder T2 zurück, der größer ist. std::conditional ist eine Art ternärer Operator zur Kompilierzeit.
  • max2 (2): typename td::common_type<T1, T2>::type gibt den gemeinsamen Typ der Typen T1 und T2 zurück. std::common_type kann eine beliebige Anzahl von Argumenten akzeptieren.
  • max3 (3): auto sollte 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) zu double umgewandelt.
Function-Templates
  • max2(2): Wie bei max1 lässt die Rückgabeanweisung den Rückgabetyp erahnen: der float-Wert wird zu double gewandelt.
Function-Templates
  • max3 (3): Jetzt können wir den Rückgabetyp explizit sehen: Es ist ein double.
Function-Templates

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.