03.25.07

Reflexión en C++… (ser o no ser…)

Posted in Programming at 4:52 pm by victor

Muchos años tiene ya nuestro querido amigo el C mais mais, y muchos son los que dicen que el lenguaje tiene algunas carencias. Estas “características” que supuestamente le faltan al C++ se encuentran presentes de forma nativa en lenguajes mas “modernos”, que hacen la vida más fácil al programador. Ejemplos de esto pueden ser los delegates al estilo C#, la gestión de memoria por parte de un garbage collector, o las capacidades de reflection de java y .net.

Pero no os dejeis engañar por las artimañas de estos lenguajes llegados del mismísimo infierno, mis queridos amigos programadores, porque todas estas cosas también las puede hacer el C++!

Hoy voy a hablar del concepto de reflection, reflexión, instrospección, o como narices lo querais llamar. La reflexión es la capacidad que tiene un código de consultar información sobre un determinado tipo (variables miembro, métodos, herencias, etc…) y acceder a él. Con esta información a nuestro alcance podemos hacer cosas realmente útiles. El uso mas común es el de la serialización. Podemos guardar a disco cualquier objeto, para posteriormente recuperarlo. También podemos modificarlo, enviarlo por red, invocar funciones sin conocer previamente su firma, etc…

Java y C# (.NET en general) disponen de los favores de la diosa reflection de forma nativa, pero C++ no. En nuestro amado lenguaje Stroustrupero disponemos de información de tipos muy limitada, las RTTI, que en la práctica sólo nos permiten saber si dos objetos son del mismo tipo o no, sacar información básica sobre él, y si se puede castear de un tipo a otro.

Existen varias maneras de implementar reflection en C++, unas mas complejas que otras, y con diferentes ventajas e inconvenientes. La más complicada de ellas, pero la menos intrusiva, consiste en “robar” los datos de reflexión del propio compilador. Existen por ahi personas con mucho tiempo libre que se dedican a sacar la información de los archivos de depuración que generan los propios compiladores. También hay proyectos, como Reflex/SEAL, que utilizan GCCXML para parsear tus archivos .h y que te generan una libreria con toda la información sobre los tipos que hay definidos en estos headers.

Existen otros métodos más sencillos de implementar, pero que son algo intrusivos. Es decir, requieren que modifiques tu código fuente para poder “reflejar” tus tipos. Para hacer esto normalmente se echa mano de la magia de los templates, algunas macros molonas, y funciones virtuales a punta pala.

Veamos una implementacion sencilla. De momento voy a hablar sólo de reflection para variables miembro de una clase, Es decir, nada de enumeradores ni metodos. Imaginemos que tenemos sólo los tipos básicos char y int. El resto de tipos seran estructuras y clases que estaran formadas por estos tipos básicos y/o otros tipos, tanto miembros como tipos base.

Necesitamos poder exponer información sobre tipos, tanto de forma estática como dinámica. Para lo primero tendremos un template declarado que nos dará informacion sobre cualquier tipo que ofrezca reflection.

template <typename T>
struct TypeInfo;

Esta es sólo la declaración del template, pero no tendrá ninguna definición templatizada. Todas las definiciones posibles vendrán dadas por diferentes especializaciones, una por cada tipo:

template<>
struct TypeInfo<char>
{
    static const char* name() { return "char"; }
    static bool IsBuiltin = true;
    // otras propiedades del char
};
template<>
struct TypeInfo<int>
{
    static const char* name() { return "int"; }
    static bool IsBuiltin = true;
    // otras propiedades del char
};
template<>
struct TypeInfo<TEstructuraMolona>
{
    static const char* name() { return "TEstructuraMolona"; }
    static bool IsBuiltin = false;
    //Propiedades de un tipo no básico, formado por otros miembros, que pueden ser basicos o no
};

Tambien debemos soportar cosas como arrays de objetos del mismo tipo. Esto lo podemos hacer con especializacion parcial (diossss, que miedo):

template<typename T, int ArraySize>
struct TypeInfo<T[ArraySize]> : public TypeInfo<T>
{
    // Heredamos las propiedades del tipo T, y añadimos informacion para indicar
    // que es un array, su tamaño, etc...
};

La especialización parcial también nos puede ser util para distinguir punteros y referencias

template<typename T>
struct TypeInfo<T*> : public TypeInfo<T>
{
    // es un puntero
};
template<typename T>
struct TypeInfo<T&> : public TypeInfo<T>
{
    // es una referencia
};

Uniendo estos conceptos, junto con otros, podemos tener una representación muy cuca de todos nuestros tipos. Eso si… todo esto es completamente estático, lo cual no nos sirve de mucho, pq en tiempo de compilación tanto nosotros como el compilador ya conocemos exactamente el tipo de cada cosa :P. Ahora bien, todos estos templates de la muerte los podemos usar para generar de una forma más o menos automatizada una representación de nuestros tipos que pueda ser consultada en runtime.

class Type
{
    virtual const char* name() const = 0;
    virtual bool IsBuiltin() const = 0;
    virtual bool IsArray() const = 0;
    virtual bool IsPointer() const = 0;
    virtual bool IsReference() const = 0;
    virtual const std::vector< Type* >& GetBaseTypes() const = 0;
    virtual const std::vector< Member* >& GetMembers() const = 0;
    // Todo tipo de cosas... dejar volar vuestra imaginación :D
};

Ahora, para generar los diferentes tipos, usariamos instanciaciones de nuestros templates

template <typename T>
class Type_Impl : public Type
{
    const char* name() const { return TypeInfo<T>::name(); }
    bool IsBuiltin() const { return TypeInfo<T>::IsBuiltin; }
    // ..etc etc etc...
};

Ahora por cada instanciación de Type_Impl tendremos un Type distinto que define las propiedades del tipo T del template, y podremos consultarlas en runtime. Todavía nos falta pulir esto un poco más… no queremos tener que tratar con Type_Impl<loquesea>, y aun no hemos resuelto del todo el tema de hacerlo dinámico. Nos falta alguna manera de tener todos estos “metadatos” registrados y guardaditos en algun sitio, para que cuando lo necesitemos podamos hacer una petición por nombre de tipo.

Primero de todo, vamos a esconder el template que genera los diferentes objetos Type, y lo vamos a hacer en la propia especialización de TypeInfo para cada tipo!. Usaremos una de las perlas del señor alexandrescu, que tanto bien (o mal, segun se mire) ha hecho al mundo del C++.

template<>
struct TypeInfo<TEstructuraMolona>
{
    typedef TEstructuraMolona type;

    static const char* name() { return "TEstructuraMolona"; }
    static bool IsBuiltin = false;
    //Propiedades de un tipo no básico, formado por otros miembros, que pueden ser basicos o no

    static const Type* GetType()
    {
        class impl : public Type_Impl<type>
        {
            impl()
            {
                // ...operaciones varias de las que hablaremos en otro momento...

                reflection::register_type( TypeInfo<type>::name, TypeInfo<type>::GetType() );
            }
        };

        static impl();
        return &impl;
    }
};

Ale, ara por cada tipo podemos hacer un TypeInfo<loquesea>::GetType(), y poder consultar lo que nos de la gana. Si queremos consultar cosas en runtime tb podremos. La clase impl tiene un constructor que autoregistra el tipo en una especie de base de datos de todos los tipos que soportan reflection. Utilizo un patrón singleton para generar una única instáncia de cada definición de tipo, y así tambien estamos protegidos a la hora de autoregistrarnos, Dios mio… cuantas “guarradas” se pueden hacer en C++. Da miedo, y al mismo tiempo da sensación de poderrrrrr! :_)

Esta es la idea general de como exponer informacion de tipos a un programa. Ahora podriamos meter en el interface Type métodos para consultar miembros por nombre, y preguntar de que tipo son. A partir de aquí podríamos trabajar con cualquier objeto cargado en runtime sin conocer a priori su tipo, y poder extraer e insertar información sobre él. Pero esto ya lo veremos en otro post :D

Espero vuestras opiniones!!

3 Comments »

  1. Nacho (a.k.a FANatiko) said,

    September 3, 2007 at 10:43 pm

    Bueno Victor, como te empezaba a comentar antes, a mi el rollo este de usar templates de forma circense (más difícil todavía) me parece poco práctico. La reflexión que parece razonable conseguir de forma sensata (sin arriesgarte a convertir tu código en algo difícil de debuggear) es muy limitada y no es más que un substituto con más features de la RTTI. La reflexión “de verdad” (que no es gratis) o que viene de serie en la mayoría de lenguajes interpretados permite hacer unos módulos mucho más molones (ya que tienes información de parámetros, puedes llamar métodos y, a veces, cambiarlos en tiempo de ejecución).

    Ya sabes que yo soy un gran advocate de Python y que creo que el C++ debería usarse de forma sensata y no hacer tanto caso al amigo Storstroup que el tío tiene ideas raritas. Al final usamos C++ por la velocidad de ejecución, los templates son un parche que se le antoja magnifico a Storstroup y que, segun el, evita mancillar C++ con cosas bonitas como lambda, higher order function, construcciones bonitas como foreach, construcciones feas como unless o ifs sufijos, etc.

  2. ok said,

    September 25, 2008 at 9:36 am

    good site kajcbr

  3. RYErnest said,

    December 1, 2008 at 10:07 am

    Nice post u have here :D Added to my RSS reader

Leave a Comment