3.7.12. Los moldes explícitos de C++

Los moldes se deben utilizar con cuidado, porque lo que está haciendo en realidad es decir al compilador «Olvida la comprobación de tipo - trátalo como si fuese de este otro tipo.» Esto significa, que está introduciendo un agujero en el sistema de tipos de C++ y evitando que el compilador informe de que está haciendo algo erróneo con un tipo. Lo que es peor, el compilador lo cree implícitamente y no realiza ninguna otra comprobación para buscar errores. Una vez ha comenzado a moldear, está expuesto a todo tipo de problemas. De hecho, cualquier programa que utilice muchos moldes se debe revisar con detenimiento, no importa cuanto haya dado por sentado que simplemente «debe» hacerse de esta manera. En general, los moldes deben ser pocos y aislados para solucionar problemas específicos.

Una vez se ha entendido esto y se presente un programa con errores, la primera impresión puede que sea mirar los moldes como si fuesen los culpables. Pero, ¿cómo encontrar los moldes estilo C? Son simplemente nombres de tipos entre paréntesis, y si se empieza a buscar estas cosas descubrirá que a menudo es difícil distinguirlos del resto del código.

El C++ Estándar incluye una sintaxis explícita de molde que se puede utilizar para reemplazar completamente los moldes del estilo antiguo de C (por supuesto, los moldes de estilo C no se pueden prohibir sin romper el código, pero los escritores de compiladores pueden advertir fácilmente acerca de los moldes antiguos). La sintaxis explícita de moldes está pensada para que sea fácil encontrarlos, tal como se puede observar por sus nombres:

static_cast Para moldes que se comportan bien o razonablemente bien, incluyendo cosas que se podrían hacer sin un molde (como una conversión automática de tipo).
const_cast Para moldear const y/o volatile
reinterpret_cast Para moldear a un significado completamente diferente. La clave es que se necesitará volver a moldear al tipo original para poderlo usar con seguridad. El tipo al que moldee se usa típicamente sólo para jugar un poco o algún otro propósito misterioso. Éste es el más peligroso de todos los moldes.
dynamic_cast Para realizar un downcasting seguro (este molde se describe en el Capítulo 15).

Tabla 3.2. Moldes explícitos de C++


Los primeros tres moldes explícitos se describirán completamente en las siguientes secciones, mientras que los últimos se explicarán después de que haya aprendido más en el Capítulo 15.

static_cast

El static_cast se utiliza para todas las conversiones que están bien definidas. Esto incluye conversiones «seguras» que el compilador permitiría sin utilizar un molde, y conversiones menos seguras que están sin embargo bien definidas. Los tipos de conversiones que cubre static_cast incluyen las conversiones típicas sin molde, conversiones de estrechamiento (pérdida de información), forzar una conversión de un void*, conversiones de tipo implícitas, y navegación estática de jerarquías de clases (ya que no se han visto aún clases ni herencias, este último apartado se pospone hasta el Capítulo 15):

//: C03:static_cast.cpp
void func(int) {}

int main() {
  int i = 0x7fff; // Max pos value = 32767
  long l;
  float f;
  // (1) Typical castless conversions:
  l = i;
  f = i;
  // Also works:
  l = static_cast<long>(i);
  f = static_cast<float>(i);

  // (2) Narrowing conversions:
  i = l; // May lose digits
  i = f; // May lose info
  // Says "I know," eliminates warnings:
  i = static_cast<int>(l);
  i = static_cast<int>(f);
  char c = static_cast<char>(i);

  // (3) Forcing a conversion from void* :
  void* vp = &i;
  // Old way produces a dangerous conversion:
  float* fp = (float*)vp;
  // The new way is equally dangerous:
  fp = static_cast<float*>(vp);

  // (4) Implicit type conversions, normally
  // performed by the compiler:
  double d = 0.0;
  int x = d; // Automatic type conversion
  x = static_cast<int>(d); // More explicit
  func(d); // Automatic type conversion
  func(static_cast<int>(d)); // More explicit
} ///:~

Listado 3.39. C03/static_cast.cpp


En la sección (FIXME:xref:1), se pueden ver tipos de conversiones que eran usuales en C, con o sin un molde. Promover un int a long o float no es un problema porque el último puede albergar siempre cualquier valor que un int pueda contener. Aunque es innecesario, se puede utilizar static_cast para remarcar estas promociones.

Se muestra en (2) como se convierte al revés. Aquí, se puede perder información porque un int no es tan «ancho» como un long o un float; no aloja números del mismo tamaño. De cualquier modo, este tipo de conversión se llama conversión de estrechamiento. El compilador no impedirá que ocurran, pero normalmente dará una advertencia. Se puede eliminar esta advertencia e indicar que realmente se pretendía esto utilizando un molde.

Tomar el valor de un void* no está permitido en C++ a menos que use un molde (al contrario de C), como se puede ver en (3). Esto es peligroso y requiere que los programadores sepan lo que están haciendo. El static_cast, al menos, es mas fácil de localizar que los moldes antiguos cuando se trata de cazar fallos.

La sección (FIXME:xref:4) del programa muestra las conversiones de tipo implícitas que normalmente se realizan de manera automática por el compilador. Son automáticas y no requieren molde, pero el utilizar static_cast acentúa dicha acción en caso de que se quiera reflejar claramente qué está ocurriendo, para poder localizarlo después.

const_cast

Si quiere convertir de un const a un no-const o de un volatile a un no-volatile, se utiliza const_cast. Es la única conversión permitida con const_cast; si está involucrada alguna conversión adicional se debe hacer utilizando una expresión separada o se obtendrá un error en tiempo de compilación.

//: C03:const_cast.cpp
int main() {
  const int i = 0;
  int* j = (int*)&i; // Deprecated form
  j  = const_cast<int*>(&i); // Preferred
  // Can't do simultaneous additional casting:
//! long* l = const_cast<long*>(&i); // Error
  volatile int k = 0;
  int* u = const_cast<int*>(&k);
} ///:~

Listado 3.40. C03/const_cast.cpp


Si toma la dirección de un objeto const, produce un puntero a const, éste no se puede asignar a un puntero que no sea const sin un molde. El molde al estilo antiguo lo puede hacer, pero el const_cast es el más apropiado en este caso. Lo mismo ocurre con volatile.

reinterpret_cast

Este es el menos seguro de los mecanismos de molde, y el más susceptible de crear fallos. Un reinterpret_cast supone que un objeto es un patrón de bits que se puede tratar (para algún oscuro propósito) como si fuese de un tipo totalmente distinto. Ese es el jugueteo de bits a bajo nivel por el cual C es famoso. Prácticamente siempre necesitará hacer reinterpret_cast para volver al tipo original (o de lo contrario tratar a la variable como su tipo original) antes de hacer nada más con ella.

//: C03:reinterpret_cast.cpp
#include <iostream>
using namespace std;
const int sz = 100;

struct X { int a[sz]; };

void print(X* x) {
  for(int i = 0; i < sz; i++)
    cout << x->a[i] << ' ';
  cout << endl << "--------------------" << endl;
}

int main() {
  X x;
  print(&x);
  int* xp = reinterpret_cast<int*>(&x);
  for(int* i = xp; i < xp + sz; i++)
    *i = 0;
  // Can't use xp as an X* at this point
  // unless you cast it back:
  print(reinterpret_cast<X*>(xp));
  // In this example, you can also just use
  // the original identifier:
  print(&x);
} ///:~

Listado 3.41. C03/reinterpret_cast.cpp


En este ejemplo, struct X contiene un array de int, pero cuando se crea uno en la pila como en X x, los valores de cada uno de los ints tienen basura (esto se demuestra utilizando la función print() para mostrar los contenidos de struct). Para inicializarlas, la dirección del X se toma y se moldea a un puntero int, que es luego iterado a través del array para inicializar cada int a cero. Fíjese como el límite superior de i se calcula «añadiendo» sz a xp; el compilador sabe que lo que usted quiere realmente son las direcciones de sz mayores que xp y él realiza el cálculo aritmético por usted. FIXME(Comprobar lo que dice este párrafo de acuerdo con el código)

La idea del uso de reinterpret_cast es que cuando se utiliza, lo que se obtiene es tan extraño que no se puede utilizar para los propósitos del tipo original, a menos que se vuelva a moldear. Aquí, vemos el molde otra vez a X* en la llamada a print(), pero por supuesto, dado que tiene el identificador original también se puede utilizar. Pero xp sólo es útil como un int*, lo que es verdaderamente una «reinterpretación» del X original.

Un reinterpret_cast a menudo indica una programación desaconsejada y/o no portable, pero está disponible si decide que lo necesita.