Союзы и типовые удары

Я долго искал, но не могу найти четкого ответа.

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

Люди цитируют строгое правило псевдонимов, но мне кажется, будто вы говорите, что вы не можете этого сделать, потому что вы не можете этого сделать.

И какой смысл в объединении, если не вводить каламбур? Я где-то видел, что они должны использоваться для использования одной и той же ячейки памяти для разной информации в разное время, но почему бы просто не удалить информацию, прежде чем использовать ее снова?

Чтобы подвести итог:

  1. Почему это плохо использовать союзы для наказания типа?
  2. Какой смысл в них, если не в этом?

Дополнительная информация: я использую в основном C ++, но хотел бы знать об этом и C. В частности, я использую союзы для преобразования между числами с плавающей точкой и необработанным гексом для отправки по шине CAN.

47

Решение

Чтобы повторить, пробивание типов через объединения прекрасно в C (но не в C ++). Напротив, использование приведения указателей нарушает строгое псевдоним C99 и является проблематичным, поскольку у разных типов могут быть разные требования к выравниванию, и вы можете поднять SIGBUS, если вы сделаете это неправильно. С профсоюзами это никогда не проблема.

Соответствующие цитаты из стандартов C:

C89 раздел 3.3.2.3 §5:

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

C11 раздел 6.5.2.3 §3:

Выражение постфикса, сопровождаемое. оператор и идентификатор обозначают член структуры или объединенного объекта. Значение соответствует названному члену

со следующей сноской 95:

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

Это должно быть совершенно ясно.


Джеймс смущен, потому что C11 раздел 6.7.2.1 §16 читает

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

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

См. Также Приложение C11, J.1 §1:

Значения байтов, которые соответствуют членам объединения, кроме последнего, сохраненного в [не определены].

В С99 это раньше читалось

Значение члена объединения, отличного от последнего, сохраненного в [не указано]

Это было неправильно. Поскольку приложение не является нормативным, оно не оценивало свой собственный TC, и ему пришлось ждать, пока не будет исправлена ​​следующая стандартная версия.


Расширения GNU для стандарта C ++ (и для C90) явно разрешить наложение типов с помощью союзов. Другие компиляторы, которые не поддерживают расширения GNU, также могут поддерживать объединение типов, но это не является частью стандарта базового языка.

34

Другие решения

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

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

Практика чтения от члена профсоюза, отличного от того, к которому последний раз писали (так называемое «наказание по типу»), распространена. Даже с параметром -fstrict-aliasing допускается перетаскивание типов при условии, что доступ к памяти осуществляется через тип объединения. Итак, приведенный выше код работает как положено.

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

Паскаль Куок утверждал, что отчет о дефектах 283 пояснил, что это было разрешено в C. Отчет о дефекте 283 добавили следующую сноску в качестве пояснения:

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

в С11 это будет сноска 95,

Хотя в std-discussion тема почтовой группы Тип Punning через союз аргумент сделан, это не указано, что кажется разумным, так как DR 283 не добавил новую нормативную формулировку, просто сноска:

Это, на мой взгляд, недостаточно конкретный семантический болото в C.
Консенсус не был достигнут между разработчиками и C
Комитет относительно того, какие именно случаи определили поведение и какие
не[…]

В C ++ неясно, определено ли поведение или нет.

Это обсуждение также охватывает, по крайней мере, одну причину, по которой допускать наложение типов через объединение нежелательно:

[…] правила стандарта C нарушают псевдоним на основе типов
оптимизация анализа, которую выполняют текущие реализации.

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

std::int64_t n;
std::memcpy(&n, &d, sizeof d);

вместо этого:

union u1
{
std::int64_t n;
double d ;
} ;

u1 u ;
u.d = d ;

и мы можем видеть используя Godbolt это генерирует идентичный код и аргумент делается, если ваш компилятор не генерирует идентичный код, это следует считать ошибкой:

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

Сообщение в блоге Тип Punning, строгое псевдоним и оптимизация также приходит к аналогичному выводу.

Обсуждение списка рассылки неопределенного поведения: Наберите punning, чтобы избежать копирования покрывает много той же самой земли, и мы можем видеть, насколько серая территория может быть.

9

Это законно в C99:

Из стандарта:
6.5.2.3 Структура и члены профсоюза

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

5

КРАТКИЙ ОТВЕТ: Тип штамповки может быть безопасным при нескольких обстоятельствах. С другой стороны, хотя это кажется очень хорошо известной практикой, кажется, что стандарт не очень заинтересован в том, чтобы сделать его официальным.

Я буду говорить только о С (не C ++).

1. ТИП ПАННИНГА И СТАНДАРТЫ

Как люди уже указали, но, типа штамповки допускается в стандарте C99, а также C11, в подразделе 6.5.2.3. Тем не менее, я перепишу факты с моим собственным восприятием вопроса:

  • Секция 6,5 стандартных документов C99 и C11 развивают тему выражения.
  • Подраздел 6.5.2 ссылается на постфиксные выражения.
  • Подраздел 6.5.2.3 Переговоры о структуры и союзы.
  • Абзац 6.5.2.3 (3) объясняет оператор точки применяется к struct или же union объект, и какое значение будет получено.
    Просто там сноска 95 появляется. Эта сноска гласит:

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

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

  • Подраздел 6.2.6 упомянуто.
  • Подраздел 6.2.6 говорит о том, как объекты представлены (в памяти, скажем).

2. ПРЕДСТАВИТЕЛЬСТВО ВИДОВ И ЕГО ПРОБЛЕМА

Если вы обратите внимание на различные аспекты стандарта, вы можете быть почти ни в чем не уверены:

  • Представление указателей четко не указано.
  • В худшем случае указатели, имеющие разные типы, могут иметь различное представление (как объекты в памяти).
  • union члены разделяют один и тот же адрес заголовка в памяти, и это тот же адрес, что и union сам объект
  • struct члены имеют увеличивающийся относительный адрес, начиная с того же адреса памяти, что и struct сам объект Однако байты заполнения могут быть добавлены в конце каждого члена. Как много? Это непредсказуемо. Заполняющие байты используются в основном для выравнивания памяти.
  • Арифметические типы (целые числа, действительные и комплексные числа с плавающей запятой) могут быть представлены несколькими способами. Это зависит от реализации.
  • В частности, целочисленные типы могут иметь биты заполнения. Я считаю, что это не так для настольных компьютеров. Однако стандарт оставил дверь открытой для этой возможности. Биты заполнения используются для специальных целей (четность, сигналы, кто знает), а не для хранения математических значений.
  • signed Типы могут иметь 3 способа представления: 1 дополнение, 2 дополнение, просто знаковый бит.
  • char типы занимают всего 1 байт, но 1 байт может иметь число битов, отличное от 8 (но никогда не меньше 8).
  • Однако мы можем быть уверены в некоторых деталях:

    а. char типы не имеют битов заполнения.
    б. unsigned целочисленные типы представлены в точности как в двоичной форме.
    с. unsigned char занимает ровно 1 байт без битов заполнения, и никакого представления прерываний не существует, поскольку используются все биты. Кроме того, он представляет значение без какой-либо неопределенности, следуя двоичному формату для целых чисел.

3. ПУНКТ ТИПА против ПРЕДСТАВИТЕЛЬСТВА ТИПА

Все эти наблюдения показывают, что, если мы попытаемся сделать типа штамповки с union члены, имеющие разные типы unsigned charУ нас может быть много двусмысленности. Это не переносимый код, и, в частности, мы можем иметь непредсказуемое поведение нашей программы.
Тем не мение, стандарт разрешает такой доступ.

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

4. БЕЗОПАСНЫЙ СЛУЧАЙ: неподписанный символ

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

  union {
TYPE data;
unsigned char type_punning[sizeof(TYPE)];
} xx;

Поскольку мы знаем, что unsigned char представлен в строгой двоичной форме, без битов заполнения, здесь можно использовать тип punning, чтобы взглянуть на двоичное представление члена data,
Этот инструмент можно использовать для анализа представления значений данного типа в конкретной реализации.

Я не могу увидеть другое безопасное и полезное приложение типа штамповки по стандартным спецификациям.

5. КОММЕНТАРИЙ О СЛУЧАЯХ …

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

  union {
unsigned char x;
double t;
} uu;

bool result;

uu.x = 7;
(uu.t == 7.0)? result = true: result = false;
// You can bet that result == false

uu.t = (double)(uu.x);
(uu.t == 7.0)? result = true: result = false;
// result == true
3

Есть (или, по крайней мере, были еще в C90) две модификации для
делая это неопределенное поведение. Первым было то, что компилятор
будет разрешено генерировать дополнительный код, который отслеживал то, что было
в союзе, и генерирует сигнал, когда вы получили неправильный доступ
член. На практике, я не думаю, что кто-либо когда-либо делал (может быть,
CenterLine?). Другой был оптимизация возможностей этого
открыл, и они используются. Я использовал компиляторы, которые
отложит запись до последнего возможного момента, на
оснований, что это может быть не нужно (потому что переменная
выходит из области видимости, или есть последующая запись другой
значение). Логично, что можно ожидать, что эта оптимизация
будет выключен, когда союз был виден, но он не был в
самые ранние версии Microsoft C.

Проблемы типа наказания являются сложными. Комитет C (назад
в конце 1980-х) более или менее заняли ту позицию, что вы
для этого следует использовать приведение (в C ++, reinterpret_cast), а не
профсоюзы, хотя оба метода были широко распространены в то время.
С тех пор некоторые компиляторы (например, g ++) взяли
противоположная точка зрения, поддерживающая использование союзов, но не
использование слепков. И на практике ни сработает, если это не так
Сразу видно, что есть типовое наказание. Это может быть
мотивация с точки зрения g ++. Если вы получаете доступ
член профсоюза, сразу видно, что может быть
тип-каламбуров. Но, конечно, учитывая что-то вроде:

int f(const int* pi, double* pd)
{
int results = *pi;
*pd = 3.14159;
return results;
}

называется с:

union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );

совершенно законно в соответствии со строгими правилами
стандартный, но не работает с g ++ (и, возможно, многие другие
Составители); при компиляции fКомпилятор предполагает, что pi
а также pd не может псевдоним, и переупорядочивает запись в *pd и
читать из *pi, (Я считаю, что это никогда не было намерение, что
это будет гарантировано. Но нынешняя редакция стандарта
действительно гарантирует это.)

РЕДАКТИРОВАТЬ:

Поскольку другие ответы утверждают, что поведение на самом деле
определено (в основном основано на цитировании ненормативной ноты, принято
из контекста):

Правильный ответ здесь — pablo1977: стандарт делает
нет попыток определить поведение, когда используется типовое наказание.
Вероятная причина этого заключается в том, что нет портативного
поведение, которое он мог определить. Это не мешает конкретному
реализация от определения его; хотя я не помню
конкретные обсуждения вопроса, я уверен, что
Намерение заключалось в том, что реализации определяют что-то (и большинство, если
не все, делай).

Что касается использования союза для типового наказания: когда
Комитет C разрабатывал C90 (в конце 1980-х), было
четкое намерение разрешить реализации отладки, которые сделали
дополнительная проверка (например, использование жирных указателей для границ
проверка). Из обсуждений в то время было ясно, что
предполагалось, что реализация отладки может кешировать
информация относительно последнего значения, инициализированного в объединении,
и заманивать в ловушку, если вы пытались получить доступ к чему-либо еще. Это явно
в §6.7.2.1 / 16 указано: «Значение не более одного члена
может быть сохранен в объекте объединения в любое время. «Доступ к значению
не существует неопределенного поведения; это может быть ассимилировано с
доступ к неинициализированной переменной. (Были некоторые
обсуждения в то время относительно того, имеет ли доступ к другому
член с таким же типом был законным или нет. Я не знаю что
окончательное решение было, однако; примерно после 1990 года я перешел
в C ++.)

Что касается цитаты из C89, говорят, что поведение
определяется реализацией: найти его в разделе 3 (Условия,
Определения и символы) кажется очень странным. Мне придется посмотреть
это в моем экземпляре C90 дома; тот факт, что это было
удалено в более поздних версиях стандартов предполагает, что его
присутствие было сочтено ошибкой комитета.

Использование союзов, которые поддерживает стандарт, является средством
симулировать вывод. Вы можете определить:

struct NodeBase
{
enum NodeType type;
};

struct InnerNode
{
enum NodeType type;
NodeBase* left;
NodeBase* right;
};

struct ConstantNode
{
enum NodeType type;
double value;
};
//  ...

union Node
{
struct NodeBase base;
struct InnerNode inner;
struct ConstantNode constant;
//  ...
};

и легально доступ к base.type, хотя узел был
инициализируется через inner, (Тот факт, что §6.5.2.3 / 6 начинается
с «Одна специальная гарантия сделана …» и продолжается
явно разрешить это является очень убедительным признаком того, что все остальные
случаи должны быть неопределенным поведением. И конечно, там
это утверждение, что «неопределенное поведение указано иначе
в этом международном стандарте словами ‘‘ undefined
поведение «или опущением любого явного определения
поведение
«в § 4/2; для того, чтобы утверждать, что поведение не
undefined, вы должны показать, где это определено в стандарте.)

Наконец, что касается типа наказания: все (или, по крайней мере, все, что
Я использовал) реализации поддерживают в некотором роде. мой
в то время сложилось впечатление, что
приведение в соответствие с реализацией; в С ++
стандарт, есть даже (ненормативный) текст, чтобы предположить, что
результаты reinterpret_cast быть «неудивительным» для кого-то
знаком с базовой архитектурой. На практике,
тем не менее, большинство реализаций поддерживают использование объединения для
Тип-наказание, если доступ осуществляется через член профсоюза.
Большинство реализаций (но не g ++) также поддерживают приведение указателей,
при условии, что указатель приведен четко виден компилятору
(для некоторого неуказанного определения приведения указателя). И
«стандартизация» базового оборудования означает, что вещи
лайк:

int
getExponent( double d )
{
return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}

на самом деле довольно портативны. (Это не будет работать на мэйнфреймах,
Конечно.) То, что не работает, такие вещи, как мой первый пример,
где псевдоним невидим для компилятора. (Я красивая
уверен, что это дефект в стандарте. Кажется, я помню
даже увидев DR по этому поводу.)

2
По вопросам рекламы [email protected]