Цель союзов в 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

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

Update:

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

  • Ответ на вопрос одинаков для C и С++; мой невежественный молодой человек отметил его как C и С++.
  • После прокрутки по стандарту С++ 11 я не мог окончательно сказать, что он вызывает доступ/проверку члена неактивного профсоюза undefined/unspecified/defined-defined. Все, что я мог найти, было §9.5/1:

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

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

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

  • Выдержка из Stroustrup TС++ PL (выделение мое)

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

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

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

Наиболее частое использование union, с которым я регулярно сталкиваюсь, - это aliasing.

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

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

Что это делает? Он обеспечивает чистый, опрятный доступ членов Vector3f vec; по любому имени:

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.

Ответ 5

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

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;

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

Другие упомянули различия в архитектуре (маленький - большой конец).

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

например.   объединение {     float f;     int i;   } x;

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

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

например.   объединение {      char c [4];     int i;   } x;

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

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

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

Ответ 14

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

  • Удобный способ доступа к тем же данным по-разному, как в вашем примере
  • Способ сохранения пространства, когда есть разные члены данных, из которых только один может быть "активным"

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

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

Ответ 15

Как уже упоминалось, союзы, объединенные с перечислениями и заключенные в структуры, могут использоваться для реализации помеченных союзов. Одним из практических применений является реализация Rust Result<T, E>, который изначально реализован с использованием чистого enum (Rust может содержать дополнительные данные в вариантах перечисления). Вот пример C++:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}