Die akzeptierte Antwort auf diese Frage der Introspektion von Compiletime-Member-Funktionen ist zwar zu Recht beliebt, weist jedoch einen Haken auf, der im folgenden Programm beobachtet werden kann:
#include <type_traits>
#include <iostream>
#include <memory>
/* Here we apply the accepted answer's technique to probe for the
the existence of `E T::operator*() const`
*/
template<typename T, typename E>
struct has_const_reference_op
{
template<typename U, E (U::*)() const> struct SFINAE {};
template<typename U> static char Test(SFINAE<U, &U::operator*>*);
template<typename U> static int Test(...);
static const bool value = sizeof(Test<T>(0)) == sizeof(char);
};
using namespace std;
/* Here we test the `std::` smart pointer templates, including the
deprecated `auto_ptr<T>`, to determine in each case whether
T = (the template instantiated for `int`) provides
`int & T::operator*() const` - which all of them in fact do.
*/
int main(void)
{
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value << endl;
return 0;
}
Errichtet mit GCC 4.6.3, die Programmausgaben 110
- informiert uns , dass
T = std::shared_ptr<int>
sich nicht sorgen int & T::operator*() const
.
Wenn Sie mit diesem Fall noch nicht vertraut sind, wird ein Blick auf die Definition von
std::shared_ptr<T>
in der Kopfzeile <memory>
Licht ins Dunkel bringen. Wird in dieser Implementierung std::shared_ptr<T>
von einer Basisklasse abgeleitet, von der es erbt operator*() const
. Die Vorlageninstanziierung
SFINAE<U, &U::operator*>
, die das "Finden" des Operators für darstellt,
U = std::shared_ptr<T>
wird also nicht stattfinden, da std::shared_ptr<T>
sie kein
operator*()
eigenes hat und die Vorlageninstanziierung keine "Vererbung" durchführt.
Dieser Haken wirkt sich nicht auf den bekannten SFINAE-Ansatz aus, bei dem "The sizeof () Trick" verwendet wird, um lediglich festzustellen, ob T
eine Elementfunktion vorliegt mf
(siehe z. B.
diese Antwort und Kommentare). Aber festzustellen, dass es T::mf
existiert, ist oft (normalerweise?) Nicht gut genug: Möglicherweise müssen Sie auch feststellen, dass es eine gewünschte Signatur hat. Hier punktet die illustrierte Technik. Die mit Zeigern versehene Variante der gewünschten Signatur wird in einen Parameter eines Vorlagentyps eingeschrieben, der erfüllt sein muss,
&T::mf
damit die SFINAE-Sonde erfolgreich ist. Diese Technik zur Instanziierung von Vorlagen gibt jedoch die falsche Antwort, wenn sie T::mf
vererbt wird.
Eine sichere SFINAE-Technik für die Introspektion während der Kompilierung T::mf
muss die Verwendung &T::mf
eines Template-Arguments vermeiden , um einen Typ zu instanziieren, von dem die Auflösung von SFINAE-Funktionsvorlagen abhängt. Stattdessen kann die Auflösung der SFINAE-Vorlagenfunktion nur von genau relevanten Typdeklarationen abhängen, die als Argumenttypen der überladenen SFINAE-Testfunktion verwendet werden.
Als Antwort auf die Frage, die diese Einschränkung einhält, werde ich zur Erkennung der Kompilierungszeit E T::operator*() const
, für willkürliche T
und E
. Das gleiche Muster wird entsprechend angewendet
, um nach Signaturen anderer Mitgliedsmethoden zu suchen.
#include <type_traits>
/*! The template `has_const_reference_op<T,E>` exports a
boolean constant `value that is true iff `T` provides
`E T::operator*() const`
*/
template< typename T, typename E>
struct has_const_reference_op
{
/* SFINAE operator-has-correct-sig :) */
template<typename A>
static std::true_type test(E (A::*)() const) {
return std::true_type();
}
/* SFINAE operator-exists :) */
template <typename A>
static decltype(test(&A::operator*))
test(decltype(&A::operator*),void *) {
/* Operator exists. What about sig? */
typedef decltype(test(&A::operator*)) return_type;
return return_type();
}
/* SFINAE game over :( */
template<typename A>
static std::false_type test(...) {
return std::false_type();
}
/* This will be either `std::true_type` or `std::false_type` */
typedef decltype(test<T>(0,0)) type;
static const bool value = type::value; /* Which is it? */
};
In dieser Lösung wird die überladene SFINAE-Sondenfunktion test()
"rekursiv aufgerufen". (Natürlich wird es überhaupt nicht aufgerufen; es enthält lediglich die vom Compiler aufgelösten Rückgabetypen hypothetischer Aufrufe.)
Wir müssen nach mindestens einem und höchstens zwei Informationspunkten suchen:
- Existiert
T::operator*()
überhaupt? Wenn nicht, sind wir fertig.
- Ist
T::operator*()
seine Unterschrift gegeben
E T::operator*() const
?
Wir erhalten die Antworten, indem wir den Rückgabetyp eines einzelnen Anrufs an bewerten test(0,0)
. Das wird gemacht von:
typedef decltype(test<T>(0,0)) type;
Dieser Aufruf wird möglicherweise in die /* SFINAE operator-exists :) */
Überlastung von test()
oder in die /* SFINAE game over :( */
Überlastung aufgelöst. Es kann nicht zur /* SFINAE operator-has-correct-sig :) */
Überlastung aufgelöst werden, da dieses nur ein Argument erwartet und wir zwei übergeben.
Warum kommen wir an zwei vorbei? Einfach, um die Auflösung zum Ausschluss zu zwingen
/* SFINAE operator-has-correct-sig :) */
. Das zweite Argument hat keine andere Bedeutung.
Dieser Aufruf von test(0,0)
wird für den /* SFINAE operator-exists :) */
Fall aufgelöst, dass das erste Argument 0 den ersten Parametertyp dieser Überladung erfüllt, dh decltype(&A::operator*)
mit A = T
. 0 wird diesen Typ nur für den Fall erfüllen, dass er T::operator*
existiert.
Nehmen wir an, der Compiler sagt Ja dazu. Dann geht es weiter
/* SFINAE operator-exists :) */
und es muss der Rückgabetyp des Funktionsaufrufs bestimmt werden, der in diesem Fall decltype(test(&A::operator*))
- der Rückgabetyp eines weiteren Aufrufs an ist test()
.
Dieses Mal übergeben wir nur ein Argument, von &A::operator*
dem wir jetzt wissen, dass es existiert, oder wir wären nicht hier. Ein Aufruf von test(&A::operator*)
kann entweder zu /* SFINAE operator-has-correct-sig :) */
oder erneut zu aufgelöst werden /* SFINAE game over :( */
. Der Aufruf stimmt /* SFINAE operator-has-correct-sig :) */
nur für den Fall überein,
&A::operator*
dass der einzelne Parametertyp dieser Überlastung erfüllt ist, dh E (A::*)() const
mit A = T
.
Der Compiler sagt hier Ja, wenn er T::operator*
die gewünschte Signatur hat, und muss dann erneut den Rückgabetyp der Überladung auswerten. Keine "Rekursionen" mehr: es ist std::true_type
.
Wenn der Compiler nicht /* SFINAE operator-exists :) */
für den Aufruf test(0,0)
oder nicht /* SFINAE operator-has-correct-sig :) */
für den Aufruf wählt test(&A::operator*)
, ist dies in beiden Fällen der Fall
/* SFINAE game over :( */
und der endgültige Rückgabetyp ist std::false_type
.
Hier ist ein Testprogramm, das die Vorlage zeigt, die die erwarteten Antworten in verschiedenen Fallbeispielen liefert (erneut GCC 4.6.3).
// To test
struct empty{};
// To test
struct int_ref
{
int & operator*() const {
return *_pint;
}
int & foo() const {
return *_pint;
}
int * _pint;
};
// To test
struct sub_int_ref : int_ref{};
// To test
template<typename E>
struct ee_ref
{
E & operator*() {
return *_pe;
}
E & foo() const {
return *_pe;
}
E * _pe;
};
// To test
struct sub_ee_ref : ee_ref<char>{};
using namespace std;
#include <iostream>
#include <memory>
#include <vector>
int main(void)
{
cout << "Expect Yes" << endl;
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value;
cout << has_const_reference_op<std::vector<int>::iterator,int &>::value;
cout << has_const_reference_op<std::vector<int>::const_iterator,
int const &>::value;
cout << has_const_reference_op<int_ref,int &>::value;
cout << has_const_reference_op<sub_int_ref,int &>::value << endl;
cout << "Expect No" << endl;
cout << has_const_reference_op<int *,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,char &>::value;
cout << has_const_reference_op<unique_ptr<int>,int const &>::value;
cout << has_const_reference_op<unique_ptr<int>,int>::value;
cout << has_const_reference_op<unique_ptr<long>,int &>::value;
cout << has_const_reference_op<int,int>::value;
cout << has_const_reference_op<std::vector<int>,int &>::value;
cout << has_const_reference_op<ee_ref<int>,int &>::value;
cout << has_const_reference_op<sub_ee_ref,int &>::value;
cout << has_const_reference_op<empty,int &>::value << endl;
return 0;
}
Gibt es neue Mängel in dieser Idee? Kann es generischer gemacht werden, ohne erneut den Haken zu verlieren, den es vermeidet?