Automatischer Rückgabetyp (C++98)
Je nach verwendetem C++-Standard gibt es verschiedene Möglichkeiten, den richtigen Rückgabetyp eines Funktions-Templates zu ermitteln.
- Rainer Grimm
Je nach verwendetem C++-Standard gibt es verschiedene Möglichkeiten, den richtigen Rückgabetyp eines Funktions-Templates zu ermitteln.
In diesem Artikel beginne ich mit Traits (C++98), fahre in meinem nächsten Artikel mit C++11/14 fort und schließe mit Concepts (C++20) ab.
Dies ist meine Herausforderung für den heutigen Artikel.
template <typename T, typename T2>
??? sum(T t, T2 t2) {
return t + t2;
}
Wer ein Funktions-Template wie sum
mit mindestens zwei Parametertypen definiert, kannt im Allgemeinen den Rückgabetyp nicht bestimmen. Natürlich sollte sum
den Typ zurückgeben, den die arithmetische Operation t + t2
liefert. Hier sind ein paar Beispiele für die Verwendung von Laufzeittypinformationen (RTTI) mit std::type_info
.
// typeinfo.cpp
#include <iostream>
#include <typeinfo>
int main() {
std::cout << '\n';
std::cout << "typeid(5.5 + 5.5).name(): "
<< typeid(5.5 + 5.5).name() << '\n';
std::cout << "typeid(5.5 + true).name(): "
<< typeid(5.5 + true).name() << '\n';
std::cout << "typeid(true + 5.5).name(): "
<< typeid(true + 5.5).name() << '\n';
std::cout << "typeid(true + false).name(): "
<< typeid(true + false).name() << '\n';
std::cout << '\n';
}
Ich habe das Programm auf Windows mit MSVC ausgeführt, weil MSVC im Gegensatz zu GCC oder Clang menschenlesbare Namen für die Datentypen erzeugt.
Die Addition von zwei double
s ergibt einen double
, die Addition von double
und bool
ergibt einen bool
und die Addition von zwei bool
s ergibt einen int
.
Ich verwende in meinen Beispielen nur arithmetische Typen. Wer benutzerdefinierte Typen anwenden willt, die arithmetische Operationen unterstützen, muss diese Umsetzung erweitern.
Jetzt beginnt meine Reise mit C++98.
C++98
Ehrlich gesagt bietet C++98 keinen allgemeine Ansatz für die Rückgabe des richtigen Typs an. Im Wesentlichen muss man die Regeln zur Typableitung mit einer Technik namens Traits, auch bekannt als Template Traits, implementieren. Eine Traits-Klasse liefert nützliche Informationen über Template-Parameter und kann anstelle der Template-Parameter verwendet werden.
Die folgende Klasse ResultType
bietet eine Typ-zu-Typ-Zuordnung unter Verwendung der vollständigen Template-Spezialisierung an.
// traits.cpp
#include <iostream>
#include <typeinfo>
template <typename T, typename T2> // primary template (1)
struct ReturnType;
template <> // full specialization for double, double
struct ReturnType <double, double> {
typedef double Type;
};
template <> // full specialization for double, bool
struct ReturnType <double, bool> {
typedef double Type; // (2)
};
template <> // full specialization for bool, double
struct ReturnType <bool, double> {
typedef double Type;
};
template <> // full specialization for bool, bool
struct ReturnType <bool, bool> {
typedef int Type;
};
template <typename T, typename T2>
typename ReturnType<T, T2>::Type sum(T t, T2 t2) { // (3)
return t + t2;
}
int main() {
std::cout << '\n';
std::cout << "typeid(sum(5.5, 5.5)).name(): "
<< typeid(sum(5.5, 5.5)).name() << '\n';
std::cout << "typeid(sum(5.5, true)).name(): "
<< typeid(sum(5.5, true)).name() << '\n';
std::cout << "typeid(sum(true, 5.5)).name(): "
<< typeid(sum(true, 5.5)).name() << '\n';
std::cout << "typeid(sum(true, false)).name(): "
<< typeid(sum(true, false)).name() << '\n';
std::cout << '\n';
}
(1) ist das primäre Template oder allgemeine Template. Das primäre Template muss vor den folgenden vollständigen Spezialisierungen deklariert werden. Wenn das primäre Template nicht benötigt wird, ist eine Deklaration wie in Zeile 1 ausreichend. Die folgenden Zeilen enthalten die vollständigen Spezialisierungen für <double, double>
, für <double, bool>
, für <bool, double>
und für <bool, bool>
. Mehr Details über die Template-Spezialisierung finden sich in meinen vorherigen Artikel:
- Einführung in die Template-Spezialisierung
- Template-Spezialisierung: Mehr Details über Klassen-Templates
- Vollständige Spezialisierung von Funktions-Templates
Die entscheidende Beobachtung bei den verschiedenen vollständigen Spezialisierungen von ReturnType
ist, dass sie alle einen Alias Type
besitzen, wie z. B. typedef double Type
(2). Dieser Alias ist der Rückgabetyp des Funktions-Templates sum
(3): typename ReturnType<T, T2>::Type
.
Die Traits verhalten sich wie erwartet.
Das wirft die Frage auf, warum ich typename
im Rückgabetyp-Ausdruck der Funktions-Templates sum
anwende. Ein Leser meines letzten Artikels über abhängige Namen hat mich gefragt, wann er typename
oder .template
in Templates verwenden soll. Die kurze Antwort ist, dass der Compiler nicht entscheiden kann, ob der Ausdruck ReturnType<T, T2>::Type
ein Typ (wie in diesem Fall), ein Nichttyp oder ein Template ist. Der Ausdruck typename
vor ReturnType<T, T2>::Type
gibt dem Compiler den entscheidenden Hinweis. Die lange Antwort kannst gibt mein vorheriger Artikel Abhängige Namen.
Fehlende Überladung
Ursprünglich wollte ich meinen Artikel mit C++11 fortsetzen. Ich nehme aber an, es gibt noch eine weitere Frage: Was passiert, wenn ich die Funktions-Templates sum mit Argumenten aufrufe, für die keine partielle Template-Spezialisierung definiert ist? Lass es mich mit sum(5.5f, 5)
ausprobieren.
// traitsError.cpp
#include <iostream>
#include <typeinfo>
template <typename T, typename T2> // primary template
struct ReturnType;
template <> // full specialization for double, double
struct ReturnType <double, double> {
typedef double Type;
};
template <> // full specialization for double, bool
struct ReturnType <double, bool> {
typedef double Type;
};
template <> // full specialization for bool, double
struct ReturnType <bool, double> {
typedef double Type;
};
template <> // full specialization for bool, bool
struct ReturnType <bool, bool> {
typedef int Type;
};
template <typename T, typename T2>
typename ReturnType<T, T2>::Type sum(T t, T2 t2) {
return t + t2;
}
int main() {
std::cout << '\n';
std::cout << "typeid(sum(5.5f, 5.5)).name(): "
<< typeid(sum(5.5f, 5.5)).name() << '\n';
std::cout << '\n';
}
Viele C++ Programmierer erwarten, dass der float
-Wert 5.5f i
n einen double
-Wert konvertiert und die volle Spezialisierung für <double, double>
verwendet wird?
NEIN! Die Typen müssen genau übereinstimmen. Der MSVC-Compiler gibt eine genaue Fehlermeldung aus. Es ist keine Überladung sum
für T = float
und T2 = double
verfügbar. Das primäre Template ist nicht definiert und kann daher nicht instanziiert werden.
Typen werden nicht konvertiert, nur Ausdrücke wie Werte können konvertiert werden: double res = 5.5f + 5.5;
Default-Rückgabetyp
Wenn man die Deklaration des primären Templates als Grundlage für eine Definition verwendet, wird das primäre Template zum Default-Fall. Folglich verwendet die folgende Implementierung von ReturnType long double
als Default-Rückgabetyp.
// traitsDefault.cpp
#include <iostream>
#include <typeinfo>
template <typename T, typename T2> // primary template
struct ReturnType {
typedef long double Type;
};
template <> // full specialization for double, double
struct ReturnType <double, double> {
typedef double Type;
};
template <> // full specialization for double, bool
struct ReturnType <double, bool> {
typedef double Type;
};
template <> // full specialization for bool, double
struct ReturnType <bool, double> {
typedef double Type;
};
template <> // full specialization for bool, bool
struct ReturnType <bool, bool> {
typedef int Type;
};
template <typename T, typename T2>
typename ReturnType<T, T2>::Type sum(T t, T2 t2) {
return t + t2;
}
int main() {
std::cout << '\n';
std::cout << "typeid(sum(5.5, 5.5)).name(): "
<< typeid(sum(5.5, 5.5)).name() << '\n';
std::cout << "typeid(sum(5.5, true)).name(): "
<< typeid(sum(5.5, true)).name() << '\n';
std::cout << "typeid(sum(true, 5.5)).name(): "
<< typeid(sum(true, 5.5)).name() << '\n';
std::cout << "typeid(sum(true, false)).name(): "
<< typeid(sum(true, false)).name() << '\n';
std::cout << "typeid(sum(5.5f, 5.5)).name(): "
<< typeid(sum(5.5f, 5.5)).name() << '\n';
std::cout << '\n';
}
Der Aufruf von sum(5.5f, 5.f
) bewirkt die Instanziierung der primären Template.
Wie geht's weiter?
In C++11 gibt es verschiedene Möglichkeiten, den Rückgabetyp automatisch abzuleiten. C++14 fügt diesen Techniken syntaktischen Zucker hinzu und C++20 ermöglicht es, sie sehr explizit zu schreiben. Diese Verbesserungen behandelt mein nächster Artikel. ()