Funktions-Templates: Mehr Details zu expliziten Template-Argumenten und Concepts

Modernes C++ Rainer Grimm  –  1 Kommentare

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.

Funktions-Templates: Mehr Details zu expliziten Template-Argumenten und Concepts

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:

Funktions-Templates: Mehr Details zu expliziten Template-Argumenten und Concepts
  • Der Aufruf max<float>(5.5, 6.0) in (1) bewirkt die Instanziierung des Funktions-Templates max für double (Zeile 10). Folglich werden beide doubles in const 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 der int Wert in bool 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.

Funktions-Templates: Mehr Details zu expliziten Template-Argumenten und Concepts

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.