Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


2.2.5 Conversiones estándar

§1 Presentación

El tema de las conversiones de tipo es uno de los puntos que generalmente se le reprochan a C++. Una división de tipos no excesivamente rígida, o simplemente permisiva como la del C++ tiene sus ventajas, aunque también sus inconvenientes. Hemos señalado ( 1.2) que después de la premisa fundamental de diseño: Potencia y velocidad de proceso, otra de las características de su antecesor C, es la de ser permisivo;  "Intentando hacer algo razonable con lo que se haya escrito", lo que incluye naturalmente el asunto de los tipos.  Aunque C++ dispone de mecanismos de comprobación más robustos en este sentido, de alguna forma "hereda" la tradición de su antecesor.  El resultado es un nuevo frente para el programador que debe prestar atención al asunto. En especial porque muchas de estas conversiones de tipo son realizadas por el compilador sin que el programador tenga constancia explícita de ello. En ocasiones este "automatismo" es realmente una comodidad; en otras es origen de problemas y quebraderos de cabeza.

§2 Conversiones estándar

Se denominan conversiones estándar a determinadas conversiones de tipo que en ocasiones realiza espontáneamente el compilador para ajustar el tipo utilizado por el programador con las necesidades del momento. Estas conversiones se refieren casi siempre a tipos básicos preconstruidos en el lenguaje ( 2.2), y pueden clasificarse en alguno de los supuestos que se relacionan a continuación (existen unas pocas conversiones que afectan a los tipos abstractos y son tratadas en el siguiente capítulo 2.2.5a). Algunas de ellas, denominadas conversiones triviales, se realizan entre tipos que son muy parecidos, hasta el extremo que para ciertas cuestiones no se consideran tipos distintos. Por ejemplo, para la sobrecarga de funciones ( 4.4.1a).

  • Conversión nula: no existe conversión.

  • Conversiones triviales

    • Conversión de tipo a referencia ( T T&)

    • Conversión de referencia a tipo ( T& T)

    • Conversión de matriz a puntero ( T[ ] T*) .

    • Conversión de función a puntero-a-función ( T(arg) T(*)(arg) )

    • Conversión de calificación de tipo ( 2.2)

      • Tipo a constante ( T const T )

      • Tipo a volatile ( T volatile T )

      • Puntero-a-tipo a puntero-a-tipo constante ( T* cons T* )

      • puntero-a-tipo a puntero-a-tipo volatile ( T* volatile T* )

  • Conversión de Lvalue a Rvalue.

  • Conversiones y promociones entre tipos numéricos

  • Conversiones a puntero

  • Conversiones a booleano .


Ejemplo
: cuando se utiliza una expresión aritmética, como a + b, donde a y b son tipos numéricos distintos, el compilador realiza espontáneamente ciertas conversiones de tipo antes de evaluar la expresión. Estas conversiones incluyen la promoción de los operandos de tipo más bajo a tipos más altos, a fin de mejorar la homogeneidad y la precisión del resultado ( 2.2.4 Precisión y rango).

En ocasiones la conversión de un tipo a otro exige la realización de una secuencia de varias de las conversiones estándar anteriores.  Ejemplo: en la definición

char* cptr = "ABC";

para el compilador la expresión de la derecha es de tipo matriz-de-const char ( 3.2.3f), que es convertida a puntero-a-const char. Posteriormente, una segunda conversión (de calificación) transforma el puntero-a-cons char en puntero-a-char

Las conversiones estándar se realizan siempre porque las circunstancias exigen un tipo (de destino o final), y los tipos disponibles son distintos. Esto puede ocurrir en diversos contextos:

  • Cuando se realizan sobre los operandos de operadores son las exigencias del operador las que dictan el tipo de destino.

  • Cuando se realizan en la expresión de condición de una sentencia if ( 4.10.2) o de iteración do...while ( 4.10.3) el tipo de destino es un booleano ( 3.2.1b).

  • Cuando se realizan en sentencias switch de selección ( 4.10.2) el tipo de destino es un entero.

  • Cuando se utiliza en el Rvalue de una asignación, el tipo de destino es el del Lvalue.

  • Cuando se utiliza en los argumentos de una función o en el valor devuelto por esta, el tipo de destino es el establecido en la declaración de la función.

A su vez existen contextos en los que las conversiones automáticas se impiden expresamente. Por ejemplo, la conversión de Lvalue a Rvalue no se realiza en el operando del operador & ( 4.9.11) de referencia.


Para que una expresión exp pueda ser convertida implícitamente a un tipo T, es condición necesaria que pueda existir un objeto temporal t tal que la asignación T t = exp sea correcta.

§3 Conversiones entre tipos numéricos

Dentro de este epígrafe consideramos en realidad varios tipos de conversiones:

  • Promociones a entero

  • Promociones a fraccionario .

  • Conversiones entre asimilables a entero .

  • Conversiones entre tipos fraccionarios .

  • Conversiones fraccionario entero .

§3.1 Promociones a entero.

Comprende las siguientes conversiones:

  • Un Rvalue de los tipos char, signed char, unsigned char, short int, o unsigned short int puede ser convertido a un Rvalue de tipo int si en la implementación un int puede contener todos los valores de los tipos a convertir. En caso contrario son convertidos a unsigned int.

  • Un Rvalue del tipo wchar_t ( 2.2.1a1) o un enumerador ( 3.2.3g) pueden ser convertidos a un Rvalue del primero de los tipos: int; unsigned intlong, o unsigned long, que pueda representar el valor correspondiente.

  • Un Rvalue de tipo campo de bits ( 4.6) puede ser convertido al primero de los tipos int o unsigned int capaz de representar el rango de valores posibles del campo de bits. En caso contrario no se realiza ninguna promoción.

  • Un Rvalue de tipo lógico (bool) puede ser promovido a un Rvalue tipo int. La regla es que false se transforma en cero, y true en 1 ( 3.2.1b).

§3.2 Promoción a tipo fraccionario

Los Rvalues de tipo float o long pueden ser promovidos a Rvalue de tipo double. Este tipo de promoción se denomina también de punto flotante.

§3.3 Conversiones entre asimilables a entero

Cualquiera de los asimilables a entero ( 2.2.1) pueden ser convertido a otro tipo asimilable a entero. Las conversiones permitidas bajo el epígrafe anterior (promociones a entero) estan excluidas de las que se consideran aquí.

  • Un Rvalue de tipo enumeración puede ser convertido a un Rvalue de tipo entero.

  • La conversión de un entero largo a entero corto trunca los bits de orden superior, manteniendo sin cambios el resto.

  • La conversión de un entero corto a largo, pone a cero los bits extra del entero largo y/o los correspondientes al signo, dependiendo que el entero corto fuese con o sin signo.

  • La asignación de un carácter con signo (signed char) a un entero, origina la adopción del signo. Los caracteres con signo siempre utilizan signo.

  • Los caracteres sin signo (unsigned char) siempre ponen a cero el bit más significativo cuando son asignados a enteros.

  • Si el tipo de destino es signed, el valor origen permanece sin cambio si puede ser representado en el tipo destino (manteniendo el ancho del campo de bits). En caso contrario, el valor depende de la implementación [3].

  • Si el tipo de destino es bool la conversión se efectúa según se indica más adelante .  Si por el contrario el tipo origen es bool, las reglas son las indicadas en la promoción a entero: false se transforma en cero, y true en 1.

§3.4 Conversiones fraccionario <=> entero

Los tipos fraccionarios (de punto flotante) pueden ser promovidos a cualquier tipo asimilable a entero. Para ello se elimina la parte fraccionaria (decimal). Si la parte entera no cabe en el tipo de destino, el resultado es indefinido. Si el tipo de destino es un bool se siguen las pautas indicadas .

A su vez los tipos enteros y las constantes de enumeración pueden ser promovidos a fraccionarios. Si la conversión es posible (lo que ocurre efectivamente en la mayoría de las implementaciones) el resultado es exacto. En algunos casos el valor del entero no puede ser representado exactamente por el fraccionario, lo que acarrea una pérdida de precisión. En tal caso, el valor fraccionario adoptado es uno de los dos valores más próximos posibles (por arriba y por abajo) del valor entero. Si el tipo origen es un booleano, false se transforma en cero, y true en 1. 

§3.5 Conversiones aritméticas estándar, reglas de conversión

A continuación se exponen los pasos que sigue C++ durante la conversión de operandos en las expresiones aritméticas. El resultado de la expresión es del mismo tipo que uno de los operandos:

1º.-  Cualquier tipo entero es convertido según se muestra en la tabla.

Tipo convierte a Método de conversión seguido
char int Con o sin signo (dependiente del tipo char por defecto)
unsigned char int Siempre rellena con cero el byte más significativo
signed char int Siempre un signed int
short int Mismo valor; signed int
unsigned short unsigned int Mismo valor; rellena con ceros el byte más significativo
enum int El mismo valor

2º.- Después de esto, cualquier par de valores asociados con un operador son:

  • Un int (incluyendo sus variedades long y unsigned),
  • Un fraccionario de cualquiera de sus tres variedades: double, float o long double.

3º.- A partir de este momento, la homogenización de tipos se realiza ahora siguiendo los patrones que se indican (en el orden señalado)

  • Algún operando es long double    el otro es convertido en long double.
  • Algún operando es double    el otro es convertido en double.
  • Algún operando es float    el otro es convertido en float.
  • Algún operando es unsigned long    el otro es convertido en unsigned long.
  • Algún operando es long    el otro es convertido en long.
  • Algún operando es unsigned    el otro es convertido en unsigned.
  • Ambos aperandos son de tipo int.
Observaciones:

Generalmente las funciones matemáticas (como las incluidas en <math.h>) esperan argumentos en doble precisión (double 2.2.1), pero hay que tener en cuenta que las variables float no son convertidas automáticamente a double, y por supuesto, los double tampoco son convertidos automáticamente a float (supondría una pérdida de precisión). Ver un ejemplo comentado en ( 2.2.4a).

Sobre la forma de convertir double a float, o cualquier tipo a otro, ver el operador de modelado de tipos ( 4.9.9).

§3.6 Precauciones

  Las conversiones aritméticas son unos de los puntos en que el programador C++ debe prestar especial atención si no quiere dispararse accidentalmente en los pies ( 1), y donde el lenguaje puede gastarnos insidiosas jugarretas. Como ejemplo mostramos una función prevista para calcular la inversa de cualquier entero que se pase como argumento:

void inverso (int x) {

  float f = 1/x;

  cout << "X = " << x << " 1/x = " << f << endl;

}


La función se obstina en devolver siempre cero como resultado de la inversa de cualquier entero. El compilador Borland C++ no muestra la menor advertencia de que estemos haciendo nada mal y aparentemente el valor 1/x debe ser promovido a float, con lo que tenemos garantizado que el resultado puede ser fraccionario. Si una cuestión como esta se presenta cualquier día que estemos especialmente cansados, puede mandarnos directamente a limpiar cochineras a Carolina del Norte. Con un poco de suerte y descanso, quizás caigamos en la cuenta que la promoción se produce "después" que se haya efectuado la división, y que esta considera todavía como "enteros" a los miembros implicados (la constante 1 y el argumento x), con lo que el cociente, que es siempre menor que la unidad [1], es redondeado a cero, y este valor (int) es el que es promovido a float.

Una solución inmediata y obvia (?) permite resolver la situación (ver: Modelado de tipos 4.9.9):

void inverso (int x) {

  float f = float(1)/float(x);

  cout << "X = " << x << " 1/x = " << f << endl;

}

Una solución un poco más elegante:

void inverso (int x) {

  float f = float(1)/x;

  cout << "X = " << x << " 1/x = " << f << endl;

}

En este caso, el compilador realiza automáticamente la promoción de x a float antes de efectuar la división (ver reglas anteriores ).

Una solución aún más elegante que también produce resultados correctos:

void inverso (int x) {

  float f = 1.0/x;

cout << "X = " << x << " 1/x = " << f << endl;

§4 Conversiones a puntero

Un Rvalue que sea una expresión constante ( 3.2.3a) que se resuelva a 0, puede ser convertida a puntero de cualquier tipo T. Se transforma entonces en una constante-puntero nulo ("Null pointer constant"), y su valor es el valor del puntero nulo del tipo T*.

Para entender estos conceptos considere que en C++, dos punteros son distintos si apuntan a tipos distintos. Por ejemplo, un puntero-a-int (int*) es distinto de un puntero-a-char (char*), y sus valores son de tipo distinto. Resulta así que el valor (0) del puntero-a-int nulo es de tipo distinto del valor (0) del puntero-a-char nulo. Si representamos ambos valores por 0i y 0c respectivamente, diríamos que:

0i  es el valor del puntero nulo de int* (puntero-a-int)

0c  es el valor del puntero nulo de char* (puntero-a-char)

Ejemplo:

int const nulo = 0;      // L1:

int* pint = nulo;        // L2:

En L1 nulo es un objeto tipo int calificado const ( 2.2), cuyo Rvelue es 0. En L2 este objeto sufre una conversión estándar y se convierte al tipo int*; en este momento su valor no es ya un 0 "pelado" ("plain 0"); es el valor del puntero nulo del tipo int*. A continuación su Rvalue es copiado a la dirección del objeto pint, que toma así su valor.

Observe que si a la expresión L1 anterior se le suprime el calificador const:

int nulo = 0;          // L1a:

int* pint= nulo;       // L2: Error!!

se obtiene un error de compilación en L2. La causa es que la conversión estándar no puede realizarse porque, aunque nulo sigue siendo un int de valor 0, le falta el calificador const.

Considere ahora otra variación del ejemplo anterior:

int const nulo = 0;        // L1:

const int* pi1 = nulo;     // L2:

int const* pi2 = nulo;     // L3:
int* const pi3 = nulo;     // L4:

Los nuevos objetos son también punteros, aunque ahora pi1 y pi2 son punteros-a-int constante (L2 y L3 son equivalentes); el objeto al que señalan no puede cambiar su valor. Su tipo es const int*.

Por su parte, pi3 es también puntero-a-int, aunque con el calificador const.  Su tipo int* no se distingue del de pint en el caso anterior. En este caso el objeto nulo sufre una conversión estándar a tipo int* calificado. La norma nos avisa que esta conversión del objeto const al tipo int* calificado es una sola conversión, y no una conversión a int* seguida de una calificación.

§5 Conversiones de constantes de enumeración

Para las conversiones de las constantes de enumeración ver Enumeraciones ( 4.8).

§6 Conversiones de matriz a puntero

El compilador puede realizar expontáneamente la conversión de una matriz-de-elementos-tipoX a puntero-a-tipoX ( 4.3.2).  Este tipo de conversión es la que permite que la etiqueta de una matriz M pueda ser tomada en determinados contextos como un puntero a su primer elemento:

M &M[0] pM

Este tipo de conversión también ocurren en las asignaciones del tipo:

char* cptr = "ABC";

§7 Conversión a booleano

Los Rvelues de tipo numérico ( 2.2.1), las constante de enumeración, los punteros y los punteros a miembro, pueden ser convertidos a Rvelues de tipo bool ( 3.2.1b). La regla es que un valor cero o un puntero nulo son convertidos a false. Cualquier otro valor es convertido a true.

§8 Conversiones de función a puntero-a-función

Esta conversión permite que el nombre de una función F pueda ser tomada en caso necesario como su puntero ( 4.2.4a) [2]. En realidad, para el compilador, el tipo de una función es puntero-a-función, de forma que en lo tocante a este atributo, no distingue entre ambas entidades ( Ejemplo comentado).

Temas relacionados:

  • Modelado de tipos ( 4.9.9)
  • Búsqueda de nombres ( Name-lookup)
  • Congruencia estándar de argumentos ( 4.4.1a)
  • Conversiones definidas por el usuario ( 4..9.18k)

  Inicio.


[1] Suponemos x distinto de cero. En caso contrario se produciría un error fatal de "runtime".

[2] Esto no ocurre con métodos no estáticos de clases.

[3] Como la redacción del párrafo no me convence del todo, y para que el lector pueda juzgar por sí mismo, incluyo la redacción original en inglés.

"If the destination type is signed, the value is unchanged if it can be represented in the destination type (and bitfield width); otherwise, the value is implementation defined".