Funktions-Templates: Mehr Details zu expliziten Template-Argumenten und Concepts
Im letzten Beitrag "Funktions-Templates" habe ich über das Überladen von Funktions-Templates und das automatische Ableiten des Rückgabetyps eines Funktions-Templates geschrieben. Heute tauche ich tiefer ein und gebe explizit die Template-Argumente eines Funktions-Templates an und bringe Concepts ins Spiel.
- Rainer Grimm
Im letzten Beitrag "Funktions-Templates" habe ich über das Überladen von Funktions-Templates und das automatische Ableiten des Rückgabetyps eines Funktions-Templates geschrieben. Heute tauche ich tiefer ein und gebe explizit die Template-Argumente eines Funktions-Templates an und bringe Concepts ins Spiel.
Bevor ich diesen Beitrag beginne, möchte ich zwei allgemeine Bemerkungen loswerden. Heute schreibe ich über ein Don't und ein Do.
- Don't: Generell sollte man die Template-Argumente für Funktions-Templates nicht explizit angeben.
- Do: Generell sollte man eingeschränkte Template-Parameter (Concepts) verwenden.
Lass mich mit dem Don't beginnen.
Die Template-Argumente explizit angeben
Die Template-Argumente müssen explizit angeben werden, wenn der Compiler die Typparameter der Funktions-Templates nicht ableiten kann oder Klassen-Templates zum Einsatz kommen. Mit C++17 kann der Compiler automatisch den Typ der Template-Argumente aus den Konstruktorargumenten ableiten:
std::vector<int> myVec{1, 2, 3, 4, 5}; // (1)
std::vector myVec{1, 2, 3, 4, 5}; // (2)
Anstelle von (1) lässt sich in C++17 einfach (2) verwenden. Ich werde in einem kommenden Beitrag mehr über dieses Feature schreiben.
Im Allgemeinen sollten Entwickler die Template-Argumente nicht angeben. Ich habe meine Regel aber absichtlich gebrochen.
// maxExplicitTypeParameter.cpp
template <typename T>
T max(const T& lhs,const T& rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
auto res1 = max<float>(5.5, 6.0); // (1)
auto res2 = max<bool>(5.5, 6.0); // (2)
auto res3 = max(5,5, 6,0); // (3)
}
Was passiert in dem Bereich (1) - (3)? C++ Insights hilft mir, den Code zu analysieren. Dies sind die entscheidenden Ausgabezeilen:
- Der Aufruf
max<float>(5.5, 6.0)
in (1) bewirkt die Instanziierung des Funktions-Templatesmax
fürdouble
(Zeile 10). Folglich werden beide doubles inconst float
umgewandelt (Zeile 40). - Der Aufruf
max<bool>(5.5, 6.0)
in (2) legt eine Menge Arbeit auf die Schultern des Compilers. - Der Aufruf veranlasst den Compiler, die doubles implizit in
bool
zu konvertieren. - Um die beiden bools innerhalb des Funktionskörpers zu vergleichen (Zeile 23), müssen sie auf
int
erweitert werden (Zeile 23). - Schließlich ist der Rückgabetyp
res2 bool
. Folglich muss derint
Wert inbool
konvertiert werden. - Der Aufruf
max(5.5, 6.0)
in (3) macht den richtigen Job. Es ist keine Umwandlung oder Erweiterung notwendig.
Ehrlich gesagt, denke ich, dass der Aufruf wie max<bool>(5.5, 6.0)
ein Fehler war und keine Absicht darstellt. Aber das passiert, wenn man schlauer sein will als der Compiler.
Es gibt eine verwandte Syntax zur expliziten Angabe von Template-Argumenten, die manchmal zum Einsatz kommt: max<>(5.5, 6.0)
. Wenn ich in meinen Seminaren die Frage stelle, was dieser Ausdruck bedeuten könne, geben mir ca. die Hälfte meiner Teilnehmer nach meiner bisherigen Theorie die richtige Antwort.
Angenommen, es ist eine Funktion und eine Funktionsvorlage max
implementiert:
// maxCompilerDeduction.cpp
double max(const double& lhs, const double& rhs) {
return (lhs > rhs)? lhs : rhs;
}
template <typename T>
T max(const T& lhs,const T& rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
auto res1 = max(5.5, 6.0); // (1)
auto res2 = max<>(5.5, 6.0); // (2)
}
Wie ich im vorherigen Artikel "Funktions-Templates" gezeigt habe, bevorzugt der Compiler die Funktion, wenn die Funktion und das Funktions-Template ideale Kandidaten sind. Okay, das beantwortet (1). (2) drückt aus, dass der Compiler nur das Funktions-Template max
berücksichtigt und die Funktion max
ignorieren soll. Zusätzlich leitet der Compiler automatisch die Template-Parameter für die Funktionsargumente ab. Folglich zeigt C++ Insights, dass der Compiler max
für double
instanziiert hat.
Bisher habe ich nur Funktionsüberladung mit Funktionen und Funktions-Templates mit unbeschränkten Typparametern berücksichtigt. Das kann und sollte ich besser machen. Jetzt bringe ich eingeschränkte Typparameter (Concepts) ins Spiel. Das heißt, hier ist mein Do für diesen Artikel: Verwende eingeschränkte Typ-Parameter, wenn möglich!
Überladen mit Concepts
In C++20 gibt es das Concept std::totally_ordered
. Ein Datentyp T
unterstützt eine totale Ordnung, wenn er eine partielle Ordnung unterstützt und beliebige Elemente von T
verglichen werden können. Lass mich etwas formaler werden:
Ein Datentyp T
unterstützt partielle Ordnung, wenn die folgenden Beziehungen für alle Elemente a
, b
und c
des Datentyps T
gelten:
- a <= a (reflexiv)
- Wenn a <= b und b <= c dann a <= c (transitiv)
- Wenn a <= b und b <= a, dann a == b (antisymmetrisch)
Ein Datentyp T
unterstützt totale Ordnung, wenn er partielle Ordnung unterstützt und alle Elemente vom Datentype T
verglichen werden können.
- a <= b oder b <= b (vergleichbar)
Das folgende Programm setzt das Concept std::totally_ordered
ein:
// maxUnconstrainedConstrained.cpp
#include <iostream>
#include <concepts>
class Account {
public:
explicit Account(double b): balance(b) {}
double getBalance() const {
return balance;
}
private:
double balance;
};
Account max(const Account& lhs, const Account& rhs) { // (1)
std::cout << "max function\n";
return (lhs.getBalance() > rhs.getBalance())? lhs : rhs;
}
template <std::totally_ordered T> // (2)
T max(const T& lhs,const T& rhs) {
std::cout << "max restricted function template\n";
return (lhs > rhs)? lhs : rhs;
}
template <typename T> // (3)
T max(const T& lhs,const T& rhs) {
std::cout << "max unrestriced function template\n";
return (lhs > rhs)? lhs : rhs;
}
int main() {
Account account1(50.5);
Account account2(60.5);
Account maxAccount = max(account1, account2); // (4)
int i1{50};
int i2(60);
int maxI = max(i2, i2); // (5)
}
Das Programm definiert eine Funktion max
und zwei Funktions-Templates max
, die jeweils zwei Accounts annehmen (1). Während das erste Funktions-Template max
in (2) voraussetzt, dass die Werte das Concept std::totally_ordered
unterstützen, besitzt das zweite Funktions-Template max
keine Typ-Einchränkungen für seine Typ-Parameter. Der Compiler wählt in bekannter Manier die am besten passende Überladung aus. Ein Funktions-Tempate a
ist ein besserer Kandidat als ein Funktions-Template b
, wenn a
stärker spezialisierter ist als b
. Das bedeutet, dass der Compiler die Funktion für Accounts (4) und das Funktions-Template max
mit eingeschränkten Typ-Parametern für int
(5) auswählt.
Die Kommentare in den verschiedenen max
-Funktionen helfen, die Entscheidungen des Compilers mithilfe des Compiler Explorer nachzuvollziehen.
Wie geht's weiter?
Nach den Grundlagen zu Funktions-Templates stelle ich in meinem nächsten Beitrag die Grundlagen zu Klassen-Templates vor. Außerdem werde ich in diesem Zusammenhang über generische Memberfunktionen, Vererbung mit Templates und Alias Templates schreiben.
C++ Seminare
Im nächsten halben Jahr biete ich die folgenden Seminare an. Falls es die Covid-19 Situation zulässt, werde ich die Seminare nach Rücksprache als Präsenzseminare durchführen.
- Clean Code mit modernem C++: 22.06.2021 - 24.06.2021
- C++20: 10.08.2021 - 12.08.2021
- Embedded Programmierung mit modernem C++: 21.09.2021 - 23.09.2021