c++ - preguntas - tag questions para que sirve
¿Por qué la operación de cambio a la izquierda invoca un comportamiento no definido cuando el operando del lado izquierdo tiene un valor negativo? (8)
En C, la operación de desplazamiento a la izquierda en modo bit invoca Comportamiento no definido cuando el operando del lado izquierdo tiene un valor negativo.
Cita relevante de ISO C99 (6.5.7 / 4)
El resultado de E1 << E2 es E1 posiciones de bit E2 desplazadas a la izquierda; los bits vacíos están llenos de ceros. Si E1 tiene un tipo sin signo, el valor del resultado es E1 × 2 E2 , módulo reducido uno más que el valor máximo representable en el tipo de resultado. Si E1 tiene un tipo firmado y un valor no negativo, y E1 × 2 E2 es representable en el tipo de resultado, entonces ese es el valor resultante; de lo contrario, el comportamiento está indefinido .
Pero en C ++ el comportamiento está bien definido.
ISO C ++ - 03 (5.8 / 2)
El valor de E1 << E2 es E1 (interpretado como un patrón de bits) posiciones de bit E2 desplazadas a la izquierda; los bits vacantes están llenos a cero. Si E1 tiene un tipo sin signo, el valor del resultado es E1 multiplicado por la cantidad 2 elevada a la potencia E2, módulo reducido ULONG_MAX + 1 si E1 tiene el tipo sin signo largo, UINT_MAX + 1 en caso contrario. [Nota: las constantes ULONG_MAX y UINT_MAX se definen en el encabezado). ]
Eso significa
int a = -1, b=2, c;
c= a << b ;
invoca comportamiento no definido en C, pero el comportamiento está bien definido en C ++.
¿Qué obligó al comité ISO C ++ a considerar ese comportamiento bien definido como opuesto al comportamiento en C?
Por otro lado, el comportamiento es la implementation defined
para la operación de desplazamiento a la derecha en modo bit cuando el operando de la izquierda es negativo, ¿verdad?
Mi pregunta es por qué la operación de cambio a la izquierda invoca Comportamiento no definido en C y por qué el operador de cambio a la derecha invoca solo el comportamiento definido de Implementación?
PD: No responda como "Es un comportamiento indefinido porque el estándar lo dice". :PAG
En C, la operación de desplazamiento a la izquierda en modo bit invoca Comportamiento no definido cuando el operando del lado izquierdo tiene un valor negativo. [...] Pero en C ++ el comportamiento está bien definido. [...] por qué [...]
La respuesta fácil es: Porque los estándares así lo dicen.
Una respuesta más larga es: Probablemente tiene algo que ver con el hecho de que C y C ++ permiten otras representaciones para números negativos además del complemento de 2. Al ofrecer menos garantías sobre lo que sucederá, es posible utilizar los idiomas en otro hardware, incluidos los equipos oscuros y / o antiguos.
Por alguna razón, el comité de estandarización de C ++ sintió ganas de agregar una pequeña garantía sobre cómo cambia la representación de bits. Pero dado que los números negativos aún pueden representarse a través del complemento 1 o el signo + magnitud, las posibilidades de valor resultante aún varían.
Suponiendo 16 bits, tendremos
-1 = 1111111111111111 // 2''s complement
-1 = 1111111111111110 // 1''s complement
-1 = 1000000000000001 // sign+magnitude
Shifted a la izquierda por 3, obtendremos
-8 = 1111111111111000 // 2''s complement
-15 = 1111111111110000 // 1''s complement
8 = 0000000000001000 // sign+magnitude
¿Qué obligó al comité ISO C ++ a considerar ese comportamiento bien definido como opuesto al comportamiento en C?
Supongo que hicieron esta garantía para que puedas usar << apropiadamente cuando sabes lo que estás haciendo (es decir, cuando estás seguro de que tu máquina usa el complemento de 2).
Por otro lado, el comportamiento es la implementación definida para la operación de desplazamiento a la derecha en modo bit cuando el operando de la izquierda es negativo, ¿verdad?
Tendría que verificar el estándar. Pero puede que tengas razón. Un desplazamiento a la derecha sin extensión de signo en una máquina complementaria de 2 no es particularmente útil. Por lo tanto, el estado actual es definitivamente mejor que requerir que los bits vacíos se llenen de cero porque deja espacio para las máquinas que hacen extensiones de letreros, aunque no está garantizado.
Mi pregunta es por qué la operación de cambio a la izquierda invoca Comportamiento no definido en C y por qué el operador de cambio a la derecha invoca solo el comportamiento definido de Implementación?
La gente de LLVM especula que el operador de turno tiene restricciones debido a la forma en que se implementa la instrucción en varias plataformas. De lo que todo programador de C debe saber sobre el comportamiento indefinido n. ° 1/3 :
... Supongo que esto se originó porque las operaciones de cambio subyacentes en varias CPU hacen cosas diferentes con esto: por ejemplo, X86 trunca la cantidad de desplazamiento de 32 bits a 5 bits (por lo que un desplazamiento de 32 bits es lo mismo que un cambio) por 0 bits), pero PowerPC trunca los cambios de 32 bits a 6 bits (por lo que un desplazamiento de 32 produce cero). Debido a estas diferencias de hardware, el comportamiento no está completamente definido por C ...
Nate que la discusión fue sobre cambiar una cantidad mayor que el tamaño del registro. Pero es lo más parecido que he encontrado a explicar las limitaciones de turno de una autoridad.
Creo que una segunda razón es el posible cambio de signo en una máquina complementaria de 2. Pero nunca lo he leído en ningún lado (sin ofender a @sellibitze (y estoy de acuerdo con él)).
El comportamiento en C ++ 03 es el mismo que en C ++ 11 y C99, solo necesita mirar más allá de la regla para el desplazamiento a la izquierda.
La Sección 5p5 del Estándar dice que:
Si durante la evaluación de una expresión, el resultado no está matemáticamente definido o no está en el rango de valores representables para su tipo, el comportamiento no está definido
Las expresiones de desplazamiento a la izquierda que se llaman específicamente en C99 y C ++ 11 como comportamiento no definido son las mismas que evalúan un resultado fuera del rango de valores representables.
De hecho, la oración acerca de los tipos sin signo que usan aritmética modular está allí específicamente para evitar la generación de valores fuera del rango representable, lo que automáticamente sería un comportamiento indefinido.
El párrafo que copió está hablando de tipos sin firmar. El comportamiento no está definido en C ++. Desde el último borrador de C ++ 0x:
El valor de E1 << E2 es E1 posiciones de bit E2 desplazadas a la izquierda; los bits vacantes están llenos a cero. Si E1 tiene un tipo sin signo, el valor del resultado es E1 × 2E ^ 2, módulo reducido uno más que el valor máximo representable en el tipo de resultado. De lo contrario, si E1 tiene un tipo firmado y un valor no negativo, y E1 × 2E ^ 2 es representable en el tipo de resultado, entonces ese es el valor resultante; de lo contrario, el comportamiento no está definido .
EDITAR: echó un vistazo al papel C ++ 98. Simplemente no menciona tipos firmados en absoluto. Entonces sigue siendo un comportamiento indefinido.
Right-shift negative es la implementación definida, a la derecha. ¿Por qué? En mi opinión: es fácil de implementar: defina porque no hay truncamiento de los problemas de la izquierda. Cuando se desplaza hacia la izquierda, debe decir no solo lo que se desplazó desde la derecha, sino también lo que sucede con el resto de las partes, por ejemplo, con la representación de dos complementos, que es otra historia.
El resultado del cambio depende de la representación numérica. El cambio se comporta como la multiplicación solo cuando los números se representan como complemento de dos. Pero el problema no es exclusivo de los números negativos. Considere un número firmado de 4 bits representado en exceso-8 (también conocido como offset binary). El número 1 se representa como 1 + 8 o 1001 Si salimos cambia esto como bits, obtenemos 0010, que es la representación de -6. Del mismo modo, -1 se representa como -1 + 8 0111 que se convierte en 1110 cuando se desplaza a la izquierda, la representación para +6. El comportamiento a nivel de bit está bien definido, pero el comportamiento numérico depende en gran medida del sistema de representación.
Hacer que el operador de desplazamiento a la izquierda invoque el Comportamiento no definido cuando el operando de la izquierda es negativo permite a los compiladores suponer que el cambio solo será alcanzable cuando el operando de la izquierda no sea negativo. Esto permite a los compiladores que reciben código como:
int do_something(int x)
{
if (x >= 0)
{
launch_missiles();
exit(1);
}
return x<<4;
}
para reconocer que tal método nunca se llamará con un valor negativo para x
, y así la prueba if
se puede eliminar y la llamada launch_missiles()
hecha incondicional. Como se sabe que la exit
no retorna, el compilador también puede omitir el cálculo de x<<4
. Si no fuera por esa regla, un programador tendría que insertar algún tipo de __assume(x >= 0);
directiva para solicitar tal comportamiento, pero haciendo cambios a la izquierda de los valores negativos Comportamiento no definido elimina la necesidad de tener un programador que obviamente quiere esa semántica (en virtud de realizar el cambio a la izquierda) para desordenar el código con ellos.
Nótese, por cierto, que en el caso hipotético de que el código llamara do_something(-1)
, estaría involucrado en un comportamiento indefinido, por lo que llamar a launch_missiles sería una cosa perfectamente legítima.
Muchos de estos tipos de cosas son un equilibrio entre lo que los CPU comunes realmente pueden admitir en una sola instrucción y lo que es útil esperar que los compiladores-escritores garanticen incluso si se necesitan instrucciones adicionales. Generalmente, un programador que usa operadores de cambio de bit espera que asigne instrucciones individuales a las CPU con tales instrucciones, por eso hay un comportamiento indefinido o de implementación donde las CPU tenían varias condiciones de "borde", en lugar de exigir un comportamiento y tener la operación ser inesperadamente lento. Tenga en cuenta que las instrucciones adicionales de pre / post o manejo pueden realizarse incluso para casos de uso más simples. el comportamiento indefinido puede haber sido necesario cuando algunas CPU generaron trampas / excepciones / interrupciones (a diferencia de las excepciones de tipo C ++ try / catch) o generalmente inútiles / inexplicables, mientras que si el conjunto de CPU consideradas por el Comité de Estándares en ese momento todas al menos algún comportamiento definido, entonces podrían definir la implementación del comportamiento.
Para responder a su pregunta real como se indica en el título: como para cualquier operación en un tipo firmado, esto tiene un comportamiento indefinido si el resultado de la operación matemática no cabe en el tipo de destino (subdesbordamiento o desbordamiento). Los tipos de enteros firmados están diseñados así.
Para la operación de desplazamiento a la izquierda si el valor es positivo o 0, la definición del operador como una multiplicación con una potencia de 2 tiene sentido, entonces todo está bien, a menos que el resultado se desborde, nada sorprendente.
Si el valor es negativo, podrías tener la misma interpretación de multiplicación con una potencia de 2, pero si solo piensas en términos de cambio de bit, esto sería quizás sorprendente. Obviamente, el comité de normas quería evitar esa ambigüedad.
Mi conclusión:
- si quiere hacer operaciones con patrones de bits reales, use tipos sin firmar
si quieres multiplicar un valor (firmado o no) por una potencia de dos, haz eso, algo como
i * (1u << k)
su compilador lo transformará en un ensamblador decente en cualquier caso.