Цель объединений в 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++.
  • После обыска через стандарт 11 C++ я не мог окончательно сказать, что он вызывает, доступ/осмотр к неактивному члену профсоюза является undefined/unspecified/implementation-defined. Все, что я мог найти, было §9.5/1:

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

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

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

  • Извлечение из TC Stroustrup ++ МН (шахта акцента)

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

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

235
задан Community 23 May 2017 в 02:25
поделиться

11 ответов

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

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

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

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

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

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

371
ответ дан 23 November 2019 в 03:27
поделиться

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

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

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

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

2
ответ дан 23 November 2019 в 03:27
поделиться

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

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

например union{ float f; int i; x;

Запись в x.i будет бессмысленной, если вы затем прочитаете из x.f - если только вы не хотели посмотреть на знак, экспоненту или компоненты мантиссы плавающей величины.

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

например. union{ char c[4]; int i; } x;

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

4
ответ дан 23 November 2019 в 03:27
поделиться

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

4
ответ дан 23 November 2019 в 03:27
поделиться

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

4
ответ дан 23 November 2019 в 03:27
поделиться

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

5
ответ дан 23 November 2019 в 03:27
поделиться

В 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;

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

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

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

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

7
ответ дан 23 November 2019 в 03:27
поделиться

Он использует 0 для указания «Я еще не разработал хэш-код». Альтернативой было бы использование отдельного логического флага, который занимал бы больше памяти. (Или не кэшировать хэш-код вообще, конечно.)

Я не ожидаю, что многие строки хэш 0; возможно, было бы целесообразно, чтобы процедура хеширования намеренно избегала 0 (например, перевести хэш от 0 до 1 и кэшировать это). Это увеличило бы столкновения, но позволило бы избежать повторного хэширования. Однако уже слишком поздно делать это, так как алгоритм String hashCode явно задокументирован.

Что касается того, является ли это хорошей идеей в целом: это, безусловно, эффективный механизм кэширования, и может (см. редактирование) быть еще лучше с изменением, чтобы избежать повторного хэширования значений, которые заканчиваются хэшем 0. Лично мне было бы интересно увидеть данные, которые заставили Sun поверить, что это стоит сделать в первую очередь - это занимает дополнительные 4 байта для каждой когда-либо созданной последовательности, однако часто или редко она хэшируется, и единственное преимущество для последовательностей, которые хэшируются более одного раза .

EDIT: Как точек KevinB в комментарии в другом месте, предложение «избегайте 0» выше вполне может иметь чистую стоимость , потому что это помогает очень редкий случай, но требует дополнительного сравнения для каждого хеш-вычисления.

-121--867472-

Напрашиваюсь на себя после долгого расследования: Невозможно без повторного внедрения всего GroupMtomWriter и других связанных уровней и проблем в WCF - почти все, что участвует в реализации mtom, является внутренним.

-121--3199706-

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

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 - эффективно те, которые без конструкторов и деструкторов.

9
ответ дан 23 November 2019 в 03:27
поделиться

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

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

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
36
ответ дан 23 November 2019 в 03:27
поделиться

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

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

{{ 1}}
5
ответ дан 23 November 2019 в 03:27
поделиться

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

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

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

34
ответ дан 23 November 2019 в 03:27
поделиться