Automatischer Rückgabetyp (C++98)

Modernes C++ Rainer Grimm  –  13 Kommentare

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.

Automatischer Rückgabetyp (C++98)

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.

Automatischer Rückgabetyp (C++98)

Die Addition von zwei doubles ergibt einen double, die Addition von double und bool ergibt einen bool und die Addition von zwei bools 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:

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.

Automatischer Rückgabetyp (C++98)

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 in einen double-Wert konvertiert und die volle Spezialisierung für <double, double> verwendet wird?

Automatischer Rückgabetyp (C++98)

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.

Automatischer Rückgabetyp (C++98)

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.