¿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?

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

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 rest de las partes, por ejemplo, con la representación de dos complementos, que es otra historia.

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.

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 comstackdor lo transformará en un ensamblador decente en cualquier caso.

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 comstackdores-escritores garanticen incluso si se necesitan instrucciones adicionales. Generalmente, un progtwigdor 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.

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 progtwigdor 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.

En C89, el comportamiento de los valores negativos de desplazamiento a la izquierda se definió inequívocamente en las plataformas complementarias de dos que no utilizaban los bits de relleno en los tipos enteros con signo y sin signo. Los bits de valor que los tipos con signo y sin signo tenían en común estar en los mismos lugares, y el único lugar al que podía ir el bit de signo para un tipo firmado, estaba en el mismo lugar que el bit de valor superior para tipos sin firmar, que a su vez estar a la izquierda de todo lo demás.

Los comportamientos obligatorios C89 fueron útiles y razonables para las plataformas de dos complementos sin relleno, al menos en los casos en que tratarlos como multiplicación no causaría desbordamiento. El comportamiento puede no haber sido óptimo en otras plataformas o en implementaciones que intentan atrapar de manera confiable el desbordamiento de entero con signo. Los autores de C99 probablemente querían permitir la flexibilidad de las implementaciones en los casos en que el comportamiento obligatorio C89 hubiera sido menos que ideal, pero nada en el fundamento sugiere una intención de que las implementaciones de calidad no continúen comportándose a la vieja usanza en casos donde había no hay una razón convincente para hacer lo contrario.

Desafortunadamente, aunque nunca hubo implementaciones de C99 que no usen matemática complementaria de dos, los autores de C11 declinaron definir el comportamiento de caso común (sin desbordamiento); IIRC, el reclamo era que hacerlo impediría la “optimizació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 comstackdores suponer que el cambio solo será alcanzable cuando el operando de la izquierda no sea negativo. Esto permite a los comstackdores 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 comstackdor también puede omitir el cálculo de x<<4 . Si no fuera por esa regla, un progtwigdor 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 progtwigdor 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.

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.