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:
T
ParameterType
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
expr
eine Referenz ist, wird die Referenz ignoriert =>newExpr
wird erzeugt - Wenn
newExpr
const
odervolatile
ist, wirdconst
odervolatile
ignoriert.
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
expr
eine Referenz ist, wird die Referenz ignoriert (aber letztendlich wieder hinzugefügt). - Wenn
expr
mit demParameterType
übereinstimmt, wird der resultierende Typ zu einer Referenz. Das bedeutet: - eine
expr
vom Typint
wird zu einemint&
- eine
expr
vom Typconst int
wird zu einemconst int&
- eine
expr
vom Typconst int&
wird zu einemconst int&
ParameterType ist eine Universelle Referenz (&&)
template <typename T>
void func(T&& param);
func(expr);
- Wenn
expr
einlvalue
ist, wird der resultierende Typ zu einer lvalue-Referenz. - Wenn
expr
einrvalue
ist, 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
funcValue
bewirken die gleiche Instanziierung der Funktions-Templates. Der abgeleitete Typ ist einint
.
- (2): Der Aufruf der Funktion
funcReference
mitconst int&
ergibt den Typconst int&
.
- (3): Der Aufruf der Funktion
funcUniversalReference
ergibt 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. ()