¿Cuál es el operador >>> = en C?

294

Dado por un colega como un rompecabezas, no puedo entender cómo este programa C realmente compila y se ejecuta. ¿Qué es este >>>=operador y el extraño 1P1literal? He probado en Clang y GCC. No hay advertencias y la salida es "???"

#include <stdio.h>

int main()
{
    int a[2]={ 10, 1 };

    while( a[ 0xFULL?'\0':-1:>>>=a<:!!0X.1P1 ] )
        printf("?");

    return 0;
}
CustomCalc
fuente
36
Algunos de esos son dígrafos .
juanchopanza
12
@Kay, no en este caso::> =] luego un [...] >> = a [...]
Adriano Repetti
66
@Marc No creo que pueda ser ">>> =" porque eso no se compilaría, sin embargo, el código anterior realmente se compila.
CustomCalc
21
El 0x.1P1es un literal hexadecimal con un exponente. El 0x.1es la parte del número, o 1/16 aquí. El número después de la 'P' es la potencia de dos, el número se multiplica por. Entonces 0x.1p1es realmente 1/16 * 2, o 1/8. Y si te estabas preguntando acerca de 0xFULLeso es justo 0xF, y ULLes el sufijo para ununsigned long long
jackarms
71
Sintaxis C: material interminable para expertos y amantes de la trivia, pero en última instancia no es tan importante.
Kerrek SB

Respuestas:

468

La línea:

while( a[ 0xFULL?'\0':-1:>>>=a<:!!0X.1P1 ] )

contiene los dígrafos :> y <:, que se traducen a ]y [respectivamente, por lo que es equivalente a:

while( a[ 0xFULL?'\0':-1 ] >>= a[ !!0X.1P1 ] )

El literal 0xFULLes el mismo que 0xF(que es hexadecimal 15); el ULLsolo especifica que es un unsigned long longliteral . En cualquier caso, como booleano es cierto, así se 0xFULL ? '\0' : -1evalúa como '\0', que es un carácter literal cuyo valor numérico es simplemente 0.

Mientras tanto, 0X.1P1es un literal de coma flotante hexadecimal igual a 2/16 = 0.125. En cualquier caso, al ser distinto de cero, también es cierto como un booleano, por lo que negarlo dos veces con !!nuevamente produce 1. Por lo tanto, todo se simplifica a:

while( a[0] >>= a[1] )

El operador >>=es una asignación compuesta que desplaza su operando izquierdo hacia la derecha por el número de bits dados por el operando derecho y devuelve el resultado. En este caso, el operando correcto a[1]siempre tiene el valor 1, por lo que es equivalente a:

while( a[0] >>= 1 )

o equivalente:

while( a[0] /= 2 )

El valor inicial de a[0]es 10. Después de desplazarse a la derecha una vez, se convierte en 5, luego (redondeando hacia abajo) 2, luego 1 y finalmente 0, en cuyo punto finaliza el ciclo. Por lo tanto, el cuerpo del bucle se ejecuta tres veces.

Ilmari Karonen
fuente
18
¿Podría explicar con más detalle la Pen 0X.1P1.
kay - SE es malvado
77
@Kay: Es lo mismo que een 10e5, excepto que tiene que usar ppara literales hexadecimales porque ees un dígito hexadecimal.
Dietrich Epp
99
@Kay: Los literales flotantes hexadecimales son parte de C99, pero GCC también los acepta en código C ++ . Como señala Dietrich, psepara la mantisa y el exponente, al igual que een la notación científica flotante normal; Una diferencia es que, con flotadores hexagonales, la base de la parte exponencial es 2 en lugar de 10, por lo que 0x0.1p1es igual a 0x0.1 = 1/16 veces 2¹ = 2. (En cualquier caso, nada de eso importa aquí; el valor funcionaría igual de bien allí.)
Ilmari Karonen
66
@chux: Aparentemente, eso depende de si el código se compila como C o (como se etiquetó originalmente) C ++. Pero arreglé el texto para decir "carácter literal" en lugar de " charliteral", y agregué un enlace de Wikipedia. ¡Gracias!
Ilmari Karonen
8
Buena reducción
Corey
69

Es un código bastante oscuro que involucra dígrafos , a saber, <:y :>que son tokens alternativos para [y ]respectivamente. También hay algún uso del operador condicional . También hay un operador de desplazamiento leve , la asignación de desplazamiento correcta >>=.

Esta es una versión más legible:

while( a[ 0xFULL ? '\0' : -1 ] >>= a[ !!0X.1P1 ] )

y una versión aún más legible, reemplazando las expresiones en []los valores por los que resuelven:

while( a[0] >>= a[1] )

Reemplazar a[0]y a[1]por sus valores debería facilitar la comprensión de lo que está haciendo el ciclo, es decir, el equivalente de:

int i = 10;
while( i >>= 1)

que simplemente realiza la división (entera) por 2 en cada iteración, produciendo la secuencia 5, 2, 1.

juanchopanza
fuente
No lo ejecuté, ¿no produciría esto ????, sin embargo, en lugar de lo que ???obtuvo el OP? (Huh.) Codepad.org/nDkxGUNi hace productos ???.
usr2564301
77
@Jongware el 10 se dividió en la primera iteración. Por lo tanto, los valores que evalúa el bucle son 5, 2, 1 y 0. Por lo tanto, solo se imprime 3 veces.
MysticXG
42

Veamos la expresión de izquierda a derecha:

a[ 0xFULL?'\0':-1:>>>=a<:!!0X.1P1 ]

Lo primero que noto es que estamos usando el operador ternario del uso de ?. Entonces la subexpresión:

0xFULL ? '\0' : -1

dice "si 0xFULLno es cero, return '\0', de lo contrario -1. 0xFULLes un literal hexadecimal con el sufijo largo largo sin signo , lo que significa que es un literal hexadecimal de tipo unsigned long long. Sin embargo, eso realmente no importa, porque 0xFpuede caber dentro de un entero regular.

Además, el operador ternario convierte los tipos del segundo y tercer término a su tipo común. '\0'luego se convierte a int, que es justo 0.

El valor de 0xFes mucho mayor que cero, por lo que pasa. La expresión ahora se convierte en:

a[ 0 :>>>=a<:!!0X.1P1 ]

A continuación, :>hay un dígrafo . Es una construcción que se expande a ]:

a[0 ]>>=a<:!!0X.1P1 ]

>>=es el operador de desplazamiento a la derecha firmado, podemos espaciarlo apara aclararlo.

Además, <:es un dígrafo que se expande a [:

a[0] >>= a[!!0X.1P1 ]

0X.1P1es un literal hexadecimal con un exponente. Pero no importa el valor, !!todo lo que no sea cero es cierto. 0X.1P1es 0.125que no es cero, por lo que se convierte en:

a[0] >>= a[true]
-> a[0] >>= a[1]

El >>=es el operador de desplazamiento a la derecha firmado. Cambia el valor de su operando izquierdo desplazando sus bits hacia adelante por el valor en el lado derecho del operador. 10en binario es 1010. Así que aquí están los pasos:

01010 >> 1 == 00101
00101 >> 1 == 00010
00010 >> 1 == 00001
00001 >> 1 == 00000

>>=devuelve el resultado de su operación, de modo que mientras el desplazamiento a[0]permanezca distinto de cero por cada vez que sus bits se desplacen uno a la derecha, el bucle continuará. El cuarto intento es donde se a[0]hace 0, por lo que el ciclo nunca se ingresa.

Como resultado, ?se imprime tres veces.

0x499602D2
fuente
3
:>es un dígrafo , no un trigrafo. No es manejado por el preprocesador, simplemente se reconoce como un token equivalente a ].
Keith Thompson el
@KeithThompson Gracias
0x499602D2
1
El operador ternario ( ?:) tiene un tipo que es el tipo común de los términos segundo y tercero. El primer término es siempre condicional y tiene un tipo bool. Dado que tanto el segundo como el tercer término tienen tipo, intel resultado de la operación ternaria será int, no unsigned long long.
Corey
2
@KeithThompson podría ser manejado por el preprocesador. El preprocesador tiene que saber acerca de los dígrafos porque #y ##tiene formas de dígrafo; no hay nada que impida que una implementación traduzca dígrafos a no dígrafos durante las primeras fases de traducción
MM
@MattMcNabb Ha pasado mucho tiempo desde que tuve que saber esto, pero IIRC como consecuencia de otros requisitos, los dígrafos tienen que permanecer en su forma de dígrafo hasta el punto en que los tokens pp se convierten en tokens (justo al comienzo de la fase de traducción 7)
zwol