Вопрос

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

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 и далее) это законно делать (спасибо Паскалю Куоку за то, что поднял этот вопрос).Однако, пытаясь сделать это все еще может привести к неопределенному поведению, если прочитанное значение оказывается недопустимым (так называемое "представление ловушки") для типа, через который оно считывается.В противном случае прочитанное значение определяется реализацией.
  • C89 / 90 вызвал это в разделе неуказанное поведение (приложение J), и в книге K & R говорится, что это определено реализацией.Цитата из K & R:

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

  • Выдержка из TC++ PL Страуструпа (выделено мной)

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

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

Это было полезно?

Решение

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

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

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

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

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

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

Другие советы

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

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

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

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

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

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

Самый Обычный использование union Я регулярно сталкиваюсь с ис сглаживание.

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

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.

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

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;

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

Кстати, 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++, Вариант наддува внедрите безопасную версию объединения, предназначенную для максимально возможного предотвращения неопределенного поведения.

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

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

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

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

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

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

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

например.объединение{ с плавающей точкой f;int я;} x;

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

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

например.объединение{ символ c[4];int я;} x;

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

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

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

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

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

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

Хороший пример из 2.можно найти в ВАРИАНТ тип, широко используемый в COM.

Как упоминали другие, объединения в сочетании с перечислениями и завернутые в структуры могут быть использованы для реализации помеченных объединений.Одним из практических применений является внедрение 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;
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top