Что такое оператор >>> = в C?

294

Заданный коллегой как загадка, я не могу понять, как на самом деле эта C-программа компилируется и работает. Что это за >>>=оператор и странный 1P1литерал? Я тестировал в Clang и GCC. Предупреждений нет и вывод "???"

#include <stdio.h>

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

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

    return 0;
}
CustomCalc
источник
36
Некоторые из них являются орграфами .
juanchopanza
12
@ Кей, нет в этом случае::> =] тогда a [...] >> = a [...]
Адриано Репетти
6
@Marc Я не думаю, что это может быть ">>> =", потому что это не скомпилирует, однако приведенный выше код на самом деле компилируется.
CustomCalc
21
Это 0x.1P1шестнадцатеричный литерал с показателем степени. Это 0x.1номер части, или 1/16 здесь. Число после 'P' является степенью двойки, на которую умножается число. Так 0x.1p1действительно 1/16 * 2 или 1/8. И если вам интересно, 0xFULLчто это просто 0xF, и ULLявляется суффиксом дляunsigned long long
jackarms
71
Синтаксис C - бесконечный материал для знатоков и мелочей, но в конечном итоге не все так важно.
Керрек С.Б.

Ответы:

468

Линия:

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

содержит орграфы :> и <:, которые переводят в ]и [соответственно, так что это эквивалентно:

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

Литерал 0xFULLтакой же как 0xF(который является шестнадцатеричным для 15); то ULLпросто указывает , что Это unsigned long longбуквальное . В любом случае, как логическое значение, оно истинно, поэтому 0xFULL ? '\0' : -1оценивается '\0'как символьный литерал , числовое значение которого просто 0.

Между тем, 0X.1P1это шестнадцатеричный литерал с плавающей точкой, равный 2/16 = 0,125. В любом случае, будучи ненулевым, это также верно как логическое значение, поэтому отрицание его дважды с помощью !!снова производит 1. Таким образом, все это упрощается до:

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

Оператор >>=является составным присваиванием, которое сдвигает бит левого операнда вправо на количество битов, заданных правым операндом, и возвращает результат. В этом случае правый операнд a[1]всегда имеет значение 1, поэтому оно эквивалентно:

while( a[0] >>= 1 )

или, что эквивалентно:

while( a[0] /= 2 )

Начальное значение a[0]равно 10. После однократного смещения вправо становится 5, затем (округление вниз) 2, затем 1 и, наконец, 0, после чего цикл заканчивается. Таким образом, тело цикла выполняется три раза.

Илмари Каронен
источник
18
Не могли бы вы подробнее остановиться на Pин 0X.1P1.
Кей - SE злой
77
@Kay: Это то же самое, что и eв 10e5, за исключением того, что вы должны использовать pдля шестнадцатеричных литералов, потому что eэто шестнадцатеричная цифра.
Дитрих Эпп
9
@Kay: шестнадцатеричные плавающие литералы являются частью C99, но GCC также принимает их в коде C ++ . Как отмечает Дитрих, pмантисса и показатель степени разделяются, как eв обычной научной нотации с плавающей точкой; Одно из отличий состоит в том, что при шестнадцатеричных числах основание экспоненциальной части равно 2 вместо 10, поэтому оно 0x0.1p1равно 0x0,1 = 1/16 умножить на 2¹ = 2. значение будет работать одинаково хорошо там.)
Ильмари Каронен
6
@chux: Очевидно, это зависит от того, скомпилирован ли код как C или (как он был изначально помечен) C ++. Но я исправил текст, чтобы сказать «буквальный символ» вместо « charбуквальный», и добавил ссылку в Википедии. Спасибо!
Ильмари Каронен
8
Хорошее сокращение.
Кори
69

Это некоторый довольно туманен код с участием диграфов , а именно , <:и :>которые являются альтернативными маркерами для [и ]соответственно. Существует также некоторое использование условного оператора . Также есть оператор сдвига битов , назначение сдвига вправо >>=.

Это более читаемая версия:

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

и еще более читаемая версия, заменяющая выражения в []значениях, для которых они разрешают:

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

Замена a[0]и a[1]их значения должны облегчить понимание того, что делает цикл, то есть эквивалент:

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

который просто выполняет (целочисленное) деление на 2 в каждой итерации, создавая последовательность 5, 2, 1.

juanchopanza
источник
Я не запускал его - разве это не произвело ????бы, а не так, ???как получил ОП? (Ха.) Codepad.org/nDkxGUNi действительно производит ???.
usr2564301
7
@ Jongware 10 были разделены на первой итерации. Значения, оцениваемые циклом, равны 5, 2, 1 и 0. Таким образом, он выводится только 3 раза.
MysticXG
42

Давайте пройдемся по выражению слева направо:

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

Первое, что я заметил, это то, что мы используем троичный оператор от использования ?. Итак, подвыражение:

0xFULL ? '\0' : -1

говорит "если 0xFULLне ноль, возвращайте '\0', в противном случае -1. 0xFULLэто шестнадцатеричный литерал с беззнаковым длинным-длинным суффиксом - это означает, что это шестнадцатеричный литерал типа unsigned long long. Это на самом деле не имеет значения, потому что 0xFможет помещаться внутри обычного целого числа.

Кроме того, троичный оператор преобразует типы второго и третьего членов в их общий тип. '\0'затем преобразуется в int, что просто 0.

Значение 0xFнамного больше нуля, поэтому оно проходит. Выражение теперь становится:

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

Далее :>идет орграф . Это конструкция, которая расширяется до ]:

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

>>=является подписанным оператором сдвига вправо, мы можем выделить его, aчтобы сделать его более понятным.

Кроме того, <:это орграф, который расширяется до [:

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

0X.1P1является шестнадцатеричным литералом с показателем степени. Но независимо от значения, !!все, что не равно нулю, верно. 0X.1P1это 0.125ненулевое значение, поэтому оно становится:

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

Это >>=подписанный оператор правого сдвига. Он изменяет значение своего левого операнда, сдвигая свои биты вперед на значение с правой стороны оператора. 10в двоичном есть 1010. Итак, вот шаги:

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

>>=возвращает результат своей операции, поэтому, пока смещение a[0]остается ненулевым, каждый раз, когда его биты сдвигаются вправо на единицу, цикл будет продолжаться. Четвертая попытка заключается в том, где a[0]делается 0, поэтому цикл никогда не вводится.

В результате ?печатается три раза.

0x499602D2
источник
3
:>это орграф , а не триграф. Он не обрабатывается препроцессором, он просто распознается как эквивалент токена ].
Кит Томпсон
@KeithThompson Спасибо
0x499602D2
1
Тернарный оператор ( ?:) имеет тип, который является общим типом второго и третьего членов. Первый член всегда является условным и имеет тип bool. Поскольку и второе, и третье слагаемые имеют тип, intрезультатом троичной операции будет int, а не unsigned long long.
Кори
2
@KeithThompson это может быть обработано препроцессором. Препроцессор должен знать о орграфах , потому что #и ##у орграфа формы; ничто не останавливает реализацию от перевода орграфиков к недиграфам на ранних этапах перевода
ММ,
@MattMcNabb Прошло много времени с тех пор, как я должен был это знать, но как следствие других требований IIRC, орграфы должны оставаться в форме орграфа до тех пор, пока pp-токены не преобразуются в токены (прямо в начале фазы перевода). 7).
zwol