Template Arguments
Es ist recht interessant, welche Regeln der Compiler anwendet, um die Template-Argumente abzuleiten. Um es kurz zu machen: Fast immer resultiert der erwartete Datentyp.
- Rainer Grimm
Es ist recht interessant, welche Regeln der Compiler anwendet, um die Template-Argumente abzuleiten. Um es kurz zu machen: Fast immer resultiert der erwartete Datentyp.
Die Regeln gelten nicht nur fĂĽr Funktions-Templates (C++98), sondern auch fĂĽr auto (C++11), fĂĽr Klassen-Templates (C++17) und Concepts (C++20).
C++ unterstützt die Ableitung von Funktions-Template-Argumenten seit seinen Anfängen. Hier ist eine kurze Rekapitulation.
Funktions-Template Argument Deduktion
Zunächst rufe ich ein Funktions-Template max für int und double auf
template <typename T>
T max(T lhs, T rhs) {
return (lhs > rhs)? lhs : rhs;
}
int main() {
max(10, 5); // (1)
max(10.5, 5.5); // (2)
}
In diesem Fall leitet der Compiler die Template-Argumente aus den Funktionsargumenten ab. C++ Insights zeigt, dass der Compiler ein vollständig spezialisiertes Funktions-Template für max für int (1) und für double (2) erstellt.
Der Prozess der Template-Typ-Deduktion, wie in diesem Fall, produziert meist den erwarteten Typ. Es ist recht aufschlussreich, diesen Prozess tiefer zu analysieren.
Template Type Deduction
Bei der Ableitung des Template-Typs kommen drei Entitäten ins Spiel: T, ParameterType und der Ausdruck expression.
template <typename T>
void func(ParameterType param);
func(expression);
Es werden zwei Typen abgeleitet:
TParameterType
Für ParameterType gibt es die drei Möglichkeiten:
- Wert
- Referenz (
&) oder Zeiger (*) - Univerale Referenz (
&&)
Der Ausdruck wiederum kann ein lvalue oder ein rvalue sein. Zusätzlich kann der lvalue oder rvalue eine Referenz, oder const oder volatile qualifiziert sein.
Der einfachste Weg, die Template-Typ-Deduktion zu verstehen, ist, den ParameterType zu variieren.
ParameterType ist ein Wert
Den Parameter als Wert zu nehmen, ist wohl die am häufigsten verwendete Variante.
template <typename T>
void func(T param);
func(expr);
- Wenn
expreine Referenz ist, wird die Referenz ignoriert =>newExprwird erzeugt - Wenn
newExprconstodervolatileist, wirdconstodervolatileignoriert.
Wenn der ParameterType eine Referenz oder eine universelle Referenz ist, wird die constness (oder volatileness) von expr beachtet.
ParameterType ist eine Referenz (&) oder ein Zeiger (*)
Der Einfachheit halber verwende ich eine Referenz. Die analoge Argumentation gilt fĂĽr einen Zeiger. Im Wesentlichen ergibt sich das erwartete Ergebnis.
template <typename T>
void func(T& param);
// void func(T* param);
func(expr);
- Wenn
expreine Referenz ist, wird die Referenz ignoriert (aber letztendlich wieder hinzugefĂĽgt). - Wenn
exprmit demParameterTypeĂĽbereinstimmt, wird der resultierende Typ zu einer Referenz. Das bedeutet: - eine
exprvom Typintwird zu einemint& - eine
exprvom Typconst intwird zu einemconst int& - eine
exprvom Typconst int&wird zu einemconst int&
ParameterType ist eine Universelle Referenz (&&)
template <typename T>
void func(T&& param);
func(expr);
- Wenn
expreinlvalueist, wird der resultierende Typ zu einer lvalue-Referenz. - Wenn
expreinrvalueist, wird der resultierende Typ zu einer rvalue-Referenz.
Zugegeben, diese Erklärung war ziemlich technisch. Hier ist ein Beispiel.
// templateTypeDeduction.cpp
template <typename T>
void funcValue(T param) { }
template <typename T>
void funcReference(T& param) { }
template <typename T>
void funcUniversalReference(T&& param) { }
class RVal{};
int main() {
const int lVal{};
const int& ref = lVal;
funcValue(lVal); // (1)
funcValue(ref);
funcReference(lVal); // (2)
funcUniversalReference(lVal); // (3)
funcUniversalReference(RVal());
}
Ich definiere und verwende ein Funktions-Template, das sein Argument per Wert (1), per Referenz (2) und per universeller Referenz (3) nimmt.
Dank C++ Insights kann ich die Typableitung des Compilers visualisieren.
- (1): Beide Aufrufe von
funcValuebewirken die gleiche Instanziierung der Funktions-Templates. Der abgeleitete Typ ist einint.
- (2): Der Aufruf der Funktion
funcReferencemitconst int&ergibt den Typconst int&.
- (3): Der Aufruf der Funktion
funcUniversalReferenceergibt eine lvalue-Referenz oder eine rvalue-Referenz.
Es gibt ein interessantes Verhalten, wenn man die Funktion funcValue mit einem C-Array aufruft. Das C-Array decays (verfällt).
Decay eines C-Arrays
Ein C-Array als Wert anzunehmen ist besonders.
// typeDeductionArray.cpp
template <typename T>
void funcValue(T param) { }
int main() {
int intArray[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
funcValue(intArray);
}
Wird das Funktions-Template funcValue mit einem C-Array aufgerufen, decayed das C-Array in einen Zeiger auf sein erstes Element. Decay hat viele Facetten. Es wird angewendet, wenn ein Funktionsargument als Wert übergeben wird. Decay bedeutet, dass eine implizite Konvertierung Funktion-zu-Zeiger, Array-zu-Zeiger oder lvalue-zu-rvalue gegebenenfalls angewendet wird. Zusätzlich werden die Referenz eines Typs T und seine const/volatile Qualifizierer entfernt.
Hier ist der Screenshot des Programms aus C++ Insights.
Das bedeutet im Wesentlichen, dass die Größe des C-Arrays nicht bekannt ist.
Aber es gibt einen Trick. Wenn man das C-Array per Referenz nimmt und den Typ und die Größe des C-Arrays annimmt, ermittelt der Compiler seine Größe.
// typeDeductionArraySize.cpp
#include <cstddef>
#include <iostream>
template <typename T, std::size_t N>
std::size_t funcArraySize(T (&arr)[N]) {
return N;
}
int main() {
std::cout << '\n';
int intArray[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
funcArraySize(intArray);
std::cout << "funcArraySize(intArray): "
<< funcArraySize(intArray) << '\n';
std::cout << '\n';
}
Das Funktions-Template funcArraySize leitet die Größe des C-Arrays ab. Ich habe aus Gründen der Lesbarkeit dem C-Array Parameter den Namen arr gegeben: std::size_t funcArraySize(T (&arr)[N]). Dies ist nicht notwendig und du kannst einfach std::size_t funcArraySize(T (&)[N]) schreiben. Hier sind die Interna aus C++ Insights.
Zum Abschluss noch die Ausgabe des Programms:
Dieses Wissen über die automatische Bestimmung der Typen der Template-Argumente, lässt sich direkt auf auto (C++11) anwenden.
auto Typ Deduktion
Die auto Typ-Deduktion verwendet die Regeln der Template-Typ-Deduktion
Zur Erinnerung, dies sind die wesentlichen Entitäten der Template-Typ-Deduktion:
template <typname T>
void func(ParameterType param);
auto val = 2011;
Das Verständnis von auto bedeutet, dass auto als Ersatz für T und die Typspezifizierer von auto als Ersatz für den ParameterType in dem Funktions-Template zu betrachten ist.
Der Typspezifizierer kann ein Wert (1), eine Referenz (2) oder eine universelle Referenz (3) sein.
auto val = arg; // (1)
auto& val = arg; // (2)
auto&& val = arg; // (3)
Probieren wir es aus und ändern das vorherige Programm templateTypeDeduction.cpp und verwenden auto anstelle von Funktions-Templates.
// autoTypeDeduction.cpp
class RVal{};
int main() {
const int lVal{};
const int& ref = lVal;
auto val1 = lVal; // (1)
auto val2 = ref;
auto& val3 = lVal; // (2)
auto&& val4 = lVal; // (3)
auto&& val5 = RVal();
}
Beim Betrachten der resultierenden Typen in C++ Insights fällt auf, dass sie identisch mit den Typen sind, die im Programm templateTypeDeduction.cpp abgeleitet wurden.
NatĂĽrlich decayed auto auch, wenn es ein C-Array als Wert annimmt.
Das neue pdf-Bundle ist fertig: C++20 Coroutines
Ich habe das pdf-Bundle vorbereitet. Es zu erhalten ist ganz einfach. Ich verschicke automatisch bei der Anmeldung an meinen deutschen oder englischen Newsletter einen Link zu dem pdf-Bundle.
Hier gibt es mehr Informationen zu dem pdf-Bundle: C++ Coroutines.
Wie geht's weiter?
C++17 macht Typ-Deduktion mächtiger. Erstens ist eine automatische Typableitung für Nicht-Typ-Template-Parameter möglich und zweitens können Klassen-Templates auch ihre Argumente ableiten. Insbesondere die Klassen-Template-Argument-Deduktion macht das Programmiererleben viel einfacher. ()