type sirve que para long data bool and c++ c++11 language-lawyer unsigned-integer signed-integer

sirve - long long c++ range



¿La comparación de un entero sin flujo y sin flujo con-1 está bien definida? (2)

Considera lo siguiente :

size_t r = 0; r--; const bool result = (r == -1);

¿La comparación cuyo resultado inicializa el result tiene un comportamiento bien definido?
¿Y es su resultado true , como lo esperaría?

Estas preguntas y respuestas se escribieron porque no estaba seguro de dos factores en particular.
Ambos pueden identificarse mediante el uso del término "crucialmente" en mi respuesta.

Este ejemplo está inspirado en un enfoque para condiciones de bucle cuando el contador no está firmado:
for (size_t r = m.size() - 1; r != -1; r--)


Sí, y el resultado es lo que cabría esperar.

Vamos a descomponerlo.

¿Cuál es el valor de r en este punto? Bueno, el flujo insuficiente está bien definido y los resultados r toman su valor máximo en el momento en que se ejecuta la comparación. std::size_t no tiene límites específicos conocidos , pero podemos hacer suposiciones razonables sobre su rango en comparación con la de un int :

std::size_t es el tipo entero sin signo del resultado del operador sizeof. [..] std::size_t puede almacenar el tamaño máximo de un objeto teóricamente posible de cualquier tipo (incluida la matriz).

Y, solo para salir del camino, la expresión -1 es unaria - aplicada al literal 1 , y tiene el tipo int en cualquier sistema:

[C++11: 2.14.2/2]: El tipo de un entero literal es el primero de la lista correspondiente en la Tabla 6 en la que se puede representar su valor. [..]

(No citaré todo el texto que describe cómo aplicar unario - a un int da como resultado un int , pero lo hace).

Es más que razonable sugerir que, en la mayoría de los sistemas, un int no podrá contener std::numeric_limits<std::size_t>::max() .

Ahora, ¿qué pasa con esos operandos?

[C++11: 5.10/1]: Los operadores == (igual a) y != (No igual a) tienen las mismas restricciones semánticas, conversiones y tipo de resultado que los operadores relacionales, excepto su precedencia y verdad más bajas -valor resultado. [..]

[C++11: 5.9/2]: Las conversiones aritméticas habituales se realizan en operandos de tipo aritmético o de enumeración. [..]

Examinemos estas "conversiones aritméticas habituales":

[C++11: 5/9]: muchos operadores binarios que esperan operandos de tipo aritmético o de enumeración causan conversiones y producen tipos de resultados de manera similar. El propósito es generar un tipo común, que también es el tipo del resultado.

Este patrón se llama las conversiones aritméticas habituales , que se definen de la siguiente manera:

  • Si cualquiera de los operandos es de tipo de enumeración (7.2), no se realizan conversiones; Si el otro operando no tiene el mismo tipo, la expresión está mal formada.
  • Si cualquiera de los operandos es de tipo long double , el otro se convertirá en doble largo.
  • De lo contrario, si cualquiera de los operandos es double , el otro se convertirá en double .
  • De lo contrario, si cualquiera de los operandos es float , el otro se convertirá en float .
  • De lo contrario, las promociones integrales (4.5) se realizarán en ambos operandos. 59 Entonces se aplicarán las siguientes reglas a los operandos promovidos:
    • Si ambos operandos tienen el mismo tipo, no se necesita más conversión.
    • De lo contrario, si ambos operandos tienen tipos enteros con signo o si ambos tienen tipos de enteros sin signo, el operando con el tipo de rango de conversión de entero menor se convertirá al tipo del operando con mayor rango.
    • De lo contrario, si el operando que tiene un tipo entero sin signo tiene un rango mayor o igual al rango del tipo del otro operando, el operando con tipo entero con signo se convertirá al tipo del operando con tipo entero sin signo.
    • De lo contrario, si el tipo del operando con tipo entero con signo puede representar todos los valores del tipo del operando con tipo entero sin signo, el operando con tipo entero sin signo se convertirá al tipo del operando con tipo entero con signo.
    • De lo contrario, ambos operandos se convertirán al tipo entero sin signo correspondiente al tipo del operando con tipo entero con signo.

He resaltado el pasaje que toma efecto aquí y, en cuanto a por qué :

[C++11: 4.13/1] : Cada tipo de entero tiene un rango de conversión de entero definido de la siguiente manera

  • [..]
  • El rango de long long int será mayor que el rango de long int , que será mayor que el rango de int , que será mayor que el rango de short int , que será mayor que el rango de signed char .
  • El rango de cualquier tipo de entero sin signo será igual al rango del tipo de entero con signo correspondiente.
  • [..]

Todos los tipos integrales, incluso los de ancho fijo, están compuestos de los tipos integrales estándar; por lo tanto, lógicamente, std::size_t debe ser unsigned long long , unsigned long , o unsigned int .

  • Si std::size_t es unsigned long long , o unsigned long , entonces el rango de std::size_t es mayor que el rango de unsigned int y, por lo tanto, también de int .

  • Si std::size_t es unsigned int , el rango de std::size_t es igual al rango de unsigned int y, por lo tanto, también de int .

De cualquier manera, de acuerdo con las conversiones aritméticas habituales , el operando firmado se convierte al tipo del operando sin signo (y, de manera crucial, no al revés). Ahora, ¿qué implica esta conversión?

[C++11: 4.7/2]: Si el tipo de destino no está firmado, el valor resultante es el entero con menos singruente congruente con el entero de origen (módulo 2 n donde n es el número de bits utilizados para representar el tipo sin signo). [Nota: En una representación de complemento a dos, esta conversión es conceptual y no hay cambios en el patrón de bits (si no hay truncamiento). "Nota final"

[C++11: 4.7/3]: Si el tipo de destino está firmado, el valor no cambia si puede representarse en el tipo de destino (y en el ancho del campo de bits); de lo contrario, el valor está definido por la implementación.

Esto significa que std::size_t(-1) es equivalente a std::numeric_limits<std::size_t>::max() ; es crucial que el valor n en la cláusula anterior se relacione con el número de bits utilizados para representar el tipo sin signo , no con el tipo de fuente. De lo contrario, estaríamos haciendo std::size_t((unsigned int)-1) , que no es lo mismo en absoluto, ¡podría ser muchos órdenes de magnitud más pequeños que nuestro valor deseado!

De hecho, ahora que sabemos que todas las conversiones están bien definidas, podemos probar este valor:

std::cout << (std::size_t(-1) == std::numeric_limits<size_t>::max()) << ''/n''; // "1"

Y, solo para ilustrar mi punto anterior, en mi sistema de 64 bits:

std::cout << std::is_same<unsigned long, std::size_t>::value << ''/n''; std::cout << std::is_same<unsigned long, unsigned int>::value << ''/n''; std::cout << std::hex << std::showbase << std::size_t(-1) << '' '' << std::size_t(static_cast<unsigned int>(-1)) << ''/n''; // "1" // "0" // "0xffffffffffffffff 0xffffffff"


size_t r = 0; r--; const bool result = (r == -1);

Estrictamente hablando, el valor del result está definido por la implementación. En la práctica, es casi seguro que sea true ; Me sorprendería si hubiera una implementación donde fuera false .

El valor de r después de r-- es el valor de SIZE_MAX , una macro definida en <stddef.h> / <cstddef> .

Para la comparación r == -1 , las conversiones aritméticas habituales se realizan en ambos operandos. El primer paso en las conversiones aritméticas habituales es aplicar las promociones integrales a ambos operandos.

r es de tipo size_t , un tipo entero sin signo definido por la implementación. -1 es una expresión de tipo int .

En la mayoría de los sistemas, size_t es al menos tan ancho como int . En tales sistemas, las promociones integrales hacen que el valor de r se convierta a unsigned int o que mantenga su tipo existente (lo primero puede suceder si size_t tiene el mismo ancho que int , pero un rango de conversión más bajo). Ahora, el operando de la izquierda (que no está firmado) tiene al menos el rango del operando de la derecha (que está firmado). El operando derecho se convierte al tipo del operando izquierdo. Esta conversión produce el mismo valor que r , por lo que la comparación de igualdad arroja el valor true .

Ese es el caso "normal".

Supongamos que tenemos una implementación en la que size_t es de 16 bits (digamos que es un typedef para unsigned short ) e int es de 32 bits. Entonces SIZE_MAX == 65535 e INT_MAX == 2147483647 . O podríamos tener un size_t 32 bits y un int 64 bits. Dudo que exista tal implementación, pero nada en la norma lo prohíbe (ver más abajo).

Ahora el lado izquierdo de la comparación tiene el size_t y el valor 65535 . Dado que int firmado puede representar todos los valores del tipo size_t , las promociones integrales convierten el valor a 65535 del tipo int . Ambos lados del operador == tienen el tipo int , por lo que las conversiones aritméticas habituales no tienen nada que hacer. La expresión es equivalente a 65535 == -1 , que es claramente false .

Como mencioné, es improbable que este tipo de cosas ocurra con una expresión de tipo size_t , pero puede ocurrir fácilmente con tipos sin signo más estrechos. Por ejemplo, si r se declara como un unsigned short , o un unsigned char , o incluso un char simple en un sistema donde ese tipo está firmado, el valor del result probablemente sea false . (Digo probablemente porque las letras short o incluso unsigned char pueden tener el mismo ancho que int , en cuyo caso el result será true ).

En la práctica, puede evitar el problema potencial haciendo la conversión explícitamente en lugar de confiar en las conversiones aritméticas habituales definidas por la implementación:

const bool result = (r == (size_t)-1);

o

const bool result = (r == SIZE_MAX);

C ++ 11 referencias estándar:

  • 5.10 [expr.eq] Operadores de igualdad
  • 5.9 [expr.rel] Operadores relacionales (especifica que se realizan las conversiones aritméticas habituales)
  • 5 [expr] Expresiones, párrafo 9: conversiones aritméticas habituales
  • 4.5 [conv.prom] promociones integrales
  • 18.2 [support.types] size_t

18.2 párrafos 6-7:

6 El tipo size_t es un tipo entero sin signo definido por la implementación que es lo suficientemente grande como para contener el tamaño en bytes de cualquier objeto.

7 [ Nota: se recomienda que las implementaciones elijan los tipos para ptrdiff_t y size_t cuyos rangos de conversión de enteros (4.13) no sean mayores que los de signed long int menos que sea necesario un tamaño mayor para contener todos los valores posibles. - nota final]

Así que no hay prohibición de hacer que size_t más estrecho que int . Casi puedo imaginar un sistema donde int es de 64 bits, pero ningún objeto individual puede ser más grande que 2 32 -1 bytes, por lo que size_t es de 32 bits.