Aquí hay una solución más simple que Johannes Schaub - litb 's one. 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 la explicación de cómo funciona esto.
Para estos tipos:
struct A { int x; };
struct B { int y; };
tenemos HasX<A>::value == true
y HasX<B>::value == false
. Veamos por qué.
En primer lugar recordemos que std::false_type
y std::true_type
tienen un miembro static constexpr bool
llamado value
que se fija para false
y true
, respectivamente. Por lo tanto, las dos plantillas HasX
anteriores heredan este miembro. (La primera plantilla de std::false_type
y la segunda de std::true_type
.)
Comencemos simple y luego procedamos paso a paso hasta que lleguemos al código anterior.
1) Punto de partida:
template <typename T, typename U>
struct HasX : std::false_type { };
En este caso, no es ninguna sorpresa: HasX
deriva de std::false_type
y por lo tanto HasX<bool, double>::value == false
y HasX<bool, int>::value == false
.
2) Incumplidor U
:
// Primary template
template <typename T, typename U = int>
struct HasX : std::false_type { };
Dado que U
valores predeterminados para int
, Has<bool>
significa en realidad HasX<bool, int>
y por lo tanto, HasX<bool>::value == HasX<bool, int>::value == false
.
3) Adición de 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>
deriva de std::false_type
. Sin embargo, existe una especialización para U = int
que deriva de std::true_type
. Por lo tanto, HasX<bool, double>::value == false
pero HasX<bool, int>::value == true
.
Gracias a la configuración predeterminada para U
, HasX<bool>::value == HasX<bool, int>::value == true
.
4) decltype
y una manera elegante de decir int
:
Una pequeña digresión aquí, pero, por favor, ten paciencia conmigo.
Básicamente (esto no es del todo correcto), decltype(expression)
produce el tipo de expresión . Por ejemplo, 0
tiene tipo int
por lo tanto, decltype(0)
significa int
. Análogamente, 1.2
tiene el tipo double
y por lo tanto, decltype(1.2)
significa double
.
Considérese una función con esta declaración:
char func(foo, int);
donde foo
es algún tipo de clase. Si f
es 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 la (built-in) coma operador que evalúa los dos sub-expresiones en orden (es decir, primero 1.2
y luego 0
), descarta el primer valor y los resultados en la segunda.Por lo tanto,
int x = (1.2, 0);
es equivalente a
int x = 0;
Poner esto junto con decltype
da que decltype(1.2, 0)
significa int
. No hay nada realmente especial sobre 1.2
o double
aquí. Por ejemplo, true
tiene tipo bool
y decltype(true, 0)
significa int
también.
¿Qué tal un tipo de clase? Por ejemplo, ¿qué significa decltype(f, 0)
? Es natural esperar que esto todavía signifique int
pero podría no ser el caso. De hecho, puede haber una sobrecarga para el operador de coma similar a la función func
anterior que toma un foo
y un int
y devuelve un 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 forma de sobrecargar al operador de coma para un operando void
y podemos convertir cualquier cosa al void
. Por lo tanto, decltype((void) f, 0)
significa int
. De hecho, (void) f
arroja f
de foo
a void
que básicamente no hace más que decir que la expresión debe considerarse como que tiene el tipo void
. A continuación, se utiliza la coma incorporada del operador y ((void) f, 0)
da como resultado 0
que tiene el tipo int
. Por lo tanto, decltype((void) f, 0)
significa int
.
¿Este modelo es realmente necesario? Bueno, si no hay sobrecarga para el operador de coma tomando foo
y int
, entonces esto no es necesario. Siempre podemos inspeccionar el código fuente para ver si hay tal operador o no. Sin embargo, si esto aparece en una plantilla y f
tiene el tipo V
que es un parámetro de plantilla, entonces ya no está claro (o incluso es imposible saber) si existe dicha sobrecarga para el operador de coma o no. Para ser genéricos, elegimos de todos modos.
En pocas palabras: decltype((void) f, 0)
es una manera elegante de decir int
.
5) SFINAE:
Esta es una ciencia entera ;-) Aceptar que estoy exagerando, pero no es muy simple tampoco. Así que mantendré la explicación al mínimo.
SFINAE significa Insuficiencia 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 cancelar la compilación, el compilador simplemente ignora el código ofensivo como si no estuviera allí. Vamos a ver 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í, de nuevo, decltype((void) T::x, 0)
es una forma elegante de decir int
pero con el beneficio de SFINAE.
Cuando T
se sustituye por un tipo, puede que aparezca una construcción no válida. Por ejemplo, bool::x
no es C++ válido, por lo que sustituir T
con bool
en T::x
produce una construcción no válida.Según el principio SFINAE, el compilador no rechaza el código, simplemente lo ignora (en partes). Más precisamente, como hemos visto HasX<bool>
significa realmente HasX<bool, int>
. Se debe seleccionar la especialización para U = int
pero, al crear una instancia, el compilador encuentra bool::x
e ignora por completo la especialización de la plantilla 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 lo tanto, HasX<bool, int>::value == false
.
El mismo argumento utilizado para bool
mantiene para B
desde B::x
es una construcción no válido (B
tiene ningún miembro x
). Sin embargo, A::x
está bien y el compilador no ve ningún problema al crear una instancia de la especialización para U = int
(o, más precisamente, para U = decltype((void) A::x, 0)
). Por lo tanto, HasX<A>::value == true
.
6) Unnaming U
:
Bueno, mirando el código en (5) de nuevo, vemos que el nombre U
no se utiliza en cualquier lugar pero en su declaración (typename U
). A continuación, podemos anular el nombre del segundo argumento de plantilla y obtenemos el código que se muestra en la parte superior de esta publicación.
No creo que la segunda forma sea estándar (las expresiones constantes integrales no pueden usar op == con operandos invo lving op &). Pero la primera forma se ve bien. ¿Qué dice msvC++ al respecto? –
@litb: Eche un vistazo al enlace al final de mi respuesta: creo que eso explica el problema (por qué los compiladores lo rechazan y si está realmente permitido por C++ 98 Standard). –
+1: desafío interesante :-) –