Подтвердить что ты не робот

Союзы и тибетские

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

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

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

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

Подводя итог:

  • Почему плохо использовать союзы для типа punning?
  • Что это за точка, если это не так?

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

4b9b3361

Ответ 1

Для повторной итерации тип-пунтинг через объединения отлично работает в 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 нет понятия активного члена, и он отлично подходит для доступа к одному сохраненному значению посредством выражения несовместимого типа.

См. также приложение C11 J.1 §1:

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

В C99 это используется для чтения

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

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


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

Ответ 2

Целью Unions было сохранение пространства, когда вы хотите иметь возможность представлять разные типы, что мы называем вариантным типом см. Boost.Variant в качестве хорошего примера.

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

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

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

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

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

в C11, который будет примечанием 95.

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

Это, на мой взгляд, неопределенное семантическое болото в 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, это создает идентичный код, и аргумент делается, если ваш компилятор не генерирует идентичный код, он должен быть считается ошибкой:

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

Сообщение в блоге Type Punning, Strict Aliasing и Optimization также приходит к аналогичному выводу.

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

Ответ 3

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

Из стандарта: 6.5.2.3 Элементы структуры и объединения

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

Ответ 4

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

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

1. ТИП ПОВЕРХНОСТИ И СТАНДАРТЫ

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

  • В разделе 6.5 стандартных документов C99 и C11 разработана тема выражений.
  • Подсекция 6.5.2 относится к постфиксным выражениям.
  • Подподдержка 6.5.2.3 рассказывает о структурах и объединениях.
  • В параграфе 6.5.2.3 (3) объясняется оператор точка, примененный к объекту struct или union, и какое значение будет получено.
     Там появляется сноска 95. В этой сноске говорится:

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

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

  • Обозначается подраздел 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. ПРЕДСТАВЛЕНИЕ ТИПА PUNNING vs TYPE

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

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

4. БЕЗОПАСНЫЙ СЛУЧАЙ: unsigned char

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

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

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

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

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

Ответ 5

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

Проблемы типа punning сложны. Комитет С (назад в конце 1980-х годов) более или менее занял позицию, что вы должен использовать casts (в С++, reinterpret_cast) для этого, а не хотя оба метода были широко распространены в то время. С тех пор некоторые компиляторы (g++, например) взяли противоположная точка зрения, поддерживающая использование профсоюзов, но не использование отливок. И на практике не работает, если это не сразу же очевидно, что существует тип-punning. Это может быть мотивация 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. (Я считаю, что никогда не было это будет гарантировано. Но нынешняя формулировка стандарта это гарантирует.)

EDIT:

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

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

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

Что касается цитаты из 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, хотя Node был инициализируется через inner. (Тот факт, что начинается §6.5.2.3/6 с "Одна специальная гарантия сделана..." и далее явно разрешить это очень сильное указание на то, что все остальные случаи должны быть undefined. И, конечно же, там это утверждение о том, что поведение Undefined указано иначе в этом международном стандарте словами "undefined поведения или путем упускания какого-либо явного определения поведение "в § 4/2, чтобы утверждать, что поведение не undefined, вы должны показать, где он определен в стандарте.)

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

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

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