viernes, 14 de noviembre de 2014

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

Finalmente llegamos a la última parte. Ya introduje en la parte 1 qué es eso de sfinae, y en la parte 2 cómo usarlo para consultar en tiempo de compilación la existencia de algo. La idea era poder plantear ejercicios de programación autocalificables; es decir, que validen solos si son correctos o no al ejecutarse. Para ello nos falta acomodar un poco el resultado de la parte 2 para que sea más cómodo de usar. Primero vamos a definir una macro para declarar con una linea la clase que hace la prueba de existencia de algo. Luego vamos a definir otra macro para declarar con una linea una función que solo se ejecuta si se pasa una prueba. Con todo eso, podemos plantear un ejercicio como el siguiente:

Por ejemplo, darle al alumno el código/ejercicio:

    /// Ejercicio.cpp
    /// Tarea: Implemente aquí una clase Pato con un
    /// constructor que reciba el nombre de un pato,
    /// y un método ContarPatas() que retorne 2.
    ...aqui va la solucion del alumno...
    // ----dejar este #include al final----
    #include "prueba.h"


Y nosotros hacemos en "prueba.h"  algo como esto:

    #include"macros_magicas.h"
    crear_test( hay_pato,     Pato, sizeof(Pato) );
    crear_test( tiene_patas,  Pato, int(((Pato*)0)->ContarPatas()) );
    crear_test( es_bautizable,Pato, Pato("nombre") );


    crear_funcion( Pato, controlar_patas,
                   es_bautizable() && tiene_patas() ) 

    {
        if (Pato("Lucas").ContarPatas()<2)
            error("Es un pato demasiado raro");
    }

    int main() {
        if (!hay_pato()) error("Falta la clase pato");
        if (!es_bautizable()) error("Falta el constructor");
        if (!tiene_patas()) error("Falta el método");
        controlar_patas();
        cout<<"Felicitaciones, es un lindo Pato"<<endl;
    }


Esto compila aunque no exista la clase Pato, o esté incompleta, o los prototipos no sean los esperados. En esos casos, al ejecutarse informará el error (la función exit que uso muestra el mensaje y finaliza la ejecución). Sino, si pasa todos los controles, felicitará al alumno. Veamos qué iría en "macros_magicas.h".

La primer macro (macro=función de preprocesador) es crear_test (test=prueba que determina si existe o no una clase/método y si puede utilizarse de cierta forma). Recibe el nombre para el test, el nombre de la clase para la que se hace la prueba, y la expresión a probar. Va a crear una clase como la del post anterior, pero con el nombre AuxC+algo. Hace esto porque la clase es genérica y no quiero tener que explicitar sus argumentos más tarde (en el ejemplo Pato) cada vez que la uso. Entonces, después de la clase agrego un typedef a la misma con los argumentos especializados y listo:

    #define crear_test(nombre,clase,expresion) \
    template<typename T> struct AuxC##nombre { \
        template<typename U> struct Aux{}; \
        template<typename clase> \

            static int Prueba(Aux<decltype( expresion )>* ); \
        template<typename U> static char Prueba(...); \
        constexpr operator bool() \

            { return sizeof(Prueba<clase>(0))==sizeof(int); }\
    }; \
    typedef AuxC##nombre<clase> nombre


Recordemos que la conversión a bool analiza que retornaría una llamada a Prueba. Si expresion es válida, se usa la primer versión de Prueba, sino la segunda.

La segunda macro tiene algo nuevo. Se basa en la idea de especialización explícita. Digamos que tengo una función genérica cuya parámetro (del template) es un int:

    template<int x> int resto(int d) { return d%x; }
    template<> int resto<0>(int d) { return 0; }


Tengo una versión genérica para todo excepto 0. Como con 0 fallaría, hay una especialización explícita para ese caso. Con una idea similar, uso una función genérica cuyo argumento es un bool, que no hace absolutamente nada, Y luego pongo el código que verifica el funcionamiento de la clase en el de una especialización explícita para true. Entonces después puedo utilizar como argumento del template alguna combinación de los tests que definí antes.

    template<class T,bool b> void verificar() { }
    template<class T,> void verificar<T,true>() {
        Pato p("Lucas");
        ...pruebas...
    }
    verificar<Pato,es_bautizable()>();


Pero esto no compila porque la especialización parcial (para algunos argumentos del template pero no todos) no es válida para funciones, solo para clases. Y necesito que el tipo siga siendo genérico, pues si no el compilador exige que tenga los métodos que se prueban al parsear la función. Entonces, para solucionarlo, reemplazo la función por una clase, y pongo las verificaciones en su constructor:

    #define crear_funcion(clase,nombre) \
        template<typename T, bool B> struct nombre {}; \
        template<typename T> struct nombre<T,true> { nombre(); }; \
        template<bool B> using nombre = nombre<clase,B>; \
        template<typename T> nombre<T,true>::nombre()

        // al usar la macro, aquí vendrian las llaves con
        // la implementación del constructor para B=true

Ahora la linea "verificar<Pato,es_bautizable()>();" en realidad está creando una instancia de este clasa y usando el constructor que corresponda según el resultado del test "es_bautizable". Podemos mejorar un poquito más esta macro para que reciba directamente el test a aplicar y lo aplique con tipo y todo mediante un typedef, así la invocamos como si fuera un función regular (por ejempo "verificar();").

Con estas dos macros, en "macros_magicas.h" el código para "prueba.h" que mostré antes se vuelve perfectamente válido y funcional en C++11. Sigue habiendo detalles menores para agregar, pero la idea está ya suficientemente completa.

En conclusión, tenemos una especia de reflexión (limitada) en tiempo de compilación, que podemos usar para reemplazar errores de compilación por mensajes propios (en tiempo de ejecución), dándole al alumno tanto detalle y ayuda como querramos, y todo en solo un bloque de código fuente, autocontenido, que no requiere más que un compilador cualquiera medianamente actualizado. Así, el alumno no debe instalar ni aprender nada nuevo, el feedback que recibe es personalizado e inmediato (mientras resuelve el ejercicio), el docente solo invierte tiempo en el diseño y en la implementa del ejercicio, pero no es su calificación (sin importar si son 3 o 3000 alumnos), y finalmente, con las macros presentadas el tiempo de implementación se reduce considerablemente .

Como resultado de todo esto escribí un artículo de congreso que presenté hace poco en el CACIC. Pueden bajar el artículo completo desde aquí, y un ejemplo desde aquí, para probarlo, usarlo, modificarlo, mejorarlo, o lo que quieran. Solo recuerden configurar ZinjaI para usar C++11 (menú ejecución -> opciones -> primer botón "..." -> copiar de plantilla -> Programa C++11 en blanco).

No hay comentarios:

Publicar un comentario