Доступ до неактивного члена профспілки та невизначена поведінка?


129

У мене склалося враження, що доступ до іншого unionчлена, окрім останнього набору, є UB, але я не можу знайти чітку довідку (крім відповідей, що стверджують, що це UB, але без підтримки стандарту).

Отже, чи не визначена поведінка?


3
C99 (а також я вважаю, що і C ++ 11) явно дозволяють накладати типи на союзи. Тож я думаю, що це підпадає під поведінку "визначено".
Таємничий

1
Я декілька разів використовував його для перетворення з окремих int в char. Отже, я точно знаю, що це не визначено. Я використовував це на компіляторі Sun CC. Отже, це все ще може залежати від компілятора.
go4sri

42
@ go4sri: Зрозуміло, ти не знаєш, що означає поведінка не визначено. Те, що, здається, працювало для вас у певному випадку, не суперечить його невизначеності.
Бенджамін Ліндлі


4
@Mysticial, публікація блогу, на яку ви посилаєтесь, дуже конкретно стосується C99; це питання позначено лише для C ++.
davmac

Відповіді:


131

Плутанина полягає в тому, що C явно дозволяє натискати тип через союз, тоді як C ++ () не має такого дозволу.

6.5.2.3 Структура та члени профспілки

95) Якщо член, який використовується для читання вмісту об'єкта об'єднання, не є тим самим, що член, який останній використовується для зберігання значення в об'єкті, відповідна частина представлення об'єкта значення переосмислюється як представлення об'єкта в новому тип, як описано в 6.2.6 (процес, який іноді називають '' типом покарання ''). Це може бути уявлення про пастку.

Ситуація з C ++:

9.5 Союзи [class.union]

У об'єднанні, щонайменше, один із нестатичних членів даних може бути активним у будь-який час, тобто значення, щонайменше, одного з нестатичних членів даних можуть бути збережені в об'єднанні в будь-який час.

Пізніше C ++ має мову, що дозволяє використовувати об'єднання, що містять structs із загальними початковими послідовностями; однак це не дозволяє накладати тип покарання.

Для того, щоб визначити , є чи об'єднання типу каламбурною це дозволено в C ++, ми повинні шукати далі. Нагадаємо, що є нормативним посиланням для C ++ 11 (і C99 має схожу мову з C11, що дозволяє накладати тип профспілки):

3.9 Типи [basic.types]

4 - Об'єктне представлення об'єкта типу T - це послідовність N непідписаних символів char, взятих об'єктом типу T, де N дорівнює sizeof (T). Представлення значення об'єкта - це набір бітів, що містять значення типу T. Для тривіально копіюваних типів представлення значення - це набір бітів у поданні об'єкта, що визначає значення, яке є одним дискретним елементом реалізації- визначений набір значень. 42
42) Ціль полягає в тому, щоб модель пам'яті C ++ була сумісною з мовою програмування ISO / IEC 9899 C.

Це стає особливо цікавим, коли ми читаємо

3.8 Тривалість життя об'єкта [basic.life]

Термін експлуатації об’єкта типу T починається тоді, коли: - отримано сховище з належним вирівнюванням і розміром для типу T, і - якщо об’єкт має нетривіальну ініціалізацію, його ініціалізація завершена.

Отже, для примітивного типу (який ipso facto має тривіальну ініціалізацію), що міститься в об'єднанні, термін експлуатації об'єкта включає принаймні термін експлуатації самого об'єднання. Це дозволяє нам посилатися

3.9.2 Типи сполук [basic.compound]

Якщо об’єкт типу T розташований за адресою A, вказівник типу cv T *, значення якого є адресою A, вказує на цей об’єкт, незалежно від того, як отримано значення.

Якщо припустити, що операція, яка нас цікавить, - це тип-покарання, тобто взяття значення неактивного члена профспілки, і з огляду на вищевикладене, що у нас є дійсне посилання на об'єкт, на який посилається цей член, ця операція має значення " -перерахунок величини:

4.1 Перетворення значення на реальне значення [conv.lval]

Glvalue нефункціонального типу без масиву Tможе бути перетворений у prvalue. Якщо Tце неповний тип, програма, яка потребує цього перетворення, неправильно сформована. Якщо об’єкт, на який посилається glvalue, не є об'єктом типу Tі не є об'єктом типу, похідним від нього T, або якщо об'єкт неініціалізований, програма, яка потребує цього перетворення, не визначає поведінку.

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

  • об'єднання копіюється в charсховище масивів і назад (3.9: 2), або
  • союз bytewise копіюється в інший союз того ж типу (3.9: 3), або
  • доступ до об'єднання здійснюється через мовні межі програмним елементом, відповідним ISO / IEC 9899 (наскільки це визначено) (примітка 42 (3,9: 4)),

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

Тобто, хоча ми можемо законно сформувати значення для неактивного члена профспілки (саме тому привласнення до неактивного члена без будівництва нормально), це вважається неініціалізованим.


5
3.8 / 1 говорить, що термін експлуатації об’єкта закінчується, коли його зберігання повторно використовується. Це вказує на мене, що життя неактивного члена профспілки закінчилося, оскільки його зберігання було повторно використане для активного члена. Це означає, що ви обмежені у використанні члена (3.8 / 6).
bames53

2
Згідно з цією інтерпретацією, то кожен біт пам'яті одночасно містить об'єкти всіх типів, які тривіально ініціалізуються та мають відповідне вирівнювання ... Отже, термін життя будь-якого нетривіально ініціалізаційного типу негайно закінчується, оскільки його зберігання повторно використовується для всіх цих типів ( а не перезапускати, оскільки вони не тривіально ініціалізуються)?
bames53

3
Формулювання 4.1 повністю і повністю порушено і з тих пір було переписано. Це забороняє всілякі цілком дійсні речі: забороняє користувацькі memcpyреалізації (доступ до об'єктів за допомогою unsigned charlvalues), забороняє доступ до *pafter int *p = 0; const int *const *pp = &p;(навіть незважаючи на те, що неявна конверсія з int**в, const int*const*є дійсною), забороняє навіть доступ cпісля struct S s; const S &c = s;. Випуск CWG 616 . Чи дозволяє це нове формулювання? Там також [basic.lval].

2
@Omnifarious: Це було б сенсом, хоча це також повинно було б уточнити (і стандарт C також повинен уточнити, btw), що &означає одиночний оператор, звернувшись до члена профспілки. Я думаю, що отриманий вказівник повинен бути придатним для доступу до члена принаймні до наступного прямого або непрямого використання будь-якого іншого члена lvalue, але в gcc вказівник не є корисним навіть настільки довго, що викликає питання про те, що передбачається, &оператор.
суперкарт

4
Одне запитання щодо "Нагадаємо, що c99 є нормативним посиланням для C ++ 11" Чи це не лише актуально, коли стандарт c ++ прямо посилається на стандарт C (наприклад, для функцій c бібліотеки)?
MikeMB

28

Стандарт C ++ 11 говорить про це так

9.5 Союзи

У об'єднанні, щонайменше, один із нестатичних членів даних може бути активним у будь-який час, тобто значення, щонайменше, одного з нестатичних членів даних можуть бути збережені в об'єднанні в будь-який час.

Якщо зберігається лише одне значення, як можна прочитати інше? Просто його немає.


Документація gcc перераховує це в розділі, визначеному реалізацією

  • Доступ до об'єкта об'єднання доступний за допомогою члена іншого типу (C90 6.3.2.3).

Відповідні байти представлення об'єкта трактуються як об'єкт типу, що використовується для доступу. Див. Розділ Типовий набір Це може бути уявлення про пастку.

що вказує на те, що цього не вимагає стандарт C.


2016-01-05: Через коментарі я був пов'язаний зі звітом про дефект C99 № 283, який додає аналогічний текст, як виноска до стандартного документа C:

78a) Якщо член, який використовується для доступу до вмісту об'єкта об'єднання, не є тим самим, як член, який останній використовується для зберігання значення в об'єкті, відповідна частина представлення об'єкта значення переінтерпретується як представлення об'єкта в новому тип, як описано в 6.2.6 (процес, який іноді називають "типом накачування"). Це може бути уявлення про пастку.

Не впевнений, чи багато це уточнює, враховуючи, що виноска не є нормативною для стандарту.


10
@LuchianGrigore: UB - це не те, що стандарт каже, що це UB, а це те, що стандарт не описує, як він повинен працювати. Це саме такий випадок. Чи описує стандарт, що відбувається? Це говорить про те, що це визначено реалізацією? Ні і ні. Так що це UB. Більше того, стосовно аргументу "члени поділяють одну і ту ж адресу пам'яті", вам доведеться посилатися на правила додання, які знову приведуть вас до UB.
Яків Галка

5
@Luchian: Цілком зрозуміло, що означає активний, "тобто значення принаймні одного з нестатичних членів даних можна зберігати в об'єднанні в будь-який час".
Бенджамін Ліндлі

5
@LuchianGrigore: Так, є. Існує нескінченна кількість випадків, коли стандарт не звертається (і не може). (C ++ - це повноцінна VM, яка є Тьюрінгом, тому вона неповна.) Що робити? Це пояснює, що означає "активний", посилаючись на цитату вище, після "це є".
Яків Галка

8
@LuchianGrigore: Опущення явного визначення поведінки також є нерозглянутим невизначеним поведінкою, відповідно до розділу визначення.
jxh

5
@Claudiu Це UB з іншої причини - це порушує суворий псевдонім.
Містичний

18

Я думаю, що найближчим до стандарту доводиться те, що це невизначена поведінка - це те, де воно визначає поведінку для об'єднання, що містить загальну початкову послідовність (C99, §6.5.2.3 / 5):

Одна спеціальна гарантія робиться з метою спрощення використання об'єднань: якщо об'єднання містить кілька структур, які мають спільну початкову послідовність (див. Нижче), і якщо об'єкт об'єднання в даний час містить одну з цих структур, дозволено перевіряти загальну початкова частина будь-якого з них де завгодно, що видно декларацію про повний тип союзу. Дві структури мають спільну початкову послідовність, якщо відповідні члени мають сумісні типи (і для бітових полів однакові ширини) для послідовності одного або більше початкових членів.

C ++ 11 дає подібні вимоги / дозвіл у § 9.2 / 19:

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

Хоча обидва не заявляють це прямо, вони обидва несуть серйозне значення, що "перевірка" (читання) члена "дозволена" лише якщо 1) це (частина) останнього написаного члена, або 2) є частиною загальної початкової послідовність.

Це не пряме твердження, що діяти в іншому випадку є невизначеною поведінкою, але це найближче, про що я знаю.


Щоб зробити це повноцінним, вам потрібно знати, що таке "сумісні з компонуванням типи" для C ++, або "сумісні типи" для C.
Майкл Андерсон

2
@MichaelAnderson: Так і ні. Вам потрібно мати справу з тими, коли / якщо ви хочете бути впевнені, чи підпадає щось під цей виняток - але справжнє питання полягає в тому, чи справді щось, що явно виходить за межі винятку, дає УБ? Я думаю, що це досить чітко мається на увазі тут, щоб зрозуміти наміри, але я не думаю, що це ніколи прямо не було заявлено.
Джеррі Труну

Ця річ "загальної початкової послідовності", можливо, врятувала 2 або 3 моїх проекти з кошика для перезаписів. Коли я вперше прочитав про більшість застосувань для unionпокарання, коли я не визначив, я почув себе невідомим, оскільки в конкретному блозі було враження, що це все в порядку, і створив навколо себе кілька великих структур та проектів. Тепер я думаю, що я можу бути все в порядку, оскільки мої unions містять класи, що мають однакові типи в передній частині
underscore_d

@ JerryCoffin, я думаю, ви натякали на те саме питання, що і я: що, якщо наш unionмістить, наприклад, a uint8_tі a class Something { uint8_t myByte; [...] };- я б припустив, що це застереження також буде застосовано тут, але це сформульовано дуже навмисно, щоб допустити лише structs. На щастя, я вже використовую такі замість сирих примітивів: O
підкреслюй

@underscore_d: Стандарт C принаймні на зразок охоплює це питання: "Вказівник на об'єкт структури, відповідним чином перетворений, вказує на його початковий член (або якщо цей член - бітове поле, то на одиницю, в якій він знаходиться) , і навпаки."
Джеррі Труну

12

Щось ще не згадується в доступних відповідях - це виноска 37 в пункті 21 розділу 6.2.5:

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

Ця вимога, очевидно, означає, що ви не повинні писати в члені та читати в іншому. У цьому випадку це може бути невизначена поведінка через відсутність конкретизації.


Багато реалізацій документують свої формати зберігання та правила компонування. Така специфікація в багатьох випадках має на увазі, який ефект матиме читання пам’яті одного типу та написання іншого, як відсутність правил, які б говорили, що компілятори не повинні використовувати їх визначений формат зберігання, за винятком випадків, коли речі читаються та записуються за допомогою покажчиків символьного типу.
supercat

-3

Я добре пояснюю це на прикладі.
припустимо, у нас є такий союз:

union A{
   int x;
   short y[2];
};

Я добре припускаю, що sizeof(int)дає 4, а це sizeof(short)дає 2.,
коли ви пишете, union A a = {10}що добре створити новий var типу A, вклавши в нього значення 10.

Ваша пам'ять має виглядати так: (пам’ятайте, що всі члени профспілки мають одне і те саме місце)

       | х |
       | y [0] | у [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

як ви могли бачити, значення ax 10 - значення ay 1 - 10, а ay [0] - 0.

Тепер, що добре, якщо я це роблю?

a.y[0] = 37;

наша пам'ять буде виглядати так:

       | х |
       | y [0] | у [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

це перетворить значення сокири на 2424842 (у десятковій кількості).

Тепер, якщо у вашому союзі є плаваюча або подвійна, ваша карта пам’яті буде більше безладдя через те, як ви зберігаєте точні цифри. Більше інформації ви можете отримати тут .


18
:) Це не те, про що я питав. Я знаю, що відбувається всередині. Я знаю, що це працює. Я запитав, чи це в стандарті.
Лучіан Григоре
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.