¿Cómo detectar si hay una variable miembro específica en la clase?

Resuelto Kirill V. Lyadvinsky asked hace 15 años • 10 respuestas

Para crear una función de plantilla de algoritmo, necesito saber si x o X (y y o Y) en la clase que es el argumento de la plantilla. Puede resultar útil cuando uso mi función para la clase MFC CPoint o la clase GDI+ PointF o algunas otras. Todos ellos usan diferentes x en ellos. Mi solución podría reducirse al siguiente código:


template<int> struct TT {typedef int type;};
template<class P> bool Check_x(P p, typename TT<sizeof(&P::x)>::type b = 0) { return true; }
template<class P> bool Check_x(P p, typename TT<sizeof(&P::X)>::type b = 0) { return false; }

struct P1 {int x; };
struct P2 {float X; };
// it also could be struct P3 {unknown_type X; };

int main()
{
    P1 p1 = {1};
    P2 p2 = {1};

    Check_x(p1); // must return true
    Check_x(p2); // must return false

    return 0;
}

Pero no se compila en Visual Studio, mientras que se compila en GNU C++. Con Visual Studio podría usar la siguiente plantilla:


template<class P> bool Check_x(P p, typename TT<&P::x==&P::x>::type b = 0) { return true; }
template<class P> bool Check_x(P p, typename TT<&P::X==&P::X>::type b = 0) { return false; }

Pero no se compila en GNU C++. ¿Existe una solución universal?

UPD: Las estructuras P1 y P2 aquí son solo a modo de ejemplo. Podría haber clases con miembros desconocidos.

PD: Por favor, no publique soluciones de C++ 11 aquí porque son obvias y no relevantes para la pregunta.

Kirill V. Lyadvinsky avatar Jun 17 '09 13:06 Kirill V. Lyadvinsky
Aceptado

He aquí una solución más sencilla que la de Johannes Schaub-litb . Requiere C++11.

#include <type_traits>

template <typename T, typename = int>
struct HasX : std::false_type { };

template <typename T>
struct HasX <T, decltype((void) T::x, 0)> : std::true_type { };

Actualización : un ejemplo rápido y una explicación de cómo funciona.

Para estos tipos:

struct A { int x; };
struct B { int y; };

tenemos HasX<A>::value == truey HasX<B>::value == false. Veamos por qué.

Primero recuerde eso std::false_typey std::true_typetenga un static constexpr boolmiembro nombrado valueque esté configurado en falsey true, respectivamente. Por lo tanto, las dos plantillas HasXanteriores heredan este miembro. (La primera plantilla de std::false_typey la segunda de std::true_type).

Comencemos de manera simple y luego avancemos paso a paso hasta llegar al código anterior.

1) Punto de partida:

template <typename T, typename U>
struct HasX : std::false_type { };

En este caso, no hay sorpresa: HasXderiva de std::false_typey por tanto HasX<bool, double>::value == falsey HasX<bool, int>::value == false.

2) Incumplimiento U:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

Dado que Uel valor predeterminado es int, Has<bool>en realidad significa HasX<bool, int>y, por lo tanto HasX<bool>::value == HasX<bool, int>::value == false,.

3) Agregar una especialización:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

// Specialization for U = int
template <typename T>
struct HasX<T, int> : std::true_type { };

En general, gracias a la plantilla primaria, HasX<T, U>se deriva de std::false_type. Sin embargo, existe una especialización que U = intderiva de std::true_type. Por lo tanto, HasX<bool, double>::value == falsepero HasX<bool, int>::value == true.

Gracias al valor predeterminado para U, HasX<bool>::value == HasX<bool, int>::value == true.

4) decltypey una forma elegante de decir int:

Una pequeña digresión aquí pero, por favor, tengan paciencia.

Básicamente (esto no es del todo correcto), decltype(expression)produce el tipo de expresión . Por ejemplo, 0tiene tipo, intpor lo tanto, decltype(0)significa int. De manera análoga, 1.2tiene tipo doubley por tanto, decltype(1.2)significa double.

Considere una función con esta declaración:

char func(foo, int);

¿ Dónde foohay algún tipo de clase? Si fes un objeto de tipo foo, entonces decltype(func(f, 0))significa char(el tipo devuelto por func(f, 0)).

Ahora, la expresión (1.2, 0)utiliza el operador de coma (integrado) que evalúa las dos subexpresiones en orden (es decir, primero 1.2y luego 0), descarta el primer valor y da como resultado el segundo. Por eso,

int x = (1.2, 0);

es equivalente a

int x = 0;

Poner esto junto con decltypeda eso decltype(1.2, 0)significa int. No hay nada realmente especial sobre 1.2o doubleaquí. Por ejemplo, también truetiene tipo booly decltype(true, 0)medio .int

¿Qué pasa con un tipo de clase? Por ejemplo, ¿qué decltype(f, 0)significa? Es natural esperar que esto siga significando int, pero puede que no sea el caso. De hecho, podría haber una sobrecarga para el operador de coma similar a la función funcanterior que toma a fooy an inty devuelve a char. En este caso, decltype(foo, 0)es char.

¿Cómo podemos evitar el uso de una sobrecarga para el operador de coma? Bueno, no hay manera de sobrecargar el operador de coma para un voidoperando y podemos convertir cualquier cosa a void. Por lo tanto, decltype((void) f, 0)significa int. De hecho, (void) flas conversiones ffrom footo voidbásicamente no hacen más que decir que se debe considerar que la expresión tiene tipo void. Luego se utiliza la coma del operador incorporado y ((void) f, 0)el resultado es 0que tiene tipo int. Por lo tanto, decltype((void) f, 0)significa int.

¿Es realmente necesario este elenco? Bueno, si no hay sobrecarga para el operador de coma foo, intentonces esto no es necesario. Siempre podemos inspeccionar el código fuente para ver si existe dicho operador o no. Sin embargo, si esto aparece en una plantilla y ftiene un tipo Vque es un parámetro de plantilla, entonces ya no está claro (o incluso es imposible saberlo) si dicha sobrecarga para el operador de coma existe o no. Para ser genéricos, lanzamos de todos modos.

En pocas palabras: decltype((void) f, 0)es una forma elegante de decir int.

5) SFINAE:

Esto es toda una ciencia ;-) Vale, estoy exagerando pero tampoco es muy sencillo. Así que mantendré la explicación al mínimo.

SFINAE significa que la falla de sustitución no es un error. Significa que cuando un parámetro de plantilla se sustituye por un tipo, puede aparecer un código C++ ilegal pero, en algunas circunstancias , en lugar de abortar la compilación, el compilador simplemente ignora el código infractor como si no estuviera allí. Veamos cómo se aplica a nuestro caso:

// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };

// Specialization for U = int
template <typename T>
struct HasX <T, decltype((void) T::x, 0)> : std::true_type { };

Aquí, nuevamente, decltype((void) T::x, 0)hay una forma elegante de decirlo intpero con el beneficio de SFINAE.

Cuando Tse sustituye por un tipo, puede aparecer una construcción no válida. Por ejemplo, bool::xC++ no es válido, por lo que sustituirlo Tcon boolin T::xproduce una construcción no válida. Según el principio SFINAE, el compilador no rechaza el código, simplemente lo ignora (partes de él). Más precisamente, como hemos visto HasX<bool>significa en realidad HasX<bool, int>. U = intSe debe seleccionar la especialización para pero, al crear una instancia, el compilador encuentra bool::xe ignora la especialización de la plantilla por completo como si no existiera.

En este punto, el código es esencialmente el mismo que en el caso (2) anterior, donde solo existe la plantilla principal. Por eso, HasX<bool, int>::value == false.

El mismo argumento utilizado para boolse cumple Bdesde que B::xes una construcción no válida ( Bno tiene miembro x). Sin embargo, A::xestá bien y el compilador no ve ningún problema en crear una instancia de la especialización para U = int(o, más precisamente, para U = decltype((void) A::x, 0)). Por eso, HasX<A>::value == true.

6) Sin nombre U:

Bueno, mirando el código en (5) nuevamente, vemos que el nombre Uno se usa en ningún otro lugar excepto en su declaración ( typename U). Luego podemos quitarle el nombre al segundo argumento de la plantilla y obtenemos el código que se muestra en la parte superior de esta publicación.

Cassio Neri avatar Apr 14 '2013 14:04 Cassio Neri

Otra forma es ésta, que también se basa en SFINAE para las expresiones . Si la búsqueda de nombres resulta ambigüedad, el compilador rechazará la plantilla.

template<typename T> struct HasX { 
    struct Fallback { int x; }; // introduce member name "x"
    struct Derived : T, Fallback { };

    template<typename C, C> struct ChT; 

    template<typename C> static char (&f(ChT<int Fallback::*, &C::x>*))[1]; 
    template<typename C> static char (&f(...))[2]; 

    static bool const value = sizeof(f<Derived>(0)) == 2;
}; 

struct A { int x; };
struct B { int X; };

int main() { 
    std::cout << HasX<A>::value << std::endl; // 1
    std::cout << HasX<B>::value << std::endl; // 0
}

Se basa en una idea brillante de alguien en Usenet.

Nota: HasX busca cualquier dato o miembro de función llamado x, con tipo arbitrario. El único propósito de introducir el nombre del miembro es generar una posible ambigüedad en la búsqueda del nombre del miembro; el tipo de miembro no es importante.

Johannes Schaub - litb avatar Jun 17 '2009 13:06 Johannes Schaub - litb

Me redirigieron aquí desde una pregunta que se cerró como un duplicado de esta. Sé que es un hilo antiguo, pero solo quería sugerir una implementación alternativa (¿más simple?) que funcione con C++ 11. Supongamos que queremos comprobar si una determinada clase tiene una variable miembro llamada id:

#include <type_traits>

template<typename T, typename = void>
struct has_id : std::false_type { };

template<typename T>
struct has_id<T, decltype(std::declval<T>().id, void())> : std::true_type { };

Eso es todo. Y así es como se usaría ( ejemplo en vivo ):

#include <iostream>

using namespace std;

struct X { int id; };
struct Y { int foo; };

int main()
{
    cout << boolalpha;
    cout << has_id<X>::value << endl;
    cout << has_id<Y>::value << endl;
}

Las cosas se pueden simplificar aún más con un par de macros:

#define DEFINE_MEMBER_CHECKER(member) \
    template<typename T, typename V = bool> \
    struct has_ ## member : false_type { }; \
    template<typename T> \
    struct has_ ## member<T, \
        typename enable_if< \
            !is_same<decltype(declval<T>().member), void>::value, \
            bool \
            >::type \
        > : true_type { };

#define HAS_MEMBER(C, member) \
    has_ ## member<C>::value

Que podría usarse de esta manera:

using namespace std;

struct X { int id; };
struct Y { int foo; };

DEFINE_MEMBER_CHECKER(foo)

int main()
{
    cout << boolalpha;
    cout << HAS_MEMBER(X, foo) << endl;
    cout << HAS_MEMBER(Y, foo) << endl;
}
Andy Prowl avatar Jan 25 '2013 14:01 Andy Prowl