En C++ existen cuatro formas nativas de convertir tipos: static_cast, const_cast, reinterpret_cast y dynamic_cast. Además, se puede convertir un tipo con un constructor de clase o sobrecargar el operador de cast. Finalmente existe el cast tradicional de C. Aquí queremos hablar de las diferencias que tienen y cuando conviene usar uno u otro.

Lista de diferentes cast

Para aclarar un poco más las diferentes formas, empezamos con una enumeración de las diferentes posibilidades.

Formas de conversión de tipos
Nombre Ejemplo Uso típico
Cast tradicional de C (int) float_var Combina static_cast, const_cast y reinterpret_cast
static_cast static_cast(float_var) Convertir tipos nativos o clases con constructor o operador de cast
const_cast const_cast(const_int_var) Eliminar attributos de tipo como const
reinterpret_cast reinterpret_cast( void_pointer) Pretender que un tipo (de puntero) es realmente otro
dynamic_cast dynamic_cast( referencia_a_clase_base) Reinterpretar una clase base como una clase derivada
Constructor con un parámetro Clase(const OtroTipo& fuente) Inicialiazar una instancia con otro tipo
Operador de cast operator int(void) const; Convertir una instancia a otro tipo

Cast tradicional de C

El cast tradicional consiste en escribir el tipo entre paréntesis delante la variable que quieres convertir.

int a = 5;
float b = (float) a;

Esta forma de cast puede actuar como muchas de las otras formas de cast. El problema es que no siempre está claro cuál. Por eso es posible que tu programa acaba haciendo otra cosa que el esperabas y, por eso, suele haber la recomendación de ya no utilizarlo. Con las demás formas de cast habrá menos malentendidos entre tú y tu compilador.

Cast entre tipos nativos de C++

static_cast

El mayor uso de static_cast es convertir tipos nativos. Por ejemplo

int a = 5;
unsigned short int b = static_cast(a);
float c = static_cast(a);

Aunque para el programador los dos static_cast parecen lo mismo, el compilador debe hacer dos cosas distintas. A convertir de un tipo entero a otro, le basta jugar con los bits. (Básicamente debe llevar el bit de signo a una nueva posición.) Al convertir entre números enteros y números de coma flotante el compilador debe generar código más complejo, ya que la representación de bits es bastante distinta en tipos flotantes.

static_cast sirve también para convertir clases con el operador de cast como veremos más abajo. Si no sabes con qué cast a probar primero, entonces static_cast es una buena opción.

reinterpret_cast

Como indica el nombre, reinterpret_cast reintepreta un tipo como si fuera otro. Esto funciona especialmente bien entre punteros, ya que tienen el mismo formato físico aunque apunten a tipos diferentes. No obstante, reinterpret_cast reintepreta todo. Convertir un int a un puntero de función te traerá una cuantas advertencias del compilador, pero con reinterpret_cast se puede hacer.

Esta claro que algo tan poderoso invita al abuso. Por eso, la mayoría de las guías de programación te recomiendan con toda corazón de estudiar alternativas. Porque suele haber alternativas como static_cast – a menos que realmente quieres convertir números enteros a punteros de funciones.

const_cast

const_cast sirve para añadir y quitar atributos de tipos. Atributos son const, volatile y mutable. En la mayoría de los casos, const_cast sirve para quitar el atributo const de una variable – de ahí el nombre del cast.

Obviamente quitar el atributo const destruye la buena intención de no modificar una constante que, por lo tanto, las guías de programación recomiendan a no hacer. Sin embargo, suele ser justificado de usarlo para no romper la const-correctness de tu código por tener que usar una función sin const-correctness.

Cast entre clases

dynamic_cast

dynamic_cast se usa cuando sé que una instancia de una clase base es realmente una instancia de una clase derivada. Se usa típicamente en funciones virtuales.

class Base
{
public:
    void hazAlgo(const Base& instancia)
    {
        // Usar una función puramente virtual
        hazAlgoMás(instancia);
    }
private:
    virtual void hazAlgoMás(const Base& instancia) = 0;
};

class Derivada : public Base
{
private:
    virtual void hazAlgoMás(const Base& instancia)
    {
        // Aquí sé que instancia es realmente del
        // tipo Derivada.
        const Derivada& instancia_de_clase_derivada =
            dynamic_cast(instancia);
        // Aquí haz algo con instancia_de_clase_derivada
    }
};

void main()
{
    Derivada uno, dos;
    uno.hazAlgo(dos);
}

En el ejemplo arriba, llamamos para el objeto uno al método hazAlgo. Como la clase derivada no sobrescribe este método, llamamos realmente al método de la clase base. hazAlgo llama a una función puramente virtual hazAlgoMás que se resuelve al método Derivada::hazAlgoMás. La clase Base no sabe nada de la clase Derivada y por eso pasa como parámetro una referencia a un objeto de tipo Base, pero que realmente es un objeto de tipo Derivada. En la implementación del método virtual sabemos que el parámetros instancia debe ser de tipo Derivada y lo obtenemos mediante dynamic_cast.

No nececitamos hacer un cast para el puntero this. El sistema de métodos virtuales ya lo hace por nosotros.

Si el dynamic_cast no succede, por ejemplo, porque el tipo a que se convierte no está dentro del árbol de herencia del objeto a convertir, entonces dynamic_cast lanza un excepción std::bad_cast. Por este sistema de manejo de error, se considera dynamic_cast un método seguro para convertir entre clases del mismo árbol de herencia.

Por cierto, no es necesario convertir una instancia de una clase derivada a una instance de la clase base. Un objeto de una clase derivada siempre es también una instancia de la clase base.

Constructor con un parámetro

Considera un constructor del estilo

class MiClase
{
    explicit MiClase(const OtroTipo& fuente);
};

Este constructor permite inicializar una instancia de MiClase con un objeto de OtroTipo. Pues, esto no es otra cosa que una conversión de tipos.

No hay muchos situaciones en que un constructor con otro tipo es útil, pero cuando es útil, entonces este constructor es la manera recomendada de conversión. Como conversiones implícitas no están recomendadas, hemos escrito la palabra clave explicit delante. Esta nos obliga llamar al constructor explícitamente.

OtroTipo fuente;
MiClase a;
a = fuente; // Error, conversión implícita
a = MiClase(fuente);  // Bien, conversión explícita

Operador de cast

Se puede definir un operador que convierte una instancia de una clase a otro tipo.

class MiClase
{
    explicit operator bool() const;
};

void main()
{
    MiClase c;
    bool b1 = c;  // Error conversión implícita
    bool b1 = (bool) c;  // Bien, pero estilo C
    bool b2 = static_cast(c);  // Bien. Forma recomendada
}

Como vemos, el operator bool permite utilizar la instancia de la clase como si fuera otro tipo. En lugar de bool podríamos poner cualquier otro tipo nativo o clase.

Desde el estándar C++11 podemos escribir explicit delante la declaración para evitar conversiones implícitas. Aunque hemos mencionado arriba que conversiones implícitas no suelen ser deseadas, se usan con abundancia en la biblioteca estándar para las clases traits como std::is_constructible. No obstante, detrás de estas clases suele ser una implementación con templates que deben poder utilizarse como una función. Es decir, un asunto con que no queremos ruinarnos el – hasta ahora – buen entendimiento del tema.

Cambios en los nuevos estándares de C++

Bjarne Stroustrup, el inventor de C++, que ha introducido static_cast para evitar los cast al estilo C, ahora recomienda no utilizar static_cast tampoco. (Ver en los CPP Core Guidelines aquí y aquí.) Es por la misma razón: porque a veces no hace lo que el programador espera. O porque uno se hace la vida demasiado fácil.

De todas formas podemos esperar la introducción de nuevos y más seguros formas de cast como el todavía no estándar narrow_cast. Y ya tienes perlas como std::move y std::forward para resolver problemas que, posiblemente, todavía no has tenido.

En todo caso, si desarrollas código para sistemas que pueden causar daño a personas (que son básicamente aquellas donde las licencias de Microsoft y Apple te prohiben utilizar sus sistemas operativos), entonces vale la pena a echar un ojo al buen uso de los cast y controlarlos con una herramienta de análsis de código estática.

Referencias

Lectura adicional