Доступ к неактивному члену союза и неопределенному поведению?

129

У меня создалось впечатление, что доступ к unionчлену, отличному от последнего набора, - это UB, но я не могу найти надежную ссылку (кроме ответов, в которых утверждается, что это UB, но без какой-либо поддержки со стороны стандарта).

Итак, это неопределенное поведение?

Лучиан Григоре
источник
3
C99 (и я верю, что и C ++ 11) явно разрешает каламбур типов с объединениями. Поэтому я думаю, что это относится к поведению, определяемому реализацией.
Mysticial 07
1
Я использовал его несколько раз для преобразования отдельных int в char. Итак, я точно знаю, что это не неопределенное. Я использовал его на компиляторе Sun CC. Таким образом, он все еще может зависеть от компилятора.
go4sri 07
42
@ go4sri: Очевидно, вы не знаете, что значит неопределенное поведение. Тот факт, что в некоторых случаях это работало для вас, не противоречит его неопределенности.
Бенджамин Линдли
4
@Mysticial, сообщение в блоге, на которое вы ссылаетесь, очень конкретно касается C99; этот вопрос помечен только для C ++.
davmac 02

Ответы:

131

Путаница в том, что C явно разрешает выбор типов через объединение, тогда как C ++ () не имеет такого разрешения.

6.5.2.3 Члены структуры и объединения

95) Если член, используемый для чтения содержимого объекта объединения, не совпадает с членом, последним использовавшимся для хранения значения в объекте, соответствующая часть объектного представления значения переинтерпретируется как представление объекта в новом type, как описано в 6.2.6 (процесс, иногда называемый «type punning»). Это может быть ловушка.

Ситуация с 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, указывает на этот объект, независимо от того, как было получено значение.

Предполагая, что интересующая нас операция - это определение типа, то есть получение значения неактивного члена объединения, и учитывая вышеизложенное, что у нас есть действительная ссылка на объект, на который ссылается этот член, эта операция будет lvalue-to -rvalue преобразование:

4.1 Преобразование Lvalue в rvalue [conv.lval]

Значение glvalue нефункционального типа, не являющегося массивом, Tможет быть преобразовано в prvalue. Если Tэто неполный тип, программа, которая требует этого преобразования, плохо сформирована. Если объект, на который ссылается glvalue, не является объектом типа Tи не является объектом производного типа T, или если объект не инициализирован, программа, которая требует этого преобразования, имеет неопределенное поведение.

Тогда возникает вопрос, инициализируется ли объект, который является неактивным членом объединения, хранилищем для активного члена объединения. Насколько я могу судить, это не так, хотя, если:

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

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

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

ecatmur
источник
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 также должен уточнить, кстати), что &означает унарный оператор при применении к члену объединения. Я бы подумал, что полученный указатель должен использоваться для доступа к члену, по крайней мере, до следующего прямого или косвенного использования любого другого члена lvalue, но в gcc указатель нельзя использовать даже так долго, что вызывает вопрос о том, что &оператор должен иметь в виду.
supercat
4
Один вопрос относительно «Напомним, что c99 является нормативным справочником для C ++ 11». Разве это не актуально только тогда, когда стандарт C ++ явно ссылается на стандарт C (например, для функций библиотеки c)?
MikeMB
28

Стандарт C ++ 11 говорит об этом так

9.5 Союзы

В объединении максимум один из нестатических элементов данных может быть активным в любое время, то есть значение максимум одного из нестатических элементов данных может быть сохранено в объединении в любое время.

Если сохраняется только одно значение, как можно прочитать другое? Его просто нет.


В документации gcc это указано в разделе « Поведение, определяемое реализацией».

  • Доступ к члену объекта union осуществляется с помощью члена другого типа (C90 6.3.2.3).

Соответствующие байты представления объекта рассматриваются как объект того типа, который используется для доступа. См. Тип каламбура. Это может быть ловушка.

указывая, что это не требуется стандартом C.


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

78a) Если член, используемый для доступа к содержимому объекта объединения, не совпадает с членом, последним использовавшимся для хранения значения в объекте, соответствующая часть объектного представления значения переинтерпретируется как представление объекта в новом введите, как описано в п. 6.2.6 (процесс, который иногда называют «набирать текст»). Это может быть ловушка.

Не уверен, что он многое проясняет, учитывая, что сноска не является нормативной для стандарта.

Бо Перссон
источник
10
@LuchianGrigore: UB - это не то, что стандарт утверждает, это UB, это то, что стандарт не описывает, как он должен работать. Это как раз такой случай. Описывает ли стандарт, что происходит? Говорит ли это, что реализация определена? Нет и нет. Так что это УБ. Более того, что касается аргумента «члены используют один и тот же адрес памяти», вам придется обратиться к правилам псевдонима, которые снова вернут вас в UB.
Яков Галка
5
@Luchian: Совершенно ясно, что означает «
Бенджамин Линдли
5
@LuchianGrigore: Да, есть. Существует бесконечное количество случаев, которые стандарт не учитывает (и не может решить). (C ++ - это полная виртуальная машина по Тьюрингу, поэтому она неполная.) И что? Это объясняет, что означает «активный», см. Приведенную выше цитату после «то есть».
Яков Галка
8
@LuchianGrigore: Согласно разделу определений, пропуск явного определения поведения также считается нерассматриваемым неопределенным поведением.
jxh 07
5
@Claudiu Это UB по другой причине - он нарушает строгий псевдоним.
Mysticial 07
18

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

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

С ++ 11 дает аналогичные требования / разрешения в §9.2 / 19:

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

Хотя ни один из них не заявляет об этом прямо, оба они имеют сильное значение, что «проверка» (чтение) члена «разрешена» только в том случае, если 1) это (часть) член, который был написан последним, или 2) является частью общего начального последовательность.

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

Джерри Гроб
источник
Чтобы завершить это, вам нужно знать, что такое «макет-совместимые типы» для C ++ или «совместимые типы» для C.
Майкл Андерсон,
2
@MichaelAnderson: Да и нет. Вам нужно иметь дело с ними, когда / если вы хотите быть уверенным, попадает ли что-либо в это исключение, но реальный вопрос здесь в том, действительно ли что-то, что явно выходит за рамки исключения, дает UB. Я думаю, что это достаточно сильно подразумевается здесь, чтобы прояснить намерение, но я не думаю, что это когда-либо прямо заявлялось.
Джерри Коффин,
Эта «обычная начальная последовательность» могла просто сохранить 2 или 3 моих проекта из корзины перезаписи. Я был в ярости, когда впервые прочитал о том, что в большинстве случаев использование unions не определено, поскольку в одном конкретном блоге у меня сложилось впечатление, что это нормально, и я построил вокруг этого несколько больших структур и проектов. Теперь я думаю , что со мной все в порядке, так как мои unions действительно содержат классы с одинаковыми типами впереди
underscore_d
@JerryCoffin, я думаю, вы намекали на тот же вопрос, что и я: что, если наш unionсодержит, например, a uint8_tи a class Something { uint8_t myByte; [...] };- я предполагаю, что это условие также применимо здесь, но оно сформулировано очень намеренно, чтобы разрешить только structs. К счастью, я уже использую их вместо сырых примитивов: O
underscore_d
@underscore_d: Стандарт C, по крайней мере, как бы охватывает этот вопрос: «Указатель на объект структуры, преобразованный соответствующим образом, указывает на его начальный член (или, если этот член является битовым полем, то на модуль, в котором он находится) , и наоборот."
Джерри Гроб,
12

То, что еще не упоминается в доступных ответах, - это сноска 37 в параграфе 21 раздела 6.2.5:

Обратите внимание, что тип агрегата не включает тип объединения, поскольку объект с типом объединения может содержать только один член за раз.

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

MPU
источник
Многие реализации документируют свои форматы хранения и правила компоновки. Такая спецификация во многих случаях будет означать, какой эффект от чтения хранилища одного типа и записи в качестве другого будет в отсутствие правил, говорящих, что компиляторы не должны фактически использовать свой определенный формат хранения, кроме случаев, когда что-то читается и записывается с использованием указателей. символьного типа.
supercat
-3

Я хорошо объясню это на примере.
Предположим, у нас есть следующий союз:

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

Я хорошо предполагаю, что это sizeof(int)дает 4, а это sizeof(short)дает 2.
когда вы пишете union A a = {10}это хорошо, создайте новую переменную типа A и вставьте в нее значение 10.

ваша память должна выглядеть так: (помните, что все члены союза находятся в одном месте)

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

как вы могли видеть, значение ax равно 10, значение ay 1 равно 10, а значение ay [0] равно 0.

А что будет, если я сделаю это?

a.y[0] = 37;

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

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

это превратит значение ax в 2424842 (в десятичном виде).

теперь, если в вашем объединении есть float или double, ваша карта памяти будет более беспорядочной из-за того, как вы храните точные числа. больше информации вы можете получить здесь .

Эльяшив
источник
18
:) Я не об этом спрашивал. Я знаю, что происходит внутри. Я знаю, что это работает. Я спросил, в стандарте ли это.
Лучиан Григоре