Доступ к неактивному члену объединения и неопределенному поведению?
У меня сложилось впечатление, что доступ к union
членом, отличным от последнего набора, является UB, но я не могу найти надежную ссылку (кроме ответов, утверждающих, что это UB, но без какой-либо поддержки со стороны стандарта).
Итак, это неопределенное поведение?
5 ответов
Путаница заключается в том, что C явно разрешает пробивание типов через объединение, тогда как C++ ( C++ 11) не имеет такого разрешения.
6.5.2.3 Структура и члены профсоюза
95) Если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения повторно интерпретируется как представление объекта в новом type, как описано в 6.2.6 (процесс иногда называется 'type punning' '). Это может быть представление ловушки.
Ситуация с C++:
9.5 Союзы [class.union]
В объединении самое большее один из элементов не статических данных может быть активным в любое время, то есть значение самое большее одного из участников не статических данных может быть сохранено в объединении в любое время.
C++ позже имеет язык, разрешающий использование союзов, содержащих struct
s с общими начальными последовательностями; это, однако, не позволяет наказание за тип.
Чтобы определить, разрешено ли объединение типов в C++, мы должны искать дальше. Напомним, что c99 является нормативным справочником для C++11 (и C99 имеет язык, аналогичный C11, разрешающему объединение типов-наказаний):
3.9 Типы [базовые.типы]
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-значение преобразования:
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 для неактивного члена профсоюза (поэтому назначение неактивного члена без конструкции нормально), оно считается неинициализированным.
Стандарт C++11 говорит об этом так
9.5 Союзы
В объединении самое большее один из элементов не статических данных может быть активным в любое время, то есть значение самое большее одного из участников не статических данных может быть сохранено в объединении в любое время.
Если хранится только одно значение, как вы можете прочитать другое? Это просто не там.
В документации gcc это указано в разделе " Реализация, определяемая поведением"
- Доступ к члену объекта объединения осуществляется с использованием члена другого типа (C90 6.3.2.3).
Соответствующие байты представления объекта обрабатываются как объект типа, используемого для доступа. Смотри Type-punning. Это может быть представление ловушки.
указывая, что это не требуется стандартом C.
2016-01-05: Благодаря комментариям я был связан с Отчетом о дефектах C99 № 283, который добавляет аналогичный текст в качестве сноски к стандартному документу C:
78a) Если элемент, используемый для доступа к содержимому объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения повторно интерпретируется как представление объекта в новом type, как описано в 6.2.6 (этот процесс иногда называют "наказанием типа"). Это может быть представление ловушки.
Не уверен, что это многое прояснит, учитывая, что сноска не является нормативной для стандарта.
Я думаю, что наиболее близким стандартом является утверждение о том, что его неопределенное поведение определяет поведение объединения, содержащего общую начальную последовательность (C99, §6.5.2.3/5):
Одна специальная гарантия сделана для того, чтобы упростить использование объединений: если объединение содержит несколько структур, которые имеют общую начальную последовательность (см. Ниже), и если объект объединения в настоящее время содержит одну из этих структур, разрешается проверять общие Начальная часть любого из них везде, где видна декларация полного типа объединения. Две структуры имеют общую начальную последовательность, если соответствующие элементы имеют совместимые типы (и, для битовых полей, одинаковой ширины) для последовательности из одного или нескольких начальных элементов.
C++ 11 дает аналогичные требования / разрешение в §9.2/19:
Если объединение стандартной компоновки содержит две или более структур стандартной компоновки, которые совместно используют общую начальную последовательность, и если объект объединения стандартной компоновки в настоящее время содержит одну из этих структур стандартной компоновки, разрешается проверять общую начальную часть любой из них. Две структуры стандартного макета разделяют общую начальную последовательность, если соответствующие элементы имеют типы, совместимые с макетом, и ни один из них не является битовым полем, либо оба являются битовыми полями одинаковой ширины для последовательности из одного или нескольких начальных элементов.
Хотя ни один из них не заявляет об этом напрямую, они оба имеют сильное следствие того, что "осмотр" (чтение) члена "разрешен" только в том случае, если 1) он является (частью) членом, который был недавно написан, или 2) является частью общего инициала. последовательность.
Это не прямое утверждение, что в противном случае это неопределенное поведение, но это самое близкое из того, что я знаю.
Кое-что, что еще не упомянуто доступными ответами, является сноской 37 в параграфе 21 раздела 6.2.5:
Обратите внимание, что агрегатный тип не включает тип объединения, поскольку объект с типом объединения может содержать только один член за раз.
Это требование явно подразумевает, что вы не должны писать в члене, а читать в другом. В этом случае это может быть неопределенное поведение из-за отсутствия спецификации.
Я хорошо объясняю это на примере.
Предположим, у нас есть следующий союз:
union A{
int x;
short y[2];
};
Я хорошо предполагаю, что sizeof(int)
дает 4, и это sizeof(short)
дает 2.
когда ты пишешь union A a = {10}
чтобы создать новую переменную типа A, введите в нее значение 10.
ваша память должна выглядеть так: (помните, что все члены профсоюза находятся в одном месте)
| х | | у [0] | у [1] | ----------------------------------------- a-> |0000 0000|0000 0000 | 0000 0000 | 0000 1010 | -----------------------------------------
как вы могли видеть, значение ax равно 10, значение ay 1 равно 10, а значение ay [0] равно 0.
что будет, если я сделаю это?
a.y[0] = 37;
наша память будет выглядеть так:
| х | | у [0] | у [1] | ----------------------------------------- a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 | -----------------------------------------
это превратит значение топора в 2424842 (в десятичном виде).
теперь, если у вашего объединения есть число с плавающей точкой или двойное число, ваша карта памяти будет более беспорядочной из-за способа хранения точных чисел. больше информации вы можете получить здесь.