Назначение Союзов в 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 и далее) это допустимо ( спасибо Паскалю Куоку за то, что поднял этот вопрос). Однако попытка сделать это может привести к неопределенному поведению, если считанное значение окажется недопустимым (так называемое "представление ловушки") для типа, через которое оно читается. В противном случае значение read определяется реализацией.
C89 / 90 вызвал это при неопределенном поведении (Приложение J), а в книге K&R говорится, что его реализация определена. Цитата из K&R:
Это цель объединения - единственная переменная, которая может на законных основаниях содержать любой из нескольких типов. [...], пока использование является последовательным: извлеченный тип должен быть последним сохраненным типом. Программист обязан следить за тем, какой тип в настоящий момент хранится в объединении; результаты зависят от реализации, если что-то хранится как один тип и извлекается как другой.
Извлечение из TC++PL Страуструпа (выделено мое)
Использование союзов может иметь важное значение для совместимости данных, [...] иногда неправильно используемых для "преобразования типов".
Прежде всего, этот вопрос (название которого остается неизменным со времени моего запроса) был задан с намерением понять цель объединений, а не то, что стандарт позволяет, например, Использование наследования для повторного использования кода, конечно, разрешено стандартом C++, но это не было целью или первоначальным намерением ввести наследование как особенность языка C++. По этой причине ответ Андрея продолжает оставаться принятым.
14 ответов
Цель профсоюзов довольно очевидна, но по некоторым причинам люди часто упускают ее.
Цель объединения - сохранить память, используя одну и ту же область памяти для хранения разных объектов в разное время. Вот и все.
Это как комната в отеле. Разные люди живут в нем в течение непересекающихся периодов времени. Эти люди никогда не встречаются и вообще ничего не знают друг о друге. Правильно управляя распределением времени между комнатами (т. Е. Следя за тем, чтобы разные люди не были назначены на одну комнату одновременно), относительно небольшая гостиница может предоставить жилье относительно большому количеству людей, то есть, какие гостиницы для.
Это именно то, что делает союз. Если вы знаете, что несколько объектов в вашей программе содержат значения с неперекрывающимися значениями времени жизни, вы можете "объединить" эти объекты в объединение и тем самым сэкономить память. Точно так же, как в гостиничном номере в каждый момент времени имеется не более одного "активного" арендатора, в профсоюзе в каждый момент времени программы может быть не более одного "активного" члена. Только "активный" член может быть прочитан. Записав другого участника, вы переключаете "активный" статус на другого участника.
По какой-то причине эта первоначальная цель объединения была "переопределена" чем-то совершенно другим: написание одного члена союза, а затем проверка его через другого члена. Этот вид реинтерпретации памяти (также известный как "наказание по типу") не является допустимым использованием союзов. Как правило, это приводит к неопределенному поведению, описанному как создание определяемого реализацией поведения в C89/90.
РЕДАКТИРОВАТЬ: Использование союзов для целей типа наказания (т.е. написание одного члена, а затем чтение другого) было дано более подробное определение в одном из Технических исправлений к стандарту C99 (см. DR#257 и DR # 283). Однако имейте в виду, что формально это не защищает вас от непреднамеренного поведения, когда вы пытаетесь прочитать представление ловушки.
Вы можете использовать объединения для создания структур, подобных следующему, в котором содержится поле, которое сообщает нам, какой компонент объединения фактически используется:
struct VAROBJECT
{
enum o_t { Int, Double, String } objectType;
union
{
int intValue;
double dblValue;
char *strValue;
} value;
} object;
Поведение не определено с языковой точки зрения. Учтите, что разные платформы могут иметь разные ограничения в выравнивании памяти и порядке байтов. Код с прямым порядком байтов по сравнению с порядковым номером немного по-другому обновит значения в структуре. Исправление поведения в языке потребовало бы, чтобы все реализации использовали один и тот же порядок байтов (и ограничения выравнивания памяти...), ограничивающие использование.
Если вы используете C++ (вы используете два тега) и действительно заботитесь о переносимости, тогда вы можете просто использовать структуру и предоставить установщик, который принимает uint32_t
и устанавливает поля соответствующим образом с помощью операций битовой маски. То же самое можно сделать в Си с помощью функции.
Редактировать: я ожидал, что AProgrammer запишет ответ для голосования и закроет этот. Как отмечалось в некоторых комментариях, порядок байтов рассматривается в других частях стандарта, позволяя каждой реализации решать, что делать, и выравнивание и заполнение также могут обрабатываться по-разному. Теперь, строгие правила псевдонимов, на которые 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-типы - фактически те, которые не имеют конструкторов и деструкторов.
В Си это был хороший способ реализовать что-то вроде варианта.
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;
Во времена маленькой памяти эта структура использует меньше памяти, чем структура, в которой есть все члены.
Кстати, С обеспечивает
typedef struct {
unsigned int mantissa_low:32; //mantissa
unsigned int mantissa_high:20;
unsigned int exponent:11; //exponent
unsigned int sign:1;
} realVal;
для доступа к битовым значениям.
Поведение может быть неопределенным, но это просто означает, что не существует "стандарта". Все достойные компиляторы предлагают #pragmas для управления упаковкой и выравниванием, но могут иметь разные значения по умолчанию. Значения по умолчанию также будут меняться в зависимости от используемых настроек оптимизации.
Кроме того, профсоюзы не только для экономии места. Они могут помочь современным компиляторам с типом штамповки. если ты reinterpret_cast<>
все, что компилятор не может делать предположения о том, что вы делаете. Возможно, придется выбросить все, что он знает о вашем типе, и начать заново (принудительная запись в память, что в наши дни очень неэффективно по сравнению с тактовой частотой процессора).
Хотя это строго неопределенное поведение, на практике оно будет работать практически с любым компилятором. Это настолько широко используемая парадигма, что любой уважающий себя компилятор должен будет делать "правильные вещи" в таких случаях, как этот. Это, безусловно, предпочтительнее, чем наказание типов, которое может генерировать некорректный код с некоторыми компиляторами.
В C++ Boost Variant реализует безопасную версию объединения, разработанную для максимально возможного предотвращения неопределенного поведения.
Его спектакли идентичны enum + union
конструкция (выделенный стек и т. д.), но он использует список типов шаблонов вместо enum
:)
Другие упоминали различия в архитектуре (little - big endian).
Я прочитал проблему, заключающуюся в том, что, поскольку память для переменных является общей, то путем записи одной переменной другие изменяются, и, в зависимости от их типа, значение может быть бессмысленным.
например. union{ float f; Int I; } Икс;
Запись в xi была бы бессмысленной, если бы вы потом читали из xf - если только это не то, что вы намеревались для того, чтобы взглянуть на компоненты знака, экспоненты или мантиссы поплавка.
Я думаю, что есть также проблема выравнивания: если некоторые переменные должны быть выровнены по словам, вы можете не получить ожидаемый результат.
например. union{ char c[4]; Int I; } Икс;
Если гипотетически на некоторой машине символ должен быть выровнен по словам, то c[0] и c[1] будут совместно использовать память с i, но не c[2] и c[3].
Технически это не определено, но на самом деле большинство (все?) Компиляторы относятся к нему точно так же, как и к reinterpret_cast
от одного типа к другому, результат которого определяется реализацией. Я не потерял бы сон из-за вашего текущего кода.
Для еще одного примера фактического использования объединений платформа CORBA сериализует объекты, используя подход с теговым объединением. Все пользовательские классы являются членами одного (огромного) объединения, а целочисленный идентификатор сообщает демаршаллеру, как интерпретировать объединение.
На языке Си, как это было задокументировано в 1974 году, все члены структуры имели общее пространство имен, и значение "ptr->member" было определено как добавление смещения члена к "ptr" и доступ к результирующему адресу с использованием типа члена. Этот дизайн позволил использовать один и тот же ptr с именами элементов, взятыми из разных определений структуры, но с одинаковым смещением; программисты использовали эту способность для различных целей.
Когда членам структуры были назначены их собственные пространства имен, стало невозможно объявить два члена структуры с одинаковым смещением. Добавление объединений к языку позволило достичь той же семантики, которая была доступна в более ранних версиях языка (хотя невозможность экспортировать имена в окружающий контекст, возможно, все еще требовала использования поиска / замены для замены члена foo-> в foo->type1.member). Важно было не столько, чтобы люди, которые добавляли союзы, имели в виду какую-то конкретную цель использования, а скорее то, что они предоставляют средство, с помощью которого программисты, которые полагались на более раннюю семантику, для какой-либо цели, все еще могли бы достичь та же семантика, даже если они должны были использовать другой синтаксис для этого.
Как уже упоминалось, объединения, объединенные с перечислениями и завернутые в структуры, могут использоваться для реализации тегированных объединений. Одно из практических применений - реализовать RustResult<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;
}
Вы можете использовать союз по двум основным причинам:
- Удобный способ доступа к одним и тем же данным разными способами, как в вашем примере
- Способ экономии места при наличии разных элементов данных, из которых только один может быть "активным"
На самом деле это скорее хакерский стиль в стиле C для быстрого написания кода на основе того, что вы знаете, как работает архитектура памяти целевой системы. Как уже говорилось, вы можете сойти с рук, если на самом деле не нацелены на множество разных платформ. Я полагаю, что некоторые компиляторы могут позволять вам также использовать директивы упаковки (я знаю, что они используют для структур)?
Хороший пример 2. можно найти в типе VARIANT, широко используемом в COM.
Код @bobobobo правильный, как указал @Joshua (к сожалению, мне не разрешено добавлять комментарии, поэтому делаю это здесь, IMO плохое решение запретить его в первую очередь):
https://en.cppreference.com/w/cpp/language/data_members#Standard_layout сообщает, что это нормально, по крайней мере, с C++14
В объединении стандартной компоновки с активным членом типа класса без объединения T1 разрешено читать нестатический член данных m другого члена объединения типа T2 класса, не являющегося объединением, при условии, что m является частью общей начальной последовательности T1 и T2 (за исключением того, что чтение изменчивого члена через энергонезависимое значение glvalue не определено).
поскольку в текущем случае T1 и T2 в любом случае передают один и тот же тип.