La risposta accettata a questa domanda sull'introspezione della funzione membro di compiletime, sebbene sia giustamente popolare, ha un problema che può essere osservato nel seguente programma:
#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;
}
Costruito con GCC 4.6.3, i risultati del programma 110
- ci informano che
T = std::shared_ptr<int>
fa non forniscono int & T::operator*() const
.
Se non sei già saggio con questo gotcha, allora uno sguardo alla definizione di
std::shared_ptr<T>
nell'intestazione <memory>
farà luce. In tale attuazione,std::shared_ptr<T>
deriva da una classe base da cui eredita operator*() const
. Quindi l'istanza del modello
SFINAE<U, &U::operator*>
che costituisce la "ricerca" per l'operatore
U = std::shared_ptr<T>
non avverrà, perché std::shared_ptr<T>
non ha alcun
operator*()
diritto e l'istanza del modello non "fa l'ereditarietà".
Questo intoppo non influisce sul noto approccio SFINAE, usando "The sizeof () Trick", per rilevare semplicemente se T
ha qualche funzione membro mf
(vedi ad es.
questa risposta e commenti). Ma stabilire che T::mf
esiste spesso (di solito?) Non è abbastanza buono: potresti anche aver bisogno di stabilire che ha una firma desiderata. È qui che segna la tecnica illustrata. La variante puntata della firma desiderata è inscritta in un parametro di un tipo di modello che deve essere soddisfatto
&T::mf
affinché la sonda SFINAE abbia successo. Ma questa tecnica di istanza di modello fornisce la risposta sbagliata quando T::mf
viene ereditata.
Una tecnica SFINAE sicura per l'introspezione di compiletime T::mf
deve evitare l'uso &T::mf
all'interno di un argomento template per creare un'istanza di un tipo da cui dipende la risoluzione del template della funzione SFINAE. Al contrario, la risoluzione della funzione del modello SFINAE può dipendere solo da dichiarazioni di tipo esattamente pertinenti utilizzate come tipi di argomento della funzione di sovraccarico SFINAE.
A titolo di risposta alla domanda che si attiene a questo vincolo, illustrerò per il rilevamento della compilazione di E T::operator*() const
, per arbitrario T
e E
. Lo stesso modello si applicherà, mutatis mutandis,
al probe per qualsiasi altra firma del metodo membro.
#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 questa soluzione, la funzione di sovraccarico della sonda SFINAE test()
viene "invocata in modo ricorsivo". (Ovviamente non è affatto invocato; ha semplicemente i tipi restituiti di invocazioni ipotetiche risolte dal compilatore.)
Dobbiamo esaminare almeno uno e al massimo due punti di informazione:
- fa
T::operator*()
affatto? Altrimenti, abbiamo finito.
- Dato che
T::operator*()
esiste, è la sua firma
E T::operator*() const
?
Otteniamo le risposte valutando il tipo di ritorno di una singola chiamata a test(0,0)
. Questo è fatto da:
typedef decltype(test<T>(0,0)) type;
Questa chiamata potrebbe essere risolta in /* SFINAE operator-exists :) */
sovraccarico test()
o potrebbe essere risolta in/* SFINAE game over :( */
sovraccarico. Non può risolvere il /* SFINAE operator-has-correct-sig :) */
sovraccarico, perché quello si aspetta solo un argomento e ne stiamo passando due.
Perché ne stiamo passando due? Semplicemente per forzare la risoluzione da escludere
/* SFINAE operator-has-correct-sig :) */
. Il secondo argomento non ha altro significato.
Questa chiamata a test(0,0)
si risolverà /* SFINAE operator-exists :) */
nel caso in cui il primo argomento 0 soddisfi il primo tipo di parametro di quel sovraccarico, ovvero decltype(&A::operator*)
con A = T
. 0 soddisferà quel tipo nel caso T::operator*
esista.
Supponiamo che il compilatore dica Sì a quello. Quindi sta andando avanti
/* SFINAE operator-exists :) */
e deve determinare il tipo di ritorno della chiamata di funzione, che in quel caso è decltype(test(&A::operator*))
- il tipo di ritorno di un'altra chiamata ancora test()
.
Questa volta, stiamo passando solo un argomento, &A::operator*
che ora sappiamo esiste, o non saremmo qui. Una chiamata a test(&A::operator*)
potrebbe risolversi in /* SFINAE operator-has-correct-sig :) */
o nuovamente in cui risolvere /* SFINAE game over :( */
. La chiamata corrisponderà
/* SFINAE operator-has-correct-sig :) */
nel caso in cui &A::operator*
soddisfi il tipo di parametro singolo di quel sovraccarico, che èE (A::*)() const
con A = T
.
Il compilatore dirà Sì qui se T::operator*
ha quella firma desiderata e quindi dovrà nuovamente valutare il tipo di ritorno del sovraccarico. Niente più "ricorsioni" ora: lo èstd::true_type
.
Se il compilatore non sceglie /* SFINAE operator-exists :) */
per la chiamata test(0,0)
o non sceglie /* SFINAE operator-has-correct-sig :) */
per la chiamata test(&A::operator*)
, in entrambi i casi va bene
/* SFINAE game over :( */
e il tipo di ritorno finale è std::false_type
.
Ecco un programma di test che mostra il modello che produce le risposte attese in vari esempi di casi (di nuovo 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;
}
Ci sono nuovi difetti in questa idea? Può essere reso più generico senza ricadere ancora una volta nel fallo che evita?