Conversión de tipos en C++

Conversión de tipos en C++

The Right Way!!

Al igual que ocurre en C, el compilador de C++ soporta convertir variables de un tipo en otro diferente. Sin embargo, aunque C++ permite indicar estas conversiones mediante la misma sintaxis que C, lo recomendado es utilizar los operadores específicos de C++.

Conversión de tipos en estilo C

En C el compilador es capaz de hacer ciertas conversiones de tipos de forma automática —o implícita—. Por ejemplo, ir de char a int o de este a float es algo que hace el compilador sin que nos demos cuenta:

int i = 10;  
float d = i;        /* correcto */

Sin embargo, hay conversiones que no son válidas:

int* i = NULL;  
float* d = i;       /* conversión inválida de 'int*' a 'float*' */

cuyo comportamiento por defecto no es el deseado:

int a = 10;  
int b = 7;  
float c = a / b;    /* c = 1.0 y no 1.4, como podría esperarse */

Para esos casos el lenguaje nos permite forzar la conversión de tipos, utilizando una expresión de typecast de la forma (type)object —o type(object)— indicando así que queremos convertir object al tipo especificado por type. Por ejemplo:

int a = 10;  
int b = 7;  
float c = (float)a / (float)b;  /* c = 1.4 */

En C++ se puede utilizar la misma expresión de typecast que en C, aunque no es lo más aconsejable. En su lugar, C++ ofrece diversos operadores de typecast cuyo uso es más adecuado y menos peligroso que la conversión de tipos estilo C.

static_cast

El operador:

static_cast(object)

es siempre el primer tipo de conversión que debemos intentar utilizar. Permite invocar conversiones implícitas entre tipos —es decir, esas conversiones automáticas del compilador que mencionamos al principio—.

Por ejemplo, la conversión implícita de int a float:

int a = 10;  
float d = a;

también se puede hacer así:

int a = 10;  
float d = static_cast<float>(a);    // correcto

quizás para hacer correctamente una división de enteros en coma flotante, evitando perder los decimales

int a = 10;  
int b = 7;  
float c = static_cast<float>(a) / static_cast<float>(b); // c = 1.4  
// float c = a / b                                       // c = 1.0

También permite la conversión de cualquier tipo de puntero a void*:

int* pa = nullptr;  
void* pb = static_cast<void*>(pa);  // correcto

que es equivalente a hacer directamente void* pb = pa, ya que la conversión de cualquier puntero a void* se hace de forma implícita. Sin embargo, también admite la conversión inversa, que no es una conversión implícita:

void* pa = nullptr;  
char* pb = static_cast<char*>(pa);  // correcto
// char* pb = pa;                   // ¡error!

Por ejemplo, podríamos reservar 10 caracteres con malloc() así:

char* c = static_cast<char*>(malloc(10 * sizeof(char)));

ya que la conversión del puntero void* que retorna malloc() a char* necesita un typecast.

En el caso de objetos, static_cast llama a los operadores de conversión explícitos definidos en las clases:

class Foo  
{  
    // ...

    // Definición del operador de conversión de objetos Foo a const char*
    operator const char*()
    {  
        // ...  
    }  
};

Foo foo;

char* c = static_cast<char*>(foo);  // correcto

En ejemplo anterior, tanto si se recurre al uso de static_cast como si se hace la conversión de forma implícita: char* c = foo; el método operator const char*() de la clase Foo es llamado para realizar la conversión.

static_cast también convierte de clases bases a derivadas en una jerarquía de clases. La conversión inversa, de clases derivadas a clase base es automática, siempre que no haya polimorfismo. Es decir, siempre que la clase base no tenga algún método virtual:

class Base  
{  
    // ...  
};

class Derived: public Base  
{  
    // ...  
};

Derived* derived = new Derive;

// Conversión implícita de puntero a Derived a puntero a Base.
Base* base = derived;

// Recuperar el puntero a Derived a partir del puntero a Base.
Derived* derived_de_nuevo = static_cast<Derived*>(base);

Hay que tener en cuenta que las conversiones static_cast se resuelven siempre en tiempo de compilación, por lo que no se comprueba si el tipo al que se convierte coincide con el tipo real del objeto. El estándar indica que queda indefinido lo que pueda pasar si se convierte de un tipo base a uno derivado cuando este último no es el tipo real del objeto.

Es decir, el resultado del siguiente ejemplo queda indefinido:

Por ejemplo:

class Base  
{  
    // ...  
};

class Derived: public Base  
{  
    // ...  
};

Base* base = new Base;

Derived* derived = static_cast<Derived*>(base); // ¡indefinido!

porque intentamos obtener un puntero a Derived para un objeto creado directamente como Base y el operador static_cast no hace ninguna comprobación para determinar si el el puntero base realmente apunta a un objeto creado al instanciar la clase Derived.

dynamic_cast

El operador:

dynamic_cast(object)

se utiliza exclusivamente para manejar el polimorfismo, ya que permite convertir un puntero o referencia de un tipo polimórfico —esto es, una clase con algún método virtual— a cualquier otro tipo. Eso no solo permite convertir de clases base a derivadas, sino también desplazarnos lateralmente e incluso movernos a una cadena de herencia diferente dentro de una misma jerarquía de clases.

class Base  
{  
    // ...

    // Declarar el destructor virtual para que la clase sea polimórfica.
    virtual ~Base() {}
};

class Derived: public Base  
{  
    // ...  
};

Derived* derived = new Derive;  

// Conversión implícita de puntero a objeto de clase derivada a puntero
// a objeto de su clase base.
Base* base = derived;

// Recuperar el puntero a `Derived` a partir del puntero a `Base` usando
// `dynamic_cast`.
Derived* derived_de_nuevo = dynamic_cast<Derived*>(base);

dynamic_cast busca en tiempo de ejecución el objeto del tipo deseado en la jerarquía del objeto, devolviéndolo en caso de encontrarlo. Si los tipos no son compatibles —por ejemplo, si el objeto no fue creado originalmente con el tipo o con un tipo derivado del tipo indicado— dynamic_cast devuelve nullptr, si se lo usó con un puntero, o lanza una excepción std::bad_cast, si se lo usó con una referencia.

class Base  
{  
    // ...

    // Declarar el destructor virtual para que la clase sea polimórfica.
    virtual ~Base() {}
};

class Derived: public Base  
{  
    // ...  
};

Base* base = new Base;

// Se obtiene `nullptr` al intentar obtener un puntero `Derived` para el objeto
// creado como `Base` 
Derived* derived = dynamic_cast<Derived*>(base); // = nullptr ¡error!

const_cast

El operador:

const_cast(object)

se usa exclusivamente para eliminar o añadir const a una variable, ya que esto es algo que no pueden hacer los otros operadores de typecast.

Añadir const a un tipo es una conversión implícita. Es decir:

int a = 10;  
const int b = const_cast<const int>(a); // correcto

es equivalente a:

int a = 10;  
const int b = a;

pero quitar const no lo es:

const int a = 10;  
int b = const_cast<int>(a); // correcto
// int b = a;               // ¡error!

Es importante destacar que su uso queda indefinido si la variable original realmente es constante. Por ejemplo, algunos compiladores optimizan las constantes reemplazándolas, allí dónde son utilizadas, directamente por el valor asignado. En ese caso, intentar modificar la variable tiene un resultado indefinido.

reinterpret_cast

El operador:

reinterpret_cast(object)

instruye al compilador para que una expresión de un tipo sea tratada sin más como de un tipo diferente. No se genera código para llevar acabo la conversión de los datos y, por tanto, es el más peligroso de los operadores de typecast.

Se utiliza para convertir punteros de un tipo a otro de forma arbitraria. Por ejemplo, si se recibe un flujo de bytes como un char* pero dichos bytes realmente son una secuencia de enteros, con reinterpret_cast se puede convertir el puntero char* en int* para recuperar fácilmente cada uno de los números de la secuencia.

También se puede utilizar para convertir un puntero en un entero para manipular una dirección directamente. Por ejemplo, así podemos obtener la dirección de c en la memoria como un entero almacenado en la varible p:

char* c = new char[15];  
uintptr_t p = reinterpret_cast<uintptr_t>(c);

La única garantía ofrecida por el estándar de C++ es que si se hace un reinterpret_cast y posteriormente se realiza otro para volver al tipo original, se obtiene el mismo resultado, siempre que el tipo intermedio tenga el tamaño suficiente para que no se pierda información.

Conversión estilo C

Si en C++ se indica una conversión de estilo C —usando la sintaxis tradicional (type)object o type(object)— el efecto será el mismo que la primera conversión de la siguiente lista que tenga éxito:

  1. const_cast.

  2. static_cast.

  3. static_cast y después const_cast.

  4. reinterpret_cast

  5. reinterpret_cast y después const_cast.

Usar en C++ typecasts estilo C es peligroso porque pueden convertirse en un reinterpret_cast sin pretenderlo. Si hace falta este tipo de conversión, es preferible indicarlo explícitamente en el código usando el operador reinterpret_cast. Así, en caso de problemas, es más fácil buscar en el editor dónde pueden estar ocurriendo conversiones que pueden ser problemáticas.

Además, la conversión estilo C ignora el control de acceso de las clases —protected o private— por lo que este tipo de conversión permite hacer operaciones que con los operadores de C++ no se puede. Por ejemplo, en el siguiente caso la compilación termina con un error:

class Base  
{  
    // ...  
};

class Derived: protected Base  
{  
    // ...  
};

Derived* derived = new Derived;  
Base* base = static_cast<Base*>(derived);   // ¡error!

ya que la clase Base es una clase base protegida de Derived. Sin embargo el ejemplo compila sin problemas usando tanto reinterpret_cast como un typecast estilo C:

class Base  
{  
    // ...  
};

class Derived: protected Base  
{  
    // ...  
};

Derived* derived = new Derived;  
Base* base = reinterpret_cast<Base*>(derived);  // correcto  
//Base* base = (Base*)derived;                  // correcto

Referencias