La respuesta aceptada a esta pregunta de la introspección de la función miembro de compiletime, aunque es bastante popular, tiene un inconveniente que se puede observar en el siguiente programa:
#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;
}
Construido con GCC 4.6.3, los productos del programa 110
- para informarnos que
T = std::shared_ptr<int>
hace no proporcionan int & T::operator*() const
.
Si aún no es sabio con este problema, una mirada a la definición de
std::shared_ptr<T>
en el encabezado <memory>
arrojará luz. En esa implementación, std::shared_ptr<T>
se deriva de una clase base de la que hereda operator*() const
. Por lo tanto, la creación de instancias de plantilla
SFINAE<U, &U::operator*>
que constituye "encontrar" el operador para
U = std::shared_ptr<T>
no ocurrirá, porque std::shared_ptr<T>
no tiene
operator*()
por derecho propio y la creación de instancias de plantilla no "hace herencia".
Este inconveniente no afecta el conocido enfoque SFINAE, que utiliza "El truco sizeof ()", para detectar simplemente si T
tiene alguna función miembro mf
(ver, por ejemplo,
esta respuesta y comentarios). Pero establecer que T::mf
existe a menudo (¿generalmente?) No es lo suficientemente bueno: es posible que también deba establecer que tiene la firma deseada. Ahí es donde puntúa la técnica ilustrada. La variante punteada de la firma deseada se inscribe en un parámetro de un tipo de plantilla que debe ser satisfecho
&T::mf
para que la sonda SFINAE tenga éxito. Pero esta técnica de creación de instancias de plantilla da la respuesta incorrecta cuando T::mf
se hereda.
Una técnica segura de SFINAE para la introspección en tiempo de compilación T::mf
debe evitar el uso &T::mf
dentro de un argumento de plantilla para crear una instancia de un tipo del que depende la resolución de la plantilla de función SFINAE. En cambio, la resolución de la función de plantilla SFINAE puede depender solo de las declaraciones de tipo exactamente pertinentes utilizadas como tipos de argumento de la función de sonda SFINAE sobrecargada.
A modo de respuesta a la pregunta que cumple con esta restricción, ilustraré la detección en tiempo de compilación de E T::operator*() const
, para arbitraria T
y E
. El mismo patrón se aplicará mutatis mutandis
para buscar cualquier otra firma de método miembro.
#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? */
};
En esta solución, la función de sonda SFINAE sobrecargada test()
se "invoca de forma recursiva". (Por supuesto, en realidad no se invoca en absoluto; simplemente tiene los tipos de retorno de invocaciones hipotéticas resueltas por el compilador).
Necesitamos investigar al menos uno y como máximo dos puntos de información:
- ¿Existe
T::operator*()
en absoluto? Si no, hemos terminado.
- Dado que
T::operator*()
existe, ¿es su firma
E T::operator*() const
?
Obtenemos las respuestas evaluando el tipo de retorno de una sola llamada a test(0,0)
. Eso lo hace:
typedef decltype(test<T>(0,0)) type;
Esta llamada puede resolverse a la /* SFINAE operator-exists :) */
sobrecarga de test()
, o puede resolverse a la /* SFINAE game over :( */
sobrecarga. No se puede resolver la /* SFINAE operator-has-correct-sig :) */
sobrecarga, porque uno espera solo un argumento y estamos pasando dos.
¿Por qué estamos pasando dos? Simplemente para forzar la resolución a excluir
/* SFINAE operator-has-correct-sig :) */
. El segundo argumento no tiene otro significado.
Esta llamada a test(0,0)
se resolverá /* SFINAE operator-exists :) */
en caso de que el primer argumento 0 satifique el primer tipo de parámetro de esa sobrecarga, que es decltype(&A::operator*)
, con A = T
. 0 satisfará ese tipo en caso de que T::operator*
exista.
Supongamos que el compilador dice Sí a eso. Luego continúa
/* SFINAE operator-exists :) */
y necesita determinar el tipo de retorno de la llamada de función, que en ese caso es decltype(test(&A::operator*))
: el tipo de retorno de otra llamada más test()
.
Esta vez, estamos pasando un solo argumento, &A::operator*
que ahora sabemos que existe, o no estaríamos aquí. Una llamada a test(&A::operator*)
podría resolverse ao /* SFINAE operator-has-correct-sig :) */
nuevamente o podría resolverse a /* SFINAE game over :( */
. La llamada coincidirá
/* SFINAE operator-has-correct-sig :) */
en caso de que &A::operator*
satisfaga el tipo de parámetro único de esa sobrecarga, que es E (A::*)() const
, con A = T
.
El compilador dirá Sí aquí si T::operator*
tiene esa firma deseada, y luego nuevamente tendrá que evaluar el tipo de retorno de la sobrecarga. No más "recurrencias" ahora: lo es std::true_type
.
Si el compilador no elige /* SFINAE operator-exists :) */
para la llamada test(0,0)
o no elige /* SFINAE operator-has-correct-sig :) */
para la llamada test(&A::operator*)
, entonces en cualquier caso va con
/* SFINAE game over :( */
y el tipo de retorno final es std::false_type
.
Aquí hay un programa de prueba que muestra la plantilla que produce las respuestas esperadas en una muestra variada de casos (GCC 4.6.3 nuevamente).
// 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;
}
¿Hay nuevos defectos en esta idea? ¿Se puede hacer más genérico sin volver a caer en el obstáculo que evita?