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.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 8 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

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.

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.

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 oder volatile ist, wird const oder volatile ignoriert.

Wenn der ParameterType eine Referenz oder eine universelle Referenz ist, wird die constness (oder volatileness) von expr beachtet.

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 dem ParameterType übereinstimmt, wird der resultierende Typ zu einer Referenz. Das bedeutet:
    • eine expr vom Typ int wird zu einem int&
    • eine expr vom Typ const int wird zu einem const int&
    • eine expr vom Typ const int& wird zu einem const int&
template <typename T>
void func(T&& param);

func(expr);
  • Wenn expr ein lvalue ist, wird der resultierende Typ zu einer lvalue-Referenz.
  • Wenn expr ein rvalue 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 ein int.
  • (2): Der Aufruf der Funktion funcReference mit const int& ergibt den Typ const 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).

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.

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.

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.

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. ()