Como todo programador de C++ sabe, no existe función alguna para comprobar en tiempo de compilación si una clase A hereda de otra B. En tiempo de ejecución podemos usar dynamic_cast, pero con mucho cuidado:

  1. dynamic_cast permite saber si un puntero de una clase base es en realidad de una clase hija, esto ya presupone que las dos clases a comparar están “emparentadas”. De no ser así, el compilador nos soltará un warning (en el mejor de los casos) diciendo que la conversión nunca tendrá éxito.
  2. dado que dynamic_cast está pensado para usarse en el caso anterior, la clase a convertir tiene que ser obligatoriamente polimórfica.
  3. dynamic_cast es… lento, y requiere soporte de RTTI.

Escribiendo plantillas nos podemos encontrar que necesitamos saber si una clase hereda de otra, por ejemplo, para especializar la plantilla con un código optimizado. En esta circunstancia sólo nos sirve de verdad si somos capaces de tomar la decisión en tiempo de compilación, y dado que C++ no ofrece nada para ello Alexandrescu nos revela este truco.

¿ Se puede saber si una instancia de tipo A se puede convertir (automáticamente) en otra de tipo B ?

Cuando los tipos A y B son clases estamos hablando de si la clase A hereda de B, pero el truco funciona igual de bien para cualquier par tipos, por ejemplo, para saber si un int se puede convertir a float o mejor si una clase A tiene conversión definida a otra B (explicit conversion).

La respuesta es ¡sí!, y todo se lo debemos curiosamente a sizeof.

sizeof es realmente potente, se puede aplicar a cualquier expresión, sin importar lo compleja que esta sea, sizeof devolverá el tamaño de esta sin llegar a evaluar la expresión en tiempo de compilación. Esto significa que sizeof tiene presente la sobrecarga, la especialización de plantillas, reglas de conversión … absolutamente todo lo que pueda aparecer en una expresión de C++ . En realidad, sizeof es capaz de deducir el tipo de una expresión; desechando la expresión pero devolviendo su tamaño.Alexandrescu

Evaluar el valor de una expresión implica hacerlo en tiempo de ejecución, pero evaluar el tamaño del tipo devuelto por una expresión es algo que se debe realizar en tiempo de compilación. Lo que Alexandrescu nos viene a decir es que sizeof es precisamente capaz de esto, y al hacerlo desecha el valor de la expresión y “calcula” solamente el tamaño del tipo devuelto.

El truco consiste en tener un par de funciones de test sobrecargadas, una que acepta el tipo objetivo (B) y otra que acepte cualquier otra cosa. Cada función devuelve un tipo de tamaño diferente, al intentar llamar a la función test con el tipo A si existe la conversión de A a B se utilizará una función y si no, la otra. Tranquilos que los detalles siguen a continuación.

Primero creamos dos clases de tamaños diferentes, Alexandrescu nos recomienda algo como:

// Esta, por definición, ocupa 1 byte
typedef char S_True;
// Esta no se sabe cuanto ocupa, pero seguro que > 1
class S_False { char dummy[2]; }

Ahora necesitamos un par de funciones sobrecargadas, una que acepte B y devuelva S_True:

S_True Test(B);

Y otra para “todo lo demás”, y que tenga menos prioridad. De esta forma si el objeto de tipo A no se puede convertir de ningúna forma a B se utilice esta función. Para ello utilizamos la construcción de varargs que por definición siempre será el último recurso a utilizar.


S_False Test(...);

Ahora ya podemos averiguar si el tipo A se puede transformar en B, sin más que comparar el tamaño de evaluar la llamada de Test:

const bool existe_conversion = sizeof( Test(A()) ) == sizeof(S_True) ;

Y ya está!! ya tenemos una constante booleana que nos indica si la conversión es posible en tiempo de compilación… pero ¿cómo funciona? Recordemos lo que hemos venido comentando:

sizeof sólo evalúa el tamaño de una expresión sin necesidad de realizar llamadas reales, esto permite que no haga falta implementar nada en absoluto, nunca se van a llamar a ninguna función Test, ni al constructor de A(), ni nada… sizeof evalúa la expresión para ver qué tamaño tendrá el tipo del resultado.

Un pequeño problema es que la expresión “A()” tiene que ser válida lo que supone que el constructor por defecto debe existir en el tipo A. Como esto no tiene por qué ser cierto, vamos a darle un pequeño giro de tuerca.


A CrearInstancia();
const bool existe_conversion = sizeof( Test(CrearInstancia()) ) == sizeof(S_True) ;

La función CrearInstancia no hay que implementarla, nuevamente sizeof sólo la usa para saber que el parámetro de vuelvo es de tipo A, de esta forma evitamos tener que usar el constructor de A.

Con todo esto, ya tenemos para hacernos un template que nos diga si una clase es convertible a otra.

template <class A, class B>
class Conversion
{
typedef char S_True;
class S_False { char dummy[2]; }
static A CrearInstancia();
static S_True Test(B);
static S_False Test(...);
public:
enum {
existe = ( sizeof(Test(CrearInstancia())) == sizeof(S_True) )
};
};

Como ejemplo…

#include <iostream>
#include <vector>
int main() {
std::cout
<< Conversion<double,int>::existe << ' '
<< Conversion<char, char*>::existe << ' '
<< Conversion<size_t ,vector<int> >::existe << ' '
}

Y el resultado ‘1 0 0’ … Finalmente para detectar la herencia de clases (como decíamos en el tíutlo del post) lo que debemos es usar punteros constantes a los tipos:

Conversion<const clase1*, const clase2*>::existe

¡¡¡ Todo esto gracias a Alexandrescu y al power of sizeof !!!