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.» Es decir, 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 mirar con sospecha, por mucho que le digan que simplemente «debe» hacerse de esa manera. En general, los moldes deben ser pocos y aislados para solucionar problemas muy específicos.

Una vez se ha entendido esto, cuando se encuentre con un programa con errores, su primera tentación puede que sea buscar los moldes como posibles 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 eliminar const y/o volatile mediante un molde
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 manipular bits 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 el último sólo se podrá mostrar 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 (1), se pueden ver los tipos de conversiones que está acostumbrado a hacer en C, con o sin un molde. Promover un int a long o float no es un problema porque estos últimos pueden 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) cómo 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. Por eso, este tipo de conversión se llama conversión de estrechamiento. El compilador las realizará igualmente, 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 más fácil de localizar que los moldes antiguos cuando se trata de cazar fallos.

La sección (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, o 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, y é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

Éste 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. Ésa es la manipulación de bits a bajo nivel por la 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 sencillo 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 contienen basura (esto se demuestra utilizando la función print() para mostrar los contenidos del struct). Para inicializarlos, la dirección del X se toma y se moldea a un puntero a int, con el que luego se recorre el array para inicializar cada int a cero. Fíjese cómo el límite superior de i se calcula «añadiendo» sz a xp; el compilador sabe que lo que usted quiere realmente son sz posiciones de puntero más allá de xp y realiza por usted la aritmética de punteros correcta.

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í se ve 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.