lunes, 10 de noviembre de 2014

SFINAE: Magia con templates en C++ (parte 2)

En la primer parte comenté cómo utilizar algunas reglas particulares de la especialización de funciones genéricas en C++ para permitir o no una determinada especialización de acuerdo a alguna característica de un tipo de dato. La clave estaba en que un argumento de la plantilla intentaba especializar otra plantilla con un entero que surgía de un sizeof. Si en esa otra plantilla fallaba la substitución, no se generaba un error mientras exista alguna otra plantilla en la que no falle. Cualquier cosa que acepte el sizeof nos sirve. Esto permite, por ejemplo, preguntar por la existencia de métodos o atributos, aplicándole el sizeof a un puntero al mismo. Y así, con pequeños podemos "controlar" varias cosas. Veamos cómo con una característica de C++11 lo generalizamos mejor, y con un par de lineas adicionales lo hacemos mucho más fácil de utilizar.

Empecemos por empaquetar las funciones de prueba en una clase. Tomemos el ejemplo donde requerimos el método size():

    template<typename T> struct TieneSize {
        template<int N> struct Aux{};
        template<typename U>
            static int Prueba(Aux<sizeof(&U::size)>* );
        template<typename U>
            static char Prueba(...);
        static const bool resultado =
            sizeof(Prueba<T>(0))==sizeof(int);
    };


Este truco está basado en una respuesta ingeniosa que vi en stackoverflow. Hay dos métodos Prueba; uno recibe un puntero, el otro argumentos variables. Lo de argumentos variables está elegido adrede, porque de entre las posibles sobrecargas válidas, para el compilador esa es siempre la de menor prioridad (la última que elegiría). La otra, recibe un puntero, entonces ambas puede invocarse con 0 (equivalente a NULL). Si la primera especialización es válida, se utiliza esa y retorna un int, sino pasa a la segunda que siempre funciona y retorna un char. No importa cual int o cual char porque no vamos a invocar estos métodos. La clave está en que los tipos que retornan tienen tamaños diferentes, entonces podemos preguntar con sizeof qué tamaño tiene el tipo de retorno de la que elige el compilador. Lo importante de esta pregunta es que se responde en tiempo de compilación, y por lo tanto es de respuesta constante. Guardamos esa respuesta en un bool para no repetirla, hacemos todo estático para no tener que instanciar la clase, y la usamos de forma bastante simple. Por ejemplo, para saber si X tiene un método size hacemos simplemente:


    cout<< TieneSize<X>::resultado <<endl;

Con C++11, podemos hasta evitar el ::resultado y cambiarlo por (), para invocar a esta clase como si fuera solo una simple función:


    if (TieneSize<X>()) ....


Para hacerlo, sobrecargamos la conversión a bool de un objeto de la clase, de forma que lo que realmente hace la linea de arriba es crear un objeto y convertirlo implícitamente a bool. Lo que necesitamos de C++11 es la palabra constexpr para que esta función de conversión se resuelva en tiempo de compilación, y no en ejecución como normalmente ocurre:

    template<typename T> struct TieneSize {
        ...lo que ya habia en esta clase...
        constexpr operator bool() { return resultado; }
    };


Manteniendo la estructura de esta clase, podemos cambiar la expresión del sizeof para preguntar muchas cosas. Supongamos por ejemplo que quiero controlar que un método exista, que se pueda invocar sin argumentos y que retorne algo, para un tipo U. Entonces puedo usar "sizeof( ((U*)0)->metodo() )". El "(U*)0" es un puntero nulo de tipo U. Uso un puntero y no un objeto común porque para hacer lo segundo tendría que asumir algo sobre su constructor. Con ese puntero intento invocar al método sin argumentos. Si el método no existe, los argumentos son incompatibles, o no retorna nada, entonces falla. Lo último ("no retorna nada") es importante, porque no es válido aplicarle sizeof a void. Se torna un problema cuando queremos un método para que haga algo, pero no nos importa qué retorna (que pueda ser void). Además, con sizeof sabemos que retorna algo, pero no sabemos qué. Podría ser un string cuando en realidad necesitamos un número.

En C++11 tenemos algo bastante más útil (en este caso) que sizeof, que es decltype. Esto funciona más o menos como el typeof de gcc. Le pasamos una expresión, y nos dice de qué tipo es. Entonces "float d;" se puede escribir como "decltype(1.5f) d;". Con decltype, podemos reemplazar el int de Aux por un typename, y usarlo en lugar de sizeof. Si queremos hacer lo mismo que antes, simplemente ponemos el sizeof adentro del decltype. Pero ahora podemos poner otras cosas más generales, o más específicas, según como se mire. Por ejemplo, poner "int(((U*)->size())" en el decltype controla que size sea un método, que se pueda invocar sin argumentos, y que retorne int o algo convertible a int, como char, short, size_t, etc.

Queda ver cómo hacer para usar este truco sin tener que escribir una de estas clases raras por cada cosa que hay que validar, y cómo usar efectivamente una de estas clases para decidir qué función compilar entre posibles opciones. Ya hay algunas clases hechas para cosas bastante generales en la biblioteca type_traits de C++11. Pero veremos ćomo, añadiendo un poco de preprocesador y otra pizca de especialización explícita, podremos definir nuestros propias versiones bien específicas de forma tan simple como:
    crear_test(puede_graznar,Pato,((Pato*)0)->Graznar());
y usarlos en otra linea para ejecutar una función solo actúa si el tipo pasa el test, algo como:
    si_pasa_el_tets(Pato,puede_graznar) { Pato p; p.Graznar(); }

Pero esto ya se hizo largo, así que eso queda para la tercera y última parte.

No hay comentarios:

Publicar un comentario