Странные результаты для условного оператора с указателями GCC и bool

17

В следующем коде значение переменной% %

задан jpa 26.12.2014 в 21:42
источник
  • От стандарта C99: 6.5.2 2 Объект, объявленный как тип _Bool, достаточно велик, чтобы хранить значения 0 и 1. –  Severin Pappadeux 26.12.2014 в 21:49
  • Определив x как bool, вы пообещали компилятору, что вы будете хранить только 0 или 1. Сохраняя 123 в x1, вы лгали компилятору. «Если вы лжете компилятору, он отомстит». - Генри Спенсер –  Keith Thompson 26.12.2014 в 21:52
  • @SeverinPappadeux: Да, но поскольку любой объект, не являющийся битовым полем, должен иметь бит CHAR_BIT (и CHAR_BIT> = 8), он также достаточно велик, чтобы удерживать значение 123. Вам не мешает хранить 123 в bool из-за его размера, но это неопределенное поведение. –  Keith Thompson 26.12.2014 в 21:53
  • Поскольку у вас есть заголовок <stdbool.h>, вы можете использовать только значение true и false (как и в Pascal с булевым типом). Это сделает ваш код немного читабельнее и уберет вас от других ценностей. –  Grzegorz Szpetkowski 26.12.2014 в 23:33
  • @MSalters: некоторые битовые шаблоны могут быть ловушками; доступ к объекту с таким представлением (через lvalue соответствующего типа) вызывает неопределенное поведение. Правила для _Bool сговариваются, чтобы сделать вещи запутанными способами, которые я слишком ленив, чтобы исследовать на данный момент. Итог: сохранение значений, отличных от 0 и 1, в объекте _Bool - это то, чего следует избегать. –  Keith Thompson 27.12.2014 в 03:02
Показать остальные комментарии

3 ответа

14
  

(Возможно, это неопределенное поведение?)

Не напрямую, но чтение с объекта после этого.

Цитата C99:

  

6.2.6 Представления типов

     

6.2.6.1 Общие сведения

     

5 Некоторые представления объектов не обязательно должны представлять значение типа объекта. Если сохраненный   значение объекта имеет такое представление и считывается выражением lvalue, которое делает   не имеют характера, поведение не определено. [...]

В основном, это означает, что если конкретная реализация решила, что только два действительных байта для bool составляют 0 и 1 , тогда вам лучше убедиться, что вы не используете никаких обманов чтобы попытаться установить его на любое другое значение.

    
ответ дан hvd 26.12.2014 в 21:46
  • Gcc doc читает, что GCC поддерживает только два целочисленных типа дополнения, и все битовые шаблоны являются обычными значениями. Это означает, что _Bool также не имеет ловушечных представлений. Не уверен, если это небрежность в документации, или если есть что-то еще в стандарте, допускающем эту оптимизацию. –  mafso 27.12.2014 в 07:50
  • @mafso Я думаю, что это небрежность в формулировке. Неподписанные целые типы (включая _Bool) никогда не могут использовать два дополнения, потому что у них нет никакого знакового бита вообще. –  hvd 27.12.2014 в 11:50
  • Да, дополнение двух не применяется, но это не изменяет «все битовые шаблоны являются обычными значениями», а _Bool должен иметь не менее 8 бит. Чтобы поставить мой вопрос по-другому: было бы, строго говоря, необходимо, чтобы Gcc документировал _Bool как имеющий CHAR_BIT-1 биты заполнения, чтобы сделать оптимизацию в вопросе возможной? Я не уверен, есть ли в стандарте, возможно, другая часть, требующая этого. [...] –  mafso 27.12.2014 в 12:27
  • [...] Этот _Bool должен быть способен представлять 0, а 1 не означает, что UB хранит в нем что-то другое. То, что символ должен быть способен удерживать каждый символ набора основных символов исполнения, не означает, что вы не можете хранить в нем что-либо еще (127 совершенно легально, например, независимо от того, находится ли это в базовом наборе символов выполнения). –  mafso 27.12.2014 в 12:28
  • @mafso В стандарте для реализации нет требования о том, имеет ли какой-либо тип биты заполнения, кроме косвенно, путем сравнения sizeof (T) * CHAR_BIT со значением T_MAX, как определено в <limits.h>. Если у типа есть биты заполнения, в стандарте для реализации нет требования о том, являются ли эти биты дополнений значительными (могут ли ошибочные значения битов заполнения выдавать ловушечные представления). –  hvd 27.12.2014 в 14:09
Показать остальные комментарии
14

Когда GCC компилирует эту программу, выход языка ассемблера включает последовательность

movzbl (%rax), %eax
movzbl %al, %eax
movl %eax, -4(%rbp)

, который выполняет следующие действия:

  1. Скопируйте 32 бита из *foo (обозначенные (%rax) в сборке) в регистр %eax и заполните старшие биты %eax нулями (не так, чтобы их было, потому что %eax является 32-разрядным регистром).
  2. Скопируйте 8-битные разряды 8% %eax (обозначенные %al ) на %eax и заполните бит более высокого порядка %eax нулями. Как программист на C, вы понимаете это как %eax &= 0xff .
  3. Скопируйте значение %eax в 4 байта выше %rbp , которое является местоположением bar в стеке.

Таким образом, этот код является ассемблерным переводом

int bar = *foo & 0xff;

Очевидно, что GCC оптимизировал линию на основании того факта, что bool никогда не должно содержать значения, отличные от 0 или 1.

Если вы измените соответствующую строку в источнике C на этот

int bar = *((int*)foo) ? 1 : 0;

, тогда сборка изменится на

movl (%rax), %eax
testl %eax, %eax
setne %al
movzbl %al, %eax
movl %eax, -4(%rbp)

, который выполняет следующие действия:

  1. Скопировать 32 бита из *foo (обозначено (%rax) в сборке) в регистр %eax .
  2. Протестируйте 32 бита %eax против себя, что означает ANDing его с собой и установкой некоторых флагов в процессоре на основе результата. (Здесь AND здесь нет необходимости, но нет инструкции просто проверять регистр и устанавливать флаги.)
  3. Задайте младшие 8 бит %eax (обозначенные %al ) равным 1, если результат ANDing равен 0 или 0 в противном случае.
  4. Скопируйте 8 бит 8-го порядка %eax (обозначенные %al ) на %eax и запишем младшие разряды %eax нулями, как в первом фрагменте.
  5. Скопируйте значение %eax в 4 байта выше %rbp , которое является местоположением bar в стеке; также как и в первом фрагменте.

Это действительно верный перевод кода на C. И действительно, если вы добавите бросок в (int*) и скомпилируете и запустите программу, вы увидите, что он выводит 1 .

    
ответ дан David Z 27.12.2014 в 10:17
  • Это, безусловно, лучший ответ. Думаю, у вас меньше голосов за то, что он «опоздал на вечеринку». –  Alex 27.12.2014 в 12:04
  • Это говорит «что делает GCC на самом деле», что является хорошим дополнением к ответу hvd «почему GCC разрешено это делать». Меня больше интересовало последнее, поэтому я принял это вместо этого. –  jpa 27.12.2014 в 16:25
  • @jpa да, я подумал, что это будет хорошим дополнением к другому ответу. Хотя я бы отметил, что ваш вопрос на самом деле ничего не спрашивает (он просто говорит «Посмотрите на это странное поведение»), что делает его довольно трудно сказать, какой ответ вы хотели. –  David Z 27.12.2014 в 20:01
12

Сохранение значения, отличного от 0 или 1 в bool , является неопределенным поведением в C.

Итак, на самом деле это:

int bar = *foo ? 1 : 0;

оптимизирован с чем-то близким к этому:

int bar = *foo ? *foo : 0;
    
ответ дан ouah 26.12.2014 в 21:45
  • Вы можете пойти еще дальше и сказать, что, поскольку x? x: 0 - это тождество в этом случае, оно далее оптимизируется только на x, откуда результат. –  Iwillnotexist Idonotexist 26.12.2014 в 23:03