martes, 5 de mayo de 2015

Variables globales otra vez

Ya comenté que pensaba reescribir desde cero MotoGT. También que el no poder predecir/controlar el orden en que se construyen y destruyen las variables globales me trajo serios problemas. Pues bien, dado que en el nuevo MotoGT voy a volver a usar una buena cantidad de variables globales, me vi forzado a repensar el problema. Y finalmente, encontré una buena solución, con la que puedo tener mis queridas y odiadas variables globales, pero puedo además asegurarme de que se creen y destruyan en un orden correcto.

Más adelante les contaré cómo va el diseño del nuevo motor para mi juego. Por ahora sepan que hay en él unas cuantas cosas que se usan desde tooodos lados y tienen instancias únicas durante toda la vida del juego. Por ejemplo, hay una sola ventana, un solo gran pool de texturas, una sola configuración, un reproductor de música de fondo, etc. No es como para andar pasándole tooodo a cada elementito del juego. Es mucho más cómodo y simple que sean instancias únicas y globales. Para todo lo demás uso o bien los punteros inteligentes de C++11, o bien clases mías que funcionan de forma similar, con la filosofía RAII. Pero para las cosas globales, necesito algo un poquito diferente.

Los problemas están en realidad a la hora de inicializar todas esas cosas, y de asegurar que se destruyan correctamente antes de finalizar el programa. Como ya comenté, no es bueno dejar el orden de inicialización de objetos globales complejos y posiblemente relacionados en manos del compilador. Es mejor explicitar las cosas, por ejemplo con una gran función de inicialización. Y para eso, hay que poner punteros globales en lugar de objetos, para que así esta función pueda inicializar con los news como más le plazca. Una alternativa con algunas ventajas es utilizar el patrón Singleton para cada uno de estos objetos. Así, garantizamos algún orden de inicialización correcto (en general no obvio pero sí correcto), y nos ahorramos esa gran función. En cualquier caso, las cosas globales (o estáticas, para el caso da lo mismo), son en realidad punteros.

Un gran problema del Singleton es que no destruye esos objetos globales. Se encarga, en principio solo de construirlos. A menos que lo modifiquemos para que nos deje explicitar la destrucción manualmente, o que usemos punteros inteligentes. La segunda opción es mucho mejor, pero tiene sus esquinas ásperas. Entonces, a mi me gustaría lograr que las cosas se destruyan en el orden correcto sin mucho esfuerzo. "Sin mucho esfuerzo" significa que puedo agregar por única vez un par de lineas al final del programa como código de limpieza, pero que no quiero tener que hacerlo cada vez que agrego, quito, o cambio una de estas instancias globales. Ni quiero correr el riesgo de olvidarme, o de equivocarme en el orden, o lo que sea.

En general, las variables de un scope se destruyen en orden inverso al que se crean. A mi me alcanza con eso: si controlo el orden de creación, con garantizar que se destruyan, y que lo hagan utilizando ese orden inverso. Lo que propongo entonces es un mecanismo mediante el cual, cada vez que creo una instancia global de algo, esta se registra en alguna lista, y mi programa antes de salir elimina todo lo de esa lista. La lista en realidad será una pila, y así garantizo el orden. Para que al crear las instancias, estas se registren automáticamente en dicha pila, voy a reemplazar el "new" por una función propia (en el mismo sentido en que lo reemplaza la función std::make_shared cuando se trata de std::shared_ptr). O sea que voy a poder tener un programa más o menos así:

    #include<trucos_globales.hpp>
   
    // porquerías globales
    std::string *s = nullptr;
    std::ifstream *f = nullptr
    std::vector<int> *v  = nullptr;
   
    int main() {
        // crear instancias globales en orden
        s = gNew<std::string>("Hola");
        f = gNew<std::ifstream>("config.ini");
        v = gNew<std::vector<int>>(42,0);
        ...bla bla bla...
        // destruir todo automáticamente
        deleteGlobals(); // primero v, luego f, y finalmente s
    }


Dentro de <trucos_globales.hpp> hay unas pocas cosas. Una pila (std::stack) de punteros a eliminar. Una función gNew<T>(...) que crea los punteros, los pone en la pila y los retorna. Y una función deleteGlobals() que va sacando las cosas de la pila y aplicándoles el operador "delete". Pero hay un detalle importante... ¿con qué tipo de puntero especializo a la pila? std::stack<¿que?>. Quiero poder guardar punteros globales de cualquier tipo, sin perder esos tipos, porque de ellos depende el funcionamiento del "delete". Entonces no podría hacer algo simple como usar void*. Piensa...

La solución pasa por una técnica conocida como type-erasure. Una clase encapsula a otra. Dentro, esa otra tiene información de tipos, pero por fuera no se ve (por fuera no es genérica, es una clase normal). ¿Cómo lo logro? La forma más sencilla que encontré usa templates y polimorfismo.

    // clase wrapper base
    struct DeleteOnExitBase {
        virtual ~DeleteOnExitBase() {}
    };

    // pila de wrappers a punteros que hay que borrar
    std::stack<std::unique_ptr<DeleteOnExitBase>> s_delete_on_exit;

    // reemplazo del new, se usa simil make_shared
    template<typename T, typename... Args>
    T *gNew(Args&&... args) {

        // clase hija especializable
        struct DeleteOnExit : public DeleteOnExitBase {
            T *m_ptr; // ptr con iform. de tipo
            DeleteOnExit(T *ptr) : m_ptr(ptr) {}
            virtual ~DeleteOnExit() { delete m_ptr; }
        };
        T *p = new T(args...);
        s_delete_on_exit.emplace(new DeleteOnExit(p));
        return p;
    }

    // limpieza, quitarlos los wrappers de la pila en orden
    void deleteGlobals() {
        while (!s_delete_on_exit.empty())
            s_delete_on_exit.pop();
    }


Pongo en la pila objetos wrappers de una clase base polimórfica. Genero luego en gNew, mediante una clase hija templatizada, especializaciones que conservan y usan la información de tipo. Es una versión muy simplificada de lo que se presenta en este video. Y como para usar polimorfismo hay que trabajar con punteros, en la pila pongo std::unique_ptr envolviendo esas especializaciones. Así, el std::unique_ptr les hace el delete a esos objetos wrappers, y esos wrappers hacen los delete que yo quería en principio. Más aún, si no quiero invocar a deleteGlobals, el destructor de la pila hará el trabajo de todas formas al finalizar el programa.

Creo que el uso de esta "biblioteca" es bastante sencillo, y no agrega absolutamente ninguna complejidad al código. Esto funcionaría igual si los punteros fueran los atributos estáticos de los Singletons en lugar de ser verdaderas variables globales. Lo interesante es que me resuelve el problema de la destrucción de todas las porquerías globales, tanto de asegurarla, como de ordenarla, sean las que sean, y sean cuantas sean, con una sola línea. Nada mal para empezar. Si alguien conoce algún otro truco mejor, no dude en sugerirlo en los comentarios.

No hay comentarios:

Publicar un comentario