Die automatisch Bestimmung der Template Argumente von Klassen-Templates

Modernes C++ Rainer Grimm  –  0 Kommentare

In meinem letzten Beitrag Template Arguments habe ich über die Typbestimmung von Funktions-Templates (C++98) und auto (C++11) geschrieben. Heute setze ich mir einen moderneren Hut auf. Ich beginne mit der automatischen Typbestimmung von Nicht-Template-Parametern, Klassen-Templates (C++17) und schließe mit der automatischen Typbestimmung von Concepts (C++20) ab.

Die Bestimmung der Template Argumente von Klassen-Templates

Der chronologischen Reihenfolge folgend, möchte ich mit zwei C++17-Features beginnen: Typbestimmung von Nicht-Typ-Template-Parametern und Typbestimmung von Klassen-Templates in C++17.

Automatische Typbestimmung von Nicht-Typ-Template-Parametern

Zuallererst, was sind Nicht-Typ-Template-Parameter? Das sind nullptr, ganzzahlige Werte wie zum Beispiel bool oder int, lvalue Referenzen, Zeiger, Aufzähler und mit C++20 Fließkommazahlen. Am häufigsten werden ganzzahlige Typen verwendet.

Nach dieser Theorie fahre ich mit einem Beispiel fort:

template <auto N> // (1)
class MyClass{
....
};

template <int N> // (2)
class MyClass<N> {
....
};


MyClass<'x'> myClass1; // (3)
MyClass<2017> myClass2; // (4)

Durch die Verwendung von auto (1) in der Template-Signatur ist N ein Nicht-Typ-Template-Parameter. Der Compiler wird ihn automatisch ableiten.MyClass lässt sich auch für int partiell spezialisieren (2). Die Template-Instanziierung (3) wird das primäre Template (1) verwenden und die folgende Template-Instanziierung die partielle Spezialisierung für int (4).

Die üblichen Typqualifizierer können verwendet werden, um den Typ der Nicht-Typ-Template-Parameter einzuschränken.

template <const auto* p>
class S;

In dieser Deklaration eines Klassen-Template S, muss p ein Zeiger auf const sein.

Die automatische Typbestimmung für Nicht-Typ-Templates lässt sich auch auf variadische Templates anwenden.

template <auto... ns>
class VariadicTemplate{ .... };

template <auto n1, decltype(n1)... ns>
class TypedVariadicTemplate{ .... };

VariadicTemplate kann eine beliebige Anzahl von Nicht-Typ-Template-Parametern ableiten. TypedVariadicTemplate wird nur den ersten Template-Parameter bestimmen. Die restlichen Template-Parameter müssen vom gleichen Typ wie der erste Typ sein: decltype(n1).

Die automatische Typbestimmung von Klassen-Templates macht ihre Verwendung recht komfortabel.

Automatische Typbestimmung von Klassen-Templates

Ein Funktions-Template kann seine Typparameter aus seinen Funktionsargumenten automatisch bestimmen. Aber das war für spezielle Funktionen nicht möglich: Konstruktoren von Klassen-Templates. Mit C++17 ist diese Aussage falsch. Ein Konstruktor kann seine Typparameter von seinen Konstruktorargumenten ableiten. Hier ist ein erstes Beispiel:

// templateArgumentDeduction.cpp

#include <iostream>

template <typename T>
void showMe(const T& t) {
std::cout << t << '\n';
}

template <typename T>
struct ShowMe{
ShowMe(const T& t) {
std::cout << t << '\n';
}
};

int main() {

std::cout << '\n';

showMe(5.5); // not showMe<double>(5.5);
showMe(5); // not showMe<int>(5);

ShowMe(5.5); // not ShowMe<double>(5.5);
ShowMe(5); // not ShowMe<int>(5);

std::cout << '\n';

}

Nun noch ein paar Worte zur main-Funktion. Die Instanziierung des Funktions-Templates showMe ist seit dem ersten C++ Standard C++98 gültig, die Instanziierung der Klassen-Templates ShowMe jedoch erst seit C++17. Aus Benutzersicht fühlt sich die Verwendung von Funktions- oder Klassen-Templates genauso an wie eine gewöhnliche Funktion oder Klasse.

Die automatisch Bestimmung der Template Argumente von Klassen-Templates

Wer noch nicht überzeugt ist, findet hier weitere Beispiele für die Ableitung der Argumente von Klassen-Templates:

// classTemplateArgumentDeduction.cpp

#include <array>
#include <Vektor>
#include <mutex>
#include <memory>

int main() {

std::array myArr{1, 2, 3}; // deduces std::array<int, 3>
std::vector myVec{1.5, 2.5}; // deduces std::vector<double>

std::mutex mut;
std::lock_guard myLock(mut); // deduces std::lock_guard<mutex>(mut)

std::pair myPair(5, 5.5); // deduces std::pair<int, double>
std::tuple myTup(5, myArr, myVec); // deduces std::tuple<int,
// std::array<int, 3>,
// std::vector<double>>
}

Die Kommentare zeigen, wie der C++17 Compiler den Typ bestimmt. Dank C++ Insights lässt sich der Prozess der Bestimmung von Template-Argumenten visualisieren.

Die letzten beiden Beispiele zu std::pair und std::tuple sind ziemlich interessant. Vor C++17 haben wir Fabrikfunktionen wie std::make_pair oder std::make_tuple verwendet, um ein std::pair oder ein std::tuple zu erzeugen, ohne die Typparameter anzugeben. Im Gegensatz zu Klassen-Templates konnte der Compiler die Typparameter automatisch aus den Funktionsargumenten ableiten. Hier ist eine vereinfachte Version von std::make_pair.

// makePair.cpp

#include <utility>

template<typename T1, typename T2>
std::pair<T1, T2> make_pair2(T1 t1, T2 t2) {
return std::pair<T1, T2>(t1, t2);
}

int main() {

auto arg{5.5};
auto pair1 = std::make_pair(5, arg);
auto pair2 = make_pair2(5, arg);
auto pair3 = std::pair(5, arg);

}

Der Compiler bestimmt die gleichen Datentypen für pair1 und pair2. Mit C++17 brauchen wir diese Fabrikfunktion nicht mehr und können direkt den Konstruktor von std::pair aufrufen, um pair3 zu erhalten.

Die automatisch Bestimmung der Template Argumente von Klassen-Templates

Das Programm lässt sich direkt auf C++ Insights studieren.

Es mag verwundern, dass mein Funktions-Template make_pair2 seine Argumente per Wert angenommen hat. std::make_pair decayed seine Argumente und so auch mein Funktions-Template make_pair2. Ich habe über den Decay von Funktionsargumenten in meinem letzten Beitrag Template Arguments geschrieben.

Bevor ich ein paar Worte über die automatische Typbestimmung mit Concepts schreibe, möchte ich es explizit betonen: Die automatische Typbestimmung ist mehr als praktisch und sie ist ein Sicherheitsfeature. Wer den Datentyp nicht angibt, kann dabei keinen Fehler machen.

// automaticTypeDeduction.cpp

#include <string>

template<typename T>
void func(T) {};

template <typename T>
struct Class{
Class(T){}
};

int main() {

int a1 = 5.5; // static_cast<int>(5.5)
auto a2 = 5.5;

func<float>(5.5); // static_cast<float>(5.5)
func(5.5);

Class<std::string> class1("class"); // calls essentially
// std::string("class")
Class class2("class");

}

Alle Fehler sind darauf zurückzuführen, dass ich den Datentyp explizit angegeben habe:

  • int a1 löst die narrowing conversion von double nach int aus
  • func<float>(5.5) bewirkt die Umwandlung von dem double-Wert 5.5 in float
  • Class<std::string> class1("class") erzeugt einen C++-String, der mit einem C-String initialisiert wird.

Auf C++ Insights lässt sich das Programm wieder schön studieren.

Der Geschichte der automatischen Typendeduktion ist nicht viel hinzuzufügen, wenn Concepts ins Spiel kommen.

Automatische Typbestimmung mit Concepts

Die automatische Typbestimmung mit Concepts birgt keine Überraschungen.

// typeDeductionConcepts.cpp

#include <concepts>

void foo(auto t) {} // (1)

void bar(std::integral auto t){} // (2)

template <std::regular T> // (3)
struct Class{
Class(T){}
};

int main() {

foo(5.5);
bar(5);
Class cl(true);

}

Unabhängig davon, ob ein uneingeschränkter Platzhalter (auto in Zeile 1), ein eingeschränkter Platzhalter (Concept in Zeile 2) oder ein eingeschränkter Template-Parameter (Concept in Zeile 3) zum Einsatz kommt, bestimmt der Compiler automatisch den erwarteten Datentyp. C++ Insights hilft dabei, die Typbestimmung zu visualisieren.

Die automatisch Bestimmung der Template Argumente von Klassen-Templates


Wie geht's weiter?

In meinem nächsten Artikel schreibe ich über ein weiteres spannendes Templatefeature: Spezialisierung. Man kann ein Funktions-Template oder Klassen-Template vollständig spezialisieren, und zusätzlich lässt es sich partiell spezialisieren.