operadores exclusive corrimiento bitwise and c++ c++11 bitwise-and type-promotion sign-extension

exclusive - operadores bitwise c++



Poco sabio ''&'' con operando firmado y sin firmar (7)

Echemos un vistazo a

uint64_t new_check = (check & 0xFFFF) << 16;

Aquí, 0xFFFF es una constante firmada, por lo que (check & 0xFFFF) nos da un entero firmado por las reglas de la promoción de enteros.

En su caso, con el tipo int 32 bits, el MSbit para este entero después del desplazamiento a la izquierda es 1, por lo que la extensión a 64 bits sin signo hará una extensión de signo, llenando los bits a la izquierda con 1. Interpretado como una representación de complemento a dos que da el mismo valor negativo.

En el segundo caso, 0xFFFFU no está firmado, por lo que obtenemos enteros sin signo y el operador de desplazamiento a la izquierda funciona como se esperaba.

Si su cadena de herramientas admite __PRETTY_FUNCTION__ , una característica muy útil, puede determinar rápidamente cómo el compilador percibe los tipos de expresión:

#include <iostream> #include <cstdint> template<typename T> void typecheck(T const& t) { std::cout << __PRETTY_FUNCTION__ << ''/n''; std::cout << t << ''/n''; } int main() { uint16_t check = 0x8123U; typecheck(0xFFFF); typecheck(check & 0xFFFF); typecheck((check & 0xFFFF) << 16); typecheck(0xFFFFU); typecheck(check & 0xFFFFU); typecheck((check & 0xFFFFU) << 16); return 0; }

Salida

void typecheck(const T &) [T = int] 65535 void typecheck(const T &) [T = int] 33059 void typecheck(const T &) [T = int] -2128412672 void typecheck(const T &) [T = unsigned int] 65535 void typecheck(const T &) [T = unsigned int] 33059 void typecheck(const T &) [T = unsigned int] 2166554624

Me enfrenté a un escenario interesante en el que obtuve resultados diferentes según el tipo de operando correcto, y realmente no puedo entender el motivo.

Aquí está el código mínimo:

#include <iostream> #include <cstdint> int main() { uint16_t check = 0x8123U; uint64_t new_check = (check & 0xFFFF) << 16; std::cout << std::hex << new_check << std::endl; new_check = (check & 0xFFFFU) << 16; std::cout << std::hex << new_check << std::endl; return 0; }

Compilé este código con g ++ (gcc versión 4.5.2) en Linux 64bit: g ++ -std = c ++ 0x -Wall example.cpp -o example

La salida fue:

ffffffff81230000

81230000

Realmente no puedo entender la razón de la salida en el primer caso.

¿Por qué en algún momento alguno de los resultados del cálculo temporal se promovería a un valor firmado de 64 bits ( int64_t ) que resultara en la extensión del signo?

Aceptaría un resultado de ''0'' en ambos casos si un valor de 16 bits se desplaza 16 bits a la izquierda en primer lugar y luego se promueve a un valor de 64 bits. También acepto la segunda salida si el compilador primero promueve la check a uint64_t y luego realiza las otras operaciones.

Pero, ¿por qué & con 0xFFFF ( int32_t ) frente a 0xFFFFU ( uint32_t ) se obtendrían esas dos salidas diferentes?


Eso es de hecho un caso interesante de la esquina. Solo ocurre aquí porque usas uint16_t para el tipo sin signo cuando la arquitectura usa 32 bits para ìnt

Aquí hay un extracto de Cláusulas 5 Expresiones del borrador n4296 para C ++ 14 (enfatice el mío):

10 Muchos operadores binarios que esperan operandos de tipo aritmético o de enumeración causan conversiones ... Este patrón se denomina conversiones aritméticas habituales, que se definen de la siguiente manera:
...
(10.5.3) - 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 sin signo tipo entero
(10.5.4) - 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.

Estás en el caso 10.5.4:

  • uint16_t solo tiene 16 bits, mientras que int es 32
  • int puede representar todos los valores de uint16_t

Por lo tanto, el uint16_t check = 0x8123U se convierte al 0x8123 firmado y el resultado a nivel de bits & aún es 0x8123.

Pero el cambio (a nivel de bits, como ocurre en el nivel de representación) hace que el resultado sea el 0x81230000 sin signo intermedio que se convierte en un int da un valor negativo (técnicamente se define por implementación, pero esta conversión es un uso común)

5.8 Operadores de turno [expr.shift]
...
De lo contrario, si E1 tiene un tipo con signo y un valor no negativo, y E1 × 2 E2 se puede representar en el tipo sin signo correspondiente del tipo de resultado, entonces ese valor, convertido al tipo de resultado, es el valor resultante; ...

y

4.7 Conversiones integrales [conv.integral]
...
3 Si el tipo de destino está firmado, el valor no cambia si se puede representar en el tipo de destino; de lo contrario, el valor está definido por la implementación .

(Tenga en cuenta que esto era cierto comportamiento indefinido en C ++ 11 ...)

Así que terminas con una conversión del int firmado 0x81230000 a un uint64_t que, como se esperaba, da 0xFFFFFFFF81230000, porque

4.7 Conversiones integrales [conv.integral]
...
2 Si el tipo de destino no tiene signo, el valor resultante es el entero con menos singruente congruente con el entero de origen (módulo 2n donde n es el número de bits utilizados para representar el tipo sin signo).

TL / DR: No hay ningún comportamiento indefinido aquí, lo que causa el resultado es la conversión de int de 32 bits con signo a int de 64 bits sin signo. La única parte parcial que es un comportamiento indefinido es un cambio que causaría un desbordamiento de signos, pero todas las implementaciones comunes comparten este y es la implementación definida en el estándar C ++ 14.

Por supuesto, si obliga al segundo operando a que no esté firmado, todo está sin firmar y, evidentemente, obtendrá el resultado 0x81230000 correcto.

[EDITAR] Según lo explicado por MSalters, el resultado del cambio es solo una implementación definida desde C ++ 14, pero de hecho fue un comportamiento indefinido en C ++ 11. El párrafo del operador del turno decía:

...
De lo contrario, si E1 tiene un tipo con signo y un valor no negativo, y E1 × 2 E2 se puede representar en el tipo de resultado , ese es el valor resultante; de lo contrario, el comportamiento es indefinido .


Este es el resultado de la promoción de enteros. Antes de que ocurra la operación & , si los operandos son "más pequeños" que un int (para esa arquitectura), el compilador promoverá ambos operandos a int , porque ambos encajan en un signed int :

Esto significa que la primera expresión será equivalente a (en una arquitectura de 32 bits):

// check is uint16_t, but it fits into int32_t. // the constant is signed, so it''s sign-extended into an int ((int32_t)check & (int32_t)0xFFFFFFFF)

mientras que el otro tendrá el segundo operando promovido a:

// check is uint16_t, but it fits into int32_t. // the constant is unsigned, so the upper 16 bits are zero ((int32_t)check & (int32_t)0x0000FFFFU)

Si realiza la check explícita a un unsigned int , entonces el resultado será el mismo en ambos casos ( unsigned * signed tendrá como resultado unsigned ):

((uint32_t)check & 0xFFFF) << 16

será igual a:

((uint32_t)check & 0xFFFFU) << 16


La operación & tiene dos operandos. El primero es un corto sin firmar, que se someterá a las promociones habituales para convertirse en un int. La segunda es una constante, en un caso de tipo int, en el otro caso de tipo unsigned int. El resultado de la & es, por lo tanto, int en un caso, unsigned int en el otro caso. Ese valor se desplaza hacia la izquierda, lo que da como resultado un int con el bit de signo establecido o un int sin signo. Convertir un int negativo en uint64_t dará un entero negativo grande.

Por supuesto, siempre debe seguir la regla: si hace algo y no comprende el resultado, ¡no lo haga!


Lo primero que debe darse cuenta es que los operadores binarios como a&b para los tipos integrados solo funcionan si ambos lados tienen el mismo tipo. (Con tipos y sobrecargas definidos por el usuario, todo vale). Esto podría realizarse a través de conversiones implícitas.

Ahora, en su caso, definitivamente hay tal conversión, porque simplemente no hay un operador binario & eso toma un tipo más pequeño que int . Ambos lados se convierten al menos en tamaño int , pero ¿qué tipos exactos?

Como sucede, en su GCC int es de hecho 32 bits. Esto es importante, porque significa que todos los valores de uint16_t pueden representarse como un int . No hay desbordamiento.

Por lo tanto, check & 0xFFFF es un caso simple. El lado derecho ya es un int , el lado izquierdo promueve a int , por lo que el resultado es int(0x8123) . Esto está perfectamente bien.

Ahora, la siguiente operación es 0x8123 << 16 . Recuerde, en su sistema int es de 32 bits, e INT_MAX es 0x7FFF''FFFF . En ausencia de desbordamiento, 0x8123 << 16 sería 0x81230000 , pero eso es claramente más grande que INT_MAX por lo que en realidad hay desbordamiento.

El desbordamiento de entero firmado en C ++ 11 es un comportamiento indefinido . Literalmente, cualquier resultado es correcto, incluido el purple o sin salida. Al menos tiene un valor numérico, pero se sabe que GCC elimina completamente las rutas de código que inevitablemente causan desbordamiento.

[editar] Las versiones más recientes de GCC son compatibles con C ++ 14, donde esta forma particular de desbordamiento se ha definido por la implementación - vea la respuesta de Serge.


Su plataforma tiene 32 bits int .

Su código es exactamente equivalente a

#include <iostream> #include <cstdint> int main() { uint16_t check = 0x8123U; auto a1 = (check & 0xFFFF) << 16 uint64_t new_check = a1; std::cout << std::hex << new_check << std::endl; auto a2 = (check & 0xFFFFU) << 16; new_check = a2; std::cout << std::hex << new_check << std::endl; return 0; }

¿Cuál es el tipo de a1 y a2 ?

  • Para a2 , el resultado se promueve a unsigned int .
  • Más interesante aún, para a1 el resultado se promueve a int , y luego se amplía a uint64_t señal a medida que se amplía a uint64_t .

Aquí hay una demostración más corta, en decimal, para que la diferencia entre los tipos firmados y no firmados sea evidente:

#include <iostream> #include <cstdint> int main() { uint16_t check = 0; std::cout << check << " " << (int)(check + 0x80000000) << " " << (uint64_t)(int)(check + 0x80000000) << std::endl; return 0; }

En mi sistema (también de 32 bits int ), obtengo

0 -2147483648 18446744071562067968

Mostrando dónde sucede la promoción y la extensión del signo.


0xFFFF es un int firmado. Entonces, después de la operación & , tenemos un valor firmado de 32 bits:

#include <stdint.h> #include <type_traits> uint64_t foo(uint16_t a) { auto x = (a & 0xFFFF); static_assert(std::is_same<int32_t, decltype(x)>::value, "not an int32_t") static_assert(std::is_same<uint16_t, decltype(x)>::value, "not a uint16_t"); return x; }

http://ideone.com/tEQmbP

Los 16 bits originales se desplazan a la izquierda, lo que da como resultado un valor de 32 bits con el conjunto de bits altos (0x80000000U), por lo que tiene un valor negativo. Durante la conversión de 64 bits, se produce la extensión de signo, llenando las palabras superiores con 1s.