Почему присвоение значения битовому полю не возвращает то же значение?

96

В этом сообщении на Quora я видел следующий код :

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = 1;
  if(s.enabled == 1)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

Как в C, так и в C ++ вывод кода является неожиданным ,

Выключен !!

Хотя объяснение, связанное с «битом знака», дается в этом посте, я не могу понять, как это возможно, что мы что-то устанавливаем, а затем это не отражает как есть.

Может кто-нибудь дать более подробное объяснение?


Примечание : оба тега & являются обязательными, потому что их стандарты немного отличаются для описания битовых полей. См ответы спецификации C и спецификации C ++ .

iammilind
источник
46
Поскольку битовое поле объявлено, как intя думаю, оно может содержать только значения 0и -1.
Осирис
6
просто подумайте, как int хранит -1. Все биты установлены в 1. Следовательно, если у вас есть только один бит, он явно должен быть -1. Итак, 1 и -1 в 1 бите int одинаковы. Измените проверку на if (s.enabled! = 0), и она заработает. Потому что 0 этого не может быть.
Юрген
3
Верно, что эти правила одинаковы в C и C ++. Но согласно политике использования тегов , мы должны помечать это только как C и воздерживаться от перекрестных тегов, когда они не нужны. Я удалю часть C ++, она не должна влиять на опубликованные ответы.
Lundin
8
Вы пробовали поменять его на struct mystruct { unsigned int enabled:1; };?
ChatterOne
4
Пожалуйста, ознакомьтесь с политиками тегов C и C ++ , особенно с частью, касающейся перекрестных тегов для C и C ++, установленной здесь на основе консенсуса сообщества . Я не собираюсь в какую-то войну отката, но этот вопрос неправильно помечен как C ++. Даже если языки имеют небольшую разницу из-за различных TC, тогда задайте отдельный вопрос о разнице между C и C ++.
Lundin

Ответы:

78

Битовые поля невероятно плохо определены стандартом. Учитывая этот код struct mystruct {int enabled:1;};, мы не знаем:

  • Сколько места это занимает - есть ли биты / байты заполнения и где они расположены в памяти.
  • Где находится бит в памяти. Не определено и также зависит от порядка байтов.
  • int:nСледует ли рассматривать битовое поле как подписанное или беззнаковое.

Что касается последней части, C17 6.7.2.1/10 говорит:

Битовое поле интерпретируется как имеющее знаковый или беззнаковый целочисленный тип, состоящий из указанного количества битов 125)

Ненормативное примечание, поясняющее вышеуказанное:

125) Как указано в п. 6.7.2 выше, если фактический используемый спецификатор типа intили typedef-name определен как int, то определяется реализацией, является ли битовое поле подписанным или беззнаковым.

В случае, если битовое поле должно рассматриваться как signed intи вы делаете немного размера 1, тогда нет места для данных, только для бит знака. Это причина, по которой ваша программа может давать странные результаты на некоторых компиляторах.

Хорошая практика:

  • Никогда не используйте битовые поля для каких-либо целей.
  • Избегайте использования подписанного intтипа для любых форм битовых манипуляций.
Лундин
источник
5
На работе у нас есть static_asserts для размера и адреса битовых полей, чтобы убедиться, что они не заполнены. В нашей прошивке мы используем битовые поля для аппаратных регистров.
Майкл
4
@Lundin: Уродство с масками и смещениями #define-d в том, что ваш код завален сдвигами и побитовыми операторами AND / OR. С битовыми полями компилятор позаботится об этом за вас.
Майкл
4
@Michael С битовыми полями компилятор позаботится об этом за вас. Что ж, это нормально, если ваши стандарты для «заботится об этом» «непереносимы» и «непредсказуемы». Мои выше этого.
Эндрю Хенле
3
@AndrewHenle Leushenko говорит, что с точки зрения самого стандарта C , это зависит от реализации, решит ли она следовать x86-64 ABI или нет.
mtraceur
3
@AndrewHenle: Верно, я согласен по обоим пунктам. Я считаю, что ваше несогласие с Леушенко сводится к тому, что вы используете термин «реализация, определенная» для обозначения только вещей, которые не определены строго стандартом C и не определены строго платформой ABI, и он использует это для ссылки ко всему, что не определено строго стандартом C.
mtraceur
58

Я не могу понять, как это возможно, что мы что-то устанавливаем, а потом это не отображается как есть.

Вы спрашиваете, почему он компилируется vs. дает вам ошибку?

Да, в идеале он должен выдать ошибку. И это так, если вы используете предупреждения вашего компилятора. В GCC с -Werror -Wall -pedantic:

main.cpp: In function 'int main()':
main.cpp:7:15: error: overflow in conversion from 'int' to 'signed char:1' 
changes value from '1' to '-1' [-Werror=overflow]
   s.enabled = 1;
           ^

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

Чтобы добавить некоторого прескриптивизма, я повторю утверждение @Lundin: «Никогда не используйте битовые поля для каких-либо целей». Если у вас есть веские причины, чтобы получить низкоуровневую и конкретную информацию о деталях макета памяти, которые заставят вас думать, что вам нужны битовые поля в первую очередь, другие связанные требования, которые у вас почти наверняка будут, столкнутся с их недостаточной спецификацией.

(TL; DR - Если вы достаточно опытны, чтобы законно "нуждаться" в битовых полях, они недостаточно четко определены, чтобы служить вам.)

HostileFork говорит, что не доверяйте SE
источник
15
В день написания главы о битовых полях авторы стандарта были в отпуске. Значит, это должен был сделать дворник. Там нет никаких оснований о что - либо о том , как битовые поля предназначены.
Lundin
9
Нет внятного технического обоснования. Но это приводит меня к выводу, что имелось политическое обоснование: избегать неправильного использования любого существующего кода или реализаций. Но в результате очень мало битовых полей, на которые можно положиться.
Джон Боллинджер
6
@JohnBollinger Определенно была политика, которая нанесла C90 большой ущерб. Однажды я поговорил с членом комитета, который объяснил источник большого количества этой ерунды - нельзя допускать, чтобы стандарт ISO отдавал предпочтение определенным существующим технологиям. Вот почему мы застряли в глупых вещах, таких как поддержка дополнения до единицы и величины со знаком char, подписи, определяемой реализацией , поддержки байтов, отличных от 8 бит и т. Д. И т. Д. Им не разрешалось ставить дебильные компьютеры в невыгодное положение на рынке.
Lundin
1
@Lundin Было бы интересно увидеть коллекцию рецензий и вскрытий от людей, которые считали, что компромисс был сделан по ошибке, и почему. Мне интересно, насколько изучение этих слов «мы делали это в прошлый раз, и это сработало / не сработало» превратилось в институциональные знания, необходимые для следующего такого случая, по сравнению с просто историями в головах людей.
HostileFork говорит, что не доверяйте SE
1
Это по-прежнему указано как пункт № Один из исходных принципов C в Хартии C2x: «Существующий код важен, существующие реализации - нет». ... «ни одна реализация не была представлена ​​как образец для определения C: предполагается, что все существующие реализации должны несколько измениться, чтобы соответствовать Стандарту».
Леушенко
23

Это поведение, определяемое реализацией. Я делаю предположение, что машины, на которых вы запускаете это, используют целые числа со знаком, дополняющие два, и обрабатывают intв этом случае целое число со знаком, чтобы объяснить, почему вы не вводите if true часть оператора if.

struct mystruct { int enabled:1; };

объявляется enableкак 1-битовое битовое поле. Поскольку он подписан, допустимыми значениями являются -1и 0. Настройка поля на 1переполнение этого бита, возвращающегося к -1(это неопределенное поведение)

По существу , когда имеет дело с подписанным битовым полем максимального значение , 2^(bits - 1) - 1которое является 0в данном случае.

Натан Оливер
источник
"поскольку он подписан, допустимые значения -1 и 0". Кто сказал, что это подписано? Это не определено, но определяется реализацией. Если он подписан, то допустимыми значениями являются -и +. Дополнение 2 не имеет значения.
Лундин
5
@Lundin 1-битное двойное комплиментное число имеет только два возможных значения. Если бит установлен, то, поскольку это бит знака, он равен -1. Если он не установлен, то это "положительный" 0. Я знаю, что это определено в реализации, я просто объясняю результаты, используя наиболее распространенную имплантацию
Натан Оливер
1
Ключевым моментом здесь является то, что дополнение 2 или любая другая подписанная форма не может работать с одним доступным битом.
Lundin
1
@JohnBollinger Я понимаю это. Вот почему у меня есть оговорка, что это определение реализации. По крайней мере, для большой тройки intв этом случае все они считаются подписанными. Обидно, что битовые поля так недоопределены. В основном вот эта функция, проконсультируйтесь со своим компилятором, как ее использовать.
NathanOliver
1
@Lundin, стандартная формулировка представления целых чисел со знаком, может отлично справиться со случаем, когда есть биты с нулевым значением, по крайней мере, в двух из трех допустимых альтернатив. Это работает , потому что он присваивает (отрицательное) значение места , чтобы подписать биты, а не давать им алгоритмическую интерпретацию.
Джон Боллинджер
10

Вы можете думать об этом как о том, что в системе дополнения до 2 крайний левый бит является битом знака. Таким образом, любое целое число со знаком с крайним левым битом является отрицательным значением.

Если у вас есть 1-битное целое число со знаком, оно имеет только знаковый бит. Таким образом, присвоение 1этому единственному биту может установить только знаковый бит. Таким образом, при чтении его значение интерпретируется как отрицательное, поэтому оно равно -1.

Значения 1 бит знаковое целое способна хранить -2^(n-1)= -2^(1-1)= -2^0= -1и2^n-1= 2^1-1=0

Пол Огилви
источник
8

В соответствии со стандартом C ++ n4713 предоставляется очень похожий фрагмент кода. Используемый тип - BOOL(пользовательский), но он может применяться к любому типу.

12.2.4

4 Если значение «истина» или «ложь» сохраняется в битовом полеboolлюбого размера (включая однобитовое битовое поле), исходноеboolзначение и значение битового поля должны сравниваться равными. Если значение перечислителя хранится в битовом поле того же типа перечисления и количество битов в битовом поле достаточно велико, чтобы содержать все значения этого типа перечисления (10.2), исходное значение перечислителя и значение битового поля должно сравниваться равным . [ Пример:

enum BOOL { FALSE=0, TRUE=1 };
struct A {
  BOOL b:1;
};
A a;
void f() {
  a.b = TRUE;
  if (a.b == TRUE)    // yields true
    { /* ... */ }
}

- конец примера]


На первый взгляд жирная часть кажется открытой для интерпретации. Однако правильное намерение становится ясным, когда enum BOOLобъект является производным от int.

enum BOOL : int { FALSE=0, TRUE=1 }; // ***this line
struct mystruct { BOOL enabled:1; };
int main()
{
  struct mystruct s;
  s.enabled = TRUE;
  if(s.enabled == TRUE)
    printf("Is enabled\n"); // --> we think this to be printed
  else
    printf("Is disabled !!\n");
}

С приведенным выше кодом он дает предупреждение без -Wall -pedantic:

предупреждение: mystruct :: enabled слишком мал, чтобы содержать все значения enum BOOL struct mystruct { BOOL enabled:1; };

Результат:

Выключен !! (при использовании enum BOOL : int)

Если enum BOOL : intсделано просто enum BOOL, то вывод будет таким, как указано в приведенном выше стандартном пассаже:

Включено (при использовании enum BOOL)


Следовательно, можно сделать вывод, как и несколько других ответов, что этот intтип недостаточно велик для хранения значения «1» только в одном битовом поле.

iammilind
источник
0

Нет ничего плохого в вашем понимании битовых полей, которые я вижу. Я вижу, что вы сначала переопределили mystruct как struct mystruct {int enabled: 1; } а затем как struct mystruct s; . Вы должны были закодировать:

#include <stdio.h>

struct mystruct { int enabled:1; };
int main()
{
    mystruct s; <-- Get rid of "struct" type declaration
    s.enabled = 1;
    if(s.enabled == 1)
        printf("Is enabled\n"); // --> we think this to be printed
    else
        printf("Is disabled !!\n");
}
ar18
источник