Назначение союзов в C и C++


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

union ARGB
{
    uint32_t colour;

    struct componentsTag
    {
        uint8_t b;
        uint8_t g;
        uint8_t r;
        uint8_t a;
    } components;

} pixel;

pixel.colour = 0xff040201;  // ARGB::colour is the active member from now on

// somewhere down the line, without any edit to pixel

if(pixel.components.a)      // accessing the non-active member ARGB::components

на самом деле неопределенное поведение, т. е. чтение из члена Союза, отличного от недавно написанного, приводит к неопределенному поведению. Если это не предполагаемое использование союзов,то что? Может кто-нибудь объяснить это подробно?

обновление:

Я хотел бы прояснить несколько вещей в ретроспективе.

  • ответ на вопрос не то же самое для C и c++; мой невежественный младший я пометил его как C и c++.
  • после просмотра стандарта C++11 я не мог окончательно сказать, что он вызывает доступ/проверку неактивного члена Союза, неопределенного/неопределенного/определенного реализацией. Все, что я смог найти, было §9.5 / 1:

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

  • в то время как в C, (C99 TC3-DR 283 далее) это законно (спасибо Паскаль Cuoq для поднятия этого). Однако, пытаясь сделать он все еще может привести к неопределенному поведению, если считанное значение оказывается недопустимым (так называемое "представление ловушки") для типа, через который оно считывается. В противном случае значение определяется реализацией.
  • C89 / 90 вызвал это под неопределенным поведение (приложение J) и книга K&R говорит, что это реализация определена. Цитата из K&R:

    это цель объединения-одна переменная, которая может законно содержать любой из нескольких типов. [...] пока использование непротиворечиво: извлеченный тип должен быть самым последним сохраненным типом. Это ответственность программиста отслеживать, какой тип в настоящее время хранится в объединении; результаты зависят от реализации, если что-то хранится как один тип и извлекается как другой.

  • извлечение из TC++PL Страуструпа (акцент мой)

    использование союзов может быть существенным для совместимости данных [...]иногда используется для "преобразования типа".

прежде всего, этот вопрос (название которого остается неизменным с момента моего запроса) был поставлен с намерением понять цель союзов, а не на то, что стандарт позволяет например, использование наследования для повторного использования кода, конечно, разрешено стандартом C++, но это не было целью или первоначальным намерением введения наследования как функции языка C++. Именно поэтому ответ Андрея продолжает оставаться принятым.

14 196

14 ответов:

цель профсоюзов достаточно очевидна, но по каким-то причинам люди довольно часто ее упускают.

цель объединения -для сохранения памяти используя одну и ту же область памяти для хранения различных объектов в разное время. вот и все.

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

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

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

EDIT: использование союзов для целей каламбура типа (т. е. запись одного члена, а затем чтение другого) было дано более подробное определение в одном из технических исправлений к стандарту C99 (см. DR#257 и DR#283). Однако, имейте в виду, что формально это не защищает вы от запуска в неопределенное поведение, пытаясь прочитать представление ловушки.

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

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;

поведение не определено с точки зрения языка. Учтите, что различные платформы могут иметь различные ограничения в выравнивании памяти и endianness. Код в большом endian по сравнению с маленьким endian машина будет обновлять значения в структуре по-разному. Исправление поведения в языке потребует, чтобы все реализации использовали одну и ту же endianness (и ограничения выравнивания памяти...) ограничение использования.

Если вы используете C++ (вы используете два тега) и вы действительно заботиться о переносимости, то вы можете просто использовать структуру и предоставить сеттер, который принимает uint32_t и устанавливает поля соответствующим образом через операции битовой маски. То же самое можно сделать в C с функцией.

Edit: Я ожидал AProgrammer записать ответ на голосование и закрыть эту. Как указывалось в некоторых комментариях, endianness рассматривается в других частях стандарта, позволяя каждой реализации решать, что делать, а выравнивание и заполнение могут также обрабатываются по-разному. Теперь, строгие правила сглаживания, на которые неявно ссылается AProgrammer, являются важным моментом здесь. Компилятору разрешается делать предположения о модификации (или отсутствии модификации) переменных. В случае объединения компилятор может переупорядочить инструкции и переместить чтение каждого цветового компонента поверх записи в переменную цвета.

самый common использование union Я регулярно попадаюсь это псевдоним.

рассмотрим следующее:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

что это значит? Это позволяет чистый, аккуратный доступ на или название:

vec.x=vec.y=vec.z=1.f ;

или целое число в массиве

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

в некоторых случаях доступ по имени-это самое ясное, что вы можете сделать. В других случаях, особенно при выборе оси программно проще всего получить доступ к оси по числовому индексу-0 для x, 1 для y и 2 для z.

Как вы говорите, это строго неопределенное поведение, хотя он будет "работать" на многих платформах. Реальная причина использования союзов заключается в создании записей вариантов.

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

конечно, вам также нужен какой-то дискриминатор, чтобы сказать, что на самом деле содержит вариант. И обратите внимание, что в C++ союзы не очень полезны, потому что они могут содержать только типы POD - эффективно те, которые не имеют конструкторов и деструкторов.

В C это был хороший способ реализовать что-то вроде варианта.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

во времена памяти litlle эта структура использует меньше памяти, чем структура, которая имеет все члены.

кстати C предоставляет

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

для доступа к битовым значениям.

В C++, Boost Variant реализовать безопасную версию объединения, предназначенную для предотвращения неопределенного поведения как можно больше.

его выступления идентичны enum + union построить (стек тоже выделен и т. д.), Но он использует список шаблонов типов вместо enum :)

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

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

в качестве еще одного примера фактического использования объединений фреймворк CORBA сериализует объекты с использованием метода объединения с тегами. Все пользовательские классы являются членами одного (огромного) Союза, и целое число, идентификатор говорит демаршаллер, как интерпретировать Союз.

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

кроме того, профсоюзы не просто для экономии пространства. Они могут помочь современным компиляторам с каламбуром типа. Если вы reinterpret_cast<> все, что компилятор не может делать предположения о том, что вы делаете. Возможно, ему придется выбросить то, что он знает о вашем типе, и начать снова (заставляя запись обратно в память, что в наши дни очень неэффективно по сравнению с тактовой частотой процессора).

другие упоминали различия в архитектуре (little - big endian).

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

например. союз{ поплавок f; int i; } x;

писать в x. я был бы бессмысленным, если бы вы тогда читали из x. f-если это не то, что вы намеревались, чтобы посмотреть на знак, экспонента или компоненты мантиссы поплавка.

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

например. союз{ char c[4]; int i; } x;

Если, гипотетически, на какой-то машине символ должен быть выровнен по словам, то c[0] и c[1] будут совместно использовать память с i, но не c[2] и c[3].

в языке C, как это было задокументировано в 1974 году, все члены структуры имели общее пространство имен, и значение "ptr - > member" было определена Как добавить перемещение члена в "ptr" и доступ к полученному адресу с помощью тип участника. Такая конструкция позволила использовать один и тот же ptr с элементом имена взяты из разных определений структуры, но с одинаковым смещением; программисты использовали эту способность для различных целей.

когда членам структуры были назначены собственные пространства имен, это стало невозможным объявить два элемента структуры с одинаковым смещением. Добавление союзов в язык позволил достичь той же семантики, что и раньше доступны в более ранних версиях языка (хотя невозможность иметь имена, экспортированные в окружающий контекст, возможно, все еще требовали использования найти / заменить для замены Foo - > member в foo - >type1.член.) Что было важно было не столько то, что люди, которые добавленные союзы имеют какие-либо особенности целевое использование в виду, а скорее, что они обеспечивают средства, с помощью которых программисты кто полагался на более раннюю семантику,С какой целью, все равно должны удастся достичь той же семантики, даже если они должны были использовать различные синтаксис, чтобы сделать это.

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

  1. удобный способ доступа к одним и тем же данным по-разному, как в вашем примере
  2. способ экономии пространства, когда есть разные элементы данных, из которых только один может быть активным'

1 на самом деле больше похоже на взлом в стиле C для кратковременного написания кода на основе того, что вы знаете, как работает архитектура памяти целевой системы. Как уже было сказано, вы можете нормально уйти с ним, если на самом деле вы не нацелены на множество различных платформ. Я считаю, что некоторые компиляторы могут позволить вам также использовать директивы упаковки (я знаю, что они делают на структурах)?

хороший пример 2. можно найти в вариант тип широко используется в COM.