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á endouble
.- De lo contrario, si cualquiera de los operandos es
float
, el otro se convertirá enfloat
.- 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 delong int
, que será mayor que el rango deint
, que será mayor que el rango deshort int
, que será mayor que el rango designed 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
esunsigned long long
, ounsigned long
, entonces el rango destd::size_t
es mayor que el rango deunsigned int
y, por lo tanto, también deint
.Si
std::size_t
esunsigned int
, el rango destd::size_t
es igual al rango deunsigned int
y, por lo tanto, también deint
.
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
ysize_t
cuyos rangos de conversión de enteros (4.13) no sean mayores que los designed 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.