Чому присвоєння значення бітовому полю не повертає однакове значення?


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 ++ .


46
Оскільки бітове поле оголошено як intя думаю, воно може містити лише значення 0та -1.
Осіріс

6
просто подумайте, як int store -1. Для всіх бітів встановлено значення 1. Отже, якщо у вас є лише один біт, він однозначно повинен бути -1. Отже, 1 та -1 у 1-бітному int однакові. Змініть позначку на "якщо (s.enabled! = 0)", і вона працює. Тому що 0 цього не може бути.
Юрген

3
Це правда, що ці правила однакові для C та C ++. Але згідно з політикою використання тегів , ми повинні позначати це як C і утримуватися від перехресного позначення, коли це не потрібно. Я видалю частину C ++, це не повинно впливати на будь-які розміщені відповіді.
Лундін

8
Ви пробували змінити його на struct mystruct { unsigned int enabled:1; };?
ChatterOne

4
Будь ласка, прочитайте політику тегів C та C ++ , зокрема частину, що стосується перехресного позначення тегів C та C ++, встановлену тут консенсусом спільноти . Я не вступаю в якусь відмову, але це питання неправильно позначено на C ++. Навіть якщо у мов є деякі незначні відмінності через різні TC, тоді задайте окреме запитання про різницю між C та C ++.
Лундін,

Відповіді:


78

Бітові поля неймовірно погано визначені стандартом. Враховуючи цей код struct mystruct {int enabled:1;};, ми не знаємо:

  • Скільки місця це займає - якщо є біти / байти заповнення та де вони знаходяться в пам'яті.
  • Де біт знаходиться в пам'яті. Не визначено, а також залежить від схильності.
  • Чи int:nслід розглядати бітове поле як підписане чи непідписане.

Щодо останньої частини, C17 6.7.2.1/10 говорить:

Бітове поле інтерпретується як таке, що має цілочисельний тип із підписом або без знака, що складається із зазначеної кількості бітів 125)

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

125) Як зазначено в 6.7.2 вище, якщо фактичним використовуваним специфікатором типу є intабо ім'я typedef, визначене як int, тоді визначається реалізацією, підписано чи не підписано поле біта.

Якщо бітове поле слід розглядати як signed intі ви зробите трохи розміру 1, тоді місця для даних немає, лише для знакового біта. Ось чому ваша програма може дати дивні результати на деяких компіляторах.

Гарна практика:

  • Ніколи не використовуйте бітові поля для будь-яких цілей.
  • Уникайте використання підписаного intтипу для будь-якої форми маніпулювання бітами.

5
На роботі ми маємо static_asserts щодо розміру та адреси бітових полів, щоб переконатися, що вони не заповнені. Ми використовуємо бітові поля для апаратних реєстрів у нашому мікропрограмі.
Майкл

4
@Lundin: Потворна річ з # define-d масками та зміщеннями полягає в тому, що ваш код засмічується зрушеннями та побічними операторами І / АБО. За допомогою бітових полів компілятор подбає про це.
Майкл

4
@Michael З бітовими полями компілятор подбає про це за вас. Ну, це нормально, якщо ваші стандарти щодо "подбає про це" є "не переносяться" і "непередбачуваними". Мої вищі за це.
Ендрю Генле,

3
@AndrewHenle Leushenko каже, що з точки зору лише самого стандарту С , від реалізації залежить, чи вирішить він дотримуватися ABI x86-64 чи ні.
mtraceur

3
@AndrewHenle Правильно, я погоджуюсь в обох пунктах. Моя думка полягала в тому, що, на мою думку, ваша незгода з Леушенком зводиться до того, що ви використовуєте "імплементацію визначено", щоб посилатися лише на речі, не чітко визначені стандартом С і не чітко визначені платформою ABI, і він використовує це для посилання до будь-чого, що не є строго визначеним лише стандартом C.
mtraceur

58

Я не можу зрозуміти, як можливо, що ми щось встановлюємо, а потім воно не відображається як є.

Ви запитуєте, чому компіляція проти видає помилку?

Так, в ідеалі це повинно привести до помилки. І це робиться, якщо ви використовуєте попередження компілятора. У країнах 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;
           ^

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

Щоб підкинути деякий рецептивізм, я повторю вислів @ Лундіна: "Ніколи не використовуйте бітові поля для будь-яких цілей". Якщо у вас є вагомі причини, щоб отримати низький рівень і конкретні деталі розмітки вашої пам'яті, які б змусили вас думати, що вам потрібні бітові поля, в першу чергу, інші пов'язані з вами вимоги, які ви майже напевно маєте, зіткнуться з їх недостатньою специфікацією.

(TL; DR - Якщо ви достатньо витончені, щоб законно "потребувати" бітові поля, вони недостатньо чітко визначені, щоб обслуговувати вас.)


15
Автори стандарту були у святкові дні в день, коли було розроблено розділ бітового поля. Тож двірник повинен був це зробити. Немає обгрунтування нічого стосовно того, як розроблені бітові поля.
Лундін,

9
Не існує цілісного технічного обґрунтування. Але це приводить мене до висновку, що існувала політична аргументація: уникати того, щоб будь-який із існуючих кодексів чи реалізацій був неправильним. Але результат полягає в тому, що про розрядні поля дуже мало можна покластися.
Джон Боллінгер,

6
@JohnBollinger Безумовно, існувала політика, яка завдала значної шкоди C90. Одного разу я поспілкувався з одним із членів комітету, який пояснив джерело безлічі глупот - не можна дозволити стандарту ISO сприяти певним існуючим технологіям. Ось чому ми застрягли у дебільних речах, таких як підтримка доповнення 1 та величини підпису, визначеність реалізації підписання char, підтримка байтів, які не становлять 8 біт тощо, тощо. Їм не було дозволено давати дебільним комп’ютерам збиток на ринку.
Лундін,

1
@Lundin Було б цікаво побачити колекцію виписок і посмертних виписок від людей, які вважали, що компроміси були зроблені помилково, і чому. Цікаво, скільки вивчення цих "ми робили це минулого разу, і це вийшло / не вийшло" стало інституційним знанням для інформування про наступний такий випадок, а не просто історії в головах людей.
HostileFork каже, що не довіряє SE

1
Це все ще вказано як пункт №. 1 оригінальних принципів C в Хартії C2x: "Існуючий код важливий, а існуючі реалізації - ні." ... "жодна реалізація не затримувалася як приклад, за допомогою якого можна було б визначити C: Передбачається, що всі існуючі реалізації повинні дещо змінитися, щоб відповідати Стандарту."
Леушенко

23

Це поведінка, визначена реалізацією. Я роблю припущення, що машини, на яких ви використовуєте це, використовують цілі числа із двома компліментами із підписом і розглядають intу цьому випадку ціле число зі знаком, щоб пояснити, чому ви не вводите, якщо істинна частина оператора if.

struct mystruct { int enabled:1; };

оголошує enableяк 1-бітове бітове поле. Оскільки він підписаний, дійсними значеннями є -1і 0. Встановлення поля для 1переповнення того біта, який повертається -1(це невизначена поведінка)

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


msgstr "на підпис підписано, допустимими значеннями є -1 та 0". Хто сказав, що він підписаний? Це не визначена, а реалізована поведінка. Якщо він підписаний, то дійсними значеннями є -і +. Доповнення 2 не має значення.
Лундін,

5
@Lundin 1-бітове двозначне число-комплімент має лише два можливих значення. Якщо біт встановлений, то оскільки це знаковий біт, він дорівнює -1. Якщо він не встановлений, це "позитивний" 0. Я знаю, що це визначено реалізацією, я просто
пояснюю

1
Ключ тут швидше в тому, що доповнення 2 або будь-яка інша підписана форма не може функціонувати з одним доступним бітом.
Лундін,

1
@JohnBollinger Я це розумію. Ось чому я розумію, що це імплементація. Принаймні для великих 3 вони всі розглядають 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 Якщо значення true або false зберігається у бітовому полі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" лише в одному бітовому полі.


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");
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.