Union c что это
Перейти к содержимому

Union c что это

  • автор:

union

В C++17 и более поздних версиях std::variant class это типобезопасная альтернатива для union.

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

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

Синтаксис

Параметры

tag
Имя типа, заданное параметру union.

member-list
Элементы, которые union могут содержаться.

Объявление union

Начните объявление с union помощью union ключевое слово и заключите список членов в фигурные скобки:

// declaring_a_union.cpp union RecordType // Declare a simple union type < char ch; int i; long l; float f; double d; int *int_ptr; >; int main() < RecordType t; t.i = 5; // t holds an int t.f = 7.25; // t now holds a float >

Использование union

В предыдущем примере любой код, обращаюющийся к union данным, должен знать, какой член содержит данные. Наиболее распространенное решение этой проблемы называется дискриминированным union. Он включает union в себя элементstruct, указывающий enum тип элемента, хранящийся в данный момент.union В следующем примере демонстрируется использование основного подхода:

#include using namespace std; enum class WeatherDataType < Temperature, Wind >; struct TempData < int StationId; time_t time; double current; double max; double min; >; struct WindData < int StationId; time_t time; int speed; short direction; >; struct Input < WeatherDataType type; union < TempData temp; WindData wind; >; >; // Functions that are specific to data types void Process_Temp(TempData t) <> void Process_Wind(WindData w) <> void Initialize(std::queue& inputs) < Input first; first.type = WeatherDataType::Temperature; first.temp = < 101, 1418855664, 91.8, 108.5, 67.2 >; inputs.push(first); Input second; second.type = WeatherDataType::Wind; second.wind = < 204, 1418859354, 14, 27 >; inputs.push(second); > int main(int argc, char* argv[]) < // Container for all the data records queueinputs; Initialize(inputs); while (!inputs.empty()) < Input const i = inputs.front(); switch (i.type) < case WeatherDataType::Temperature: Process_Temp(i.temp); break; case WeatherDataType::Wind: Process_Wind(i.wind); break; default: break; >inputs.pop(); > return 0; > 

В предыдущем примере union имяstruct Input отсутствует, поэтому он называется анонимным union. Доступ к его членам можно получить напрямую, как если бы они являются членами .struct Дополнительные сведения об использовании анонимного unionсм. в разделе «Анонимныйunion «.

В предыдущем примере показана проблема, которую также можно решить с помощью class типов, производных от общей базы class. Вы можете ветвить код на основе типа среды выполнения каждого объекта в контейнере. Ваш код может быть проще поддерживать и понимать, но он также может быть медленнее, чем использование union. Кроме того, с unionпомощью приложения можно хранить несвязанные типы. Позволяет union динамически изменять тип хранимого значения, не изменяя тип самой переменной union . Например, можно создать разнородный массив MyUnionType , элементы которого хранят разные значения разных типов.

Это легко неправильно использовать Input struct в примере. Это до пользователя, чтобы использовать дискриминатор правильно для доступа к члену, в котором хранятся данные. Вы можете защититься от неправильного union private использования, сделав и предоставив специальные функции доступа, как показано в следующем примере.

Неограниченный union (C++11)

В C++03 и более ранних версиях union может содержатьstatic не члены данных, имеющие class тип, если у типа нет предоставленных пользователем constructors, destructors или операторов назначения. В C++11 эти ограничения отсутствуют. Если в него включен такой членunion, компилятор автоматически помечает какие-либо специальные функции-члены, которые не предоставляются пользователем. deleted union Если он является анонимным union внутри class или struct, то любые специальные функции-члены classstruct или не предоставленные пользователем помечены как deleted . В следующем примере показано, как обрабатывать этот случай. Один из членов имеет член, который требует этого специального union лечения:

// for MyVariant #include #include #include // for sample objects and output #include #include #include using namespace std; struct A < A() = default; A(int i, const string& str) : num(i), name(str) <>int num; string name; //. >; struct B < B() = default; B(int i, const string& str) : num(i), name(str) <>int num; string name; vector vec; // . >; enum class Kind < None, A, B, Integer >; #pragma warning (push) #pragma warning(disable:4624) class MyVariant < public: MyVariant() : kind_(Kind::None) < >MyVariant(Kind kind) : kind_(kind) < switch (kind_) < case Kind::None: break; case Kind::A: new (&a_) A(); break; case Kind::B: new (&b_) B(); break; case Kind::Integer: i_ = 0; break; default: _ASSERT(false); break; >> ~MyVariant() < switch (kind_) < case Kind::None: break; case Kind::A: a_.~A(); break; case Kind::B: b_.~B(); break; case Kind::Integer: break; default: _ASSERT(false); break; >kind_ = Kind::None; > MyVariant(const MyVariant& other) : kind_(other.kind_) < switch (kind_) < case Kind::None: break; case Kind::A: new (&a_) A(other.a_); break; case Kind::B: new (&b_) B(other.b_); break; case Kind::Integer: i_ = other.i_; break; default: _ASSERT(false); break; >> MyVariant(MyVariant&& other) : kind_(other.kind_) < switch (kind_) < case Kind::None: break; case Kind::A: new (&a_) A(move(other.a_)); break; case Kind::B: new (&b_) B(move(other.b_)); break; case Kind::Integer: i_ = other.i_; break; default: _ASSERT(false); break; >other.kind_ = Kind::None; > MyVariant& operator=(const MyVariant& other) < if (&other != this) < switch (other.kind_) < case Kind::None: this->~MyVariant(); break; case Kind::A: *this = other.a_; break; case Kind::B: *this = other.b_; break; case Kind::Integer: *this = other.i_; break; default: _ASSERT(false); break; > > return *this; > MyVariant& operator=(MyVariant&& other) < _ASSERT(this != &other); switch (other.kind_) < case Kind::None: this->~MyVariant(); break; case Kind::A: *this = move(other.a_); break; case Kind::B: *this = move(other.b_); break; case Kind::Integer: *this = other.i_; break; default: _ASSERT(false); break; > other.kind_ = Kind::None; return *this; > MyVariant(const A& a) : kind_(Kind::A), a_(a) < >MyVariant(A&& a) : kind_(Kind::A), a_(move(a)) < >MyVariant& operator=(const A& a) < if (kind_ != Kind::A) < this->~MyVariant(); new (this) MyVariant(a); > else < a_ = a; >return *this; > MyVariant& operator=(A&& a) < if (kind_ != Kind::A) < this->~MyVariant(); new (this) MyVariant(move(a)); > else < a_ = move(a); >return *this; > MyVariant(const B& b) : kind_(Kind::B), b_(b) < >MyVariant(B&& b) : kind_(Kind::B), b_(move(b)) < >MyVariant& operator=(const B& b) < if (kind_ != Kind::B) < this->~MyVariant(); new (this) MyVariant(b); > else < b_ = b; >return *this; > MyVariant& operator=(B&& b) < if (kind_ != Kind::B) < this->~MyVariant(); new (this) MyVariant(move(b)); > else < b_ = move(b); >return *this; > MyVariant(int i) : kind_(Kind::Integer), i_(i) < >MyVariant& operator=(int i) < if (kind_ != Kind::Integer) < this->~MyVariant(); new (this) MyVariant(i); > else < i_ = i; >return *this; > Kind GetKind() const < return kind_; >A& GetA() < _ASSERT(kind_ == Kind::A); return a_; >const A& GetA() const < _ASSERT(kind_ == Kind::A); return a_; >B& GetB() < _ASSERT(kind_ == Kind::B); return b_; >const B& GetB() const < _ASSERT(kind_ == Kind::B); return b_; >int& GetInteger() < _ASSERT(kind_ == Kind::Integer); return i_; >const int& GetInteger() const < _ASSERT(kind_ == Kind::Integer); return i_; >private: Kind kind_; union < A a_; B b_; int i_; >; >; #pragma warning (pop) int main() < A a(1, "Hello from A"); B b(2, "Hello from B"); MyVariant mv_1 = a; cout ; mv_1 = move(b); cout > c; > 

Не union удается сохранить ссылку. Кроме того, не union поддерживается наследование. Это означает, что вы не можете использовать union как базу classили наследовать от другого classили иметь виртуальные функции.

Инициализация union

Можно объявить и инициализировать union в той же инструкции, назначив выражение, заключенное в фигурные скобки. Выражение вычисляется и назначается первому полю union.

#include using namespace std; union NumericType < short iValue; long lValue; double dValue; >; int main() < union NumericType Values = < 10 >; // iValue = 10 cout /* Output: 10 3.141600 */ 

Он NumericType union упорядочен в памяти (концептуально), как показано на следующем рисунке:

Diagram that shows the overlapping storage of data in the NumericType union.

На схеме показаны 8 байт данных. Двойной тип dValue занимает весь 8 байт. Тип long lValue занимает первые 4 байта. Короткий тип iValue занимает первый байт.

Анонимные union

Анонимный union объявлен без class-name или declarator-list .

Имена, объявленные в анонимном, union используются напрямую, например переменные nonmember. Это означает, что имена, объявленные в анонимном, union должны быть уникальными в окружающем область.

Анонимный union имеет следующие ограничения:

  • Если он объявлен в файле или пространстве имен область, он также должен быть объявлен как static .
  • У него могут быть только public члены; наличие private и protected члены в анонимном union генерации ошибок.
  • Он не может иметь функции-члены.

Объединения

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

union union_type int i; char ch;
>;

Как и для структур, можно объявить переменную, поместив ее имя в конце определения или используя отдельный оператор объявления. Для объявления переменной cnvt объединения union_type следует написать:

union union_type cnvt;

В cnvt как целое число i, так и символ ch занимают один участок памяти. (Конечно, i занимает 2 или 4 байта, a ch — только 1.) Рисунок показывает, как i и ch разделяют один участок памяти (предполагается наличие 16-битных целых). Можно обратиться к данным, сохраненным в cnvt, как к целому числу, так и к символу.

write_int(1000, fp);
fclose(fp);
return 0;
>

/* вывод целого с помощью объединения */

int write_int (int num, FILE *fp) union pw wrd;
wrd.i = num;
putс(wrd.ch[0], fp); /* вывод первой половины */
return putc(wrd.ch[1], fp); /* вывод второй половины */
>

Хотя write_int() вызывается с целым, она использует объединение для записи обеих половинок целого в дисковый файл побайтно.

Union c что это

На структуры во многом похожи объединения. Объединения (union) также позволяют определить свой тип данных и также хранят набор элементов, но в отличие от структуры все элементы объединения имеют нулевое смещение. А это значит, что разные элементы занимают в памяти один и тот же участок, то есть в памяти они накладываются друг на друга.

Для определения объединений применяется ключевое слово union и следующий формальный синтаксис:

union имя_объединения < тип элемент1; тип элемент2; . тип элементN; >;

Фактически объединение определяется точно также, как и структура, только вместо слова struct используется ключевое слово union .

Так, создадим простейшее объединение, которое хранит символ и его числовой код из таблицы ASCII:

union ascii < int digit; char letter; >;

Объединение ascii хранит в одном и том же участки памяти объект int (числовой код символа) и объект char (сам символ). Конкретный размер выделенной памяти будет зависеть от системы и реализации, но в общем случае это будет выглядеть примерно следующим образом:

Объединение union в языке программирования Си

В этом случае объединение сode на большинстве платформ будет занимать 4 байта. Длина элементов, как здесь, может быть разной, и в этом случае размер объединения вычисляется по наибольшему элементу.

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

union ascii code;

При определении переменной объединения мы ее можем сразу инициализировать, но стоит учитывать, что инициализировать мы можем только первый элемент объединения. В данном случае это элемент digit типа int, поэтому мы можем передать ему только целое число:

union ascii code = ;

Для обращения к элементам объединения, как и в случае со структурами, можно использовать операцию «точка»:

#include union ascii < int digit; char letter; >; int main(void) < union ascii code; code.digit = 120; printf("%d - %c \n", code.digit, code.letter); // 120 - x printf("%d - %d \n", code.digit, code.letter); // 120 - 120 code.letter = 87; printf("%d - %c \n", code.digit, code.letter); // 87 - W return 0; >

Здесь создается переменная code, которая представляет объединение ascii. Далее его элементу digit (числовой код символа) присваивается число 120:

code.digit = 120;

Стоит отметить, что, так как оба элемента — letter и digit занимают одну и ту же память, то данные фактически одни и те же, только при обращении к code.digit данные интерпретируются как объект int, а при обращении к code.letter — как объект char.

printf("%d - %c \n", code.digit, code.letter); // 120 - x printf("%d - %d \n", code.digit, code.letter); // 120 - 120

И изменение одного из них приведет к изменению другого.

code.letter = 87; printf("%d - %c \n", code.digit, code.letter); // 87 - W

Анонимные объединения и установка псевдонима с помощью typedef

Также можно определять анонимные объединения:

#include union < int digit; char letter; >code1, code2; // переменные code1, code2 int main(void)

С помощью оператора typedef можно задать псевдоним для объединения:

typedef union ascii < int digit; char letter; >ascii_code;

Здесь псевдонимом является идентификатор ascii_code , поэтому следующие определения переменных будут аналогичны:

union ascii code = ; ascii_code code2 = ;

Также можно устанавливать псевдоним для анонимных объединений:

typedef union < int digit; char letter; >ascii_code;

Пример с объединениями

Ключевая возможность объединений состоит в том, что они могут применяться для хранения значений разных типов. Выше мы посмотрели, как объединение может хранить данные одновременно типов int и char. Но в подобном случае все несколько просто — int и char накладываются друг на друга — числовой код символа можно выразить и с помощью типа char, и с помощью типа int. Теперь посмотрим более сложный пример:

#include typedef enum < NODE_STRING, NODE_INT >node_type; typedef union < int int_value; char* str_value; >node_data; typedef struct < node_type type; node_data data; >node; void print_node(node n) < if(n.type == NODE_STRING)< printf("String: %s\n", n.data.str_value); >else if(n.type == NODE_INT) < printf("Int: %d\n", n.data.int_value); >> int main(void)

Здесь у нас определяется структура node, которая имеет два поля — тип структуры в виде перечисления node_type и непосредственно данные структуры в виде поля node_data. Перечисление node_type может принимать два значения — NODE_STRING и NODE_INT , указывая, что структура будет хранить соответственно или строку, или целое число. А объединение node_data имеет два поля:

typedef union < int int_value; char* str_value; >node_data;

Если структура представляет число, то для обращения к данным используется поле int_value , а если строку — то поле str_value

Для упрощения вывода данных структуры node определяем отдельную функцию print_node() :

void print_node(node n) < if(n.type == NODE_STRING)< printf("String: %s\n", n.data.str_value); >else if(n.type == NODE_INT) < printf("Int: %d\n", n.data.int_value); >>

В зависимости от типа структуры обращаем к полю n.data.str_value или n.data.int_value

В функции main для тестирования создаем пару структур и выводим их данные на консоль. Консольный вывод программы:

Int: 22 String: Hello World

Указатели на объединения

И как и со структурами, можно определять указатели на объединения. Для обращения к элементам объединения по указателю применяется тот же синтаксис, что и в случае со структурами:

(* указатель_на_объединение).имя_элемента указатель_на_объединение->имя_элемента

Используем указатели на объединения:

#include union ascii < int digit; char letter; >; int main(void) < union ascii code = ; union ascii * p_code = &code; printf("%d \n", p_code->digit); // 45 p_code->digit= 89; printf("%d \n", code.digit); // 89 return 0; >

Работа с С-объединениями (union) в Rust FFI

Предлагаю вашему вниманию перевод статьи «Working with C unions in Rust FFI» за авторством Herman J. Radtke III.

Примечание: Эта статья предполагает, что читатель знаком с Rust FFI, порядком байтов (endianess) и ioctl.

При создании биндингов к коду на С мы неизбежно столкнёмся со структурой, которая содержит в себе объединение. В Rust отсутствует встроенная поддержка объединений, так что нам придётся выработать стратегию самостоятельно. В С объединение — это тип, который хранит разные типы данных в одной области памяти. Существует много причин, по которым можно отдать предпочтение объединению, такие как: преобразование между бинарными представлениями целых чисел и чисел с плавающей точкой, реализация псевдо-полиморфизма и прямой доступ к битам. Я сфокусируюсь на псевдо-полиморфизме.

Как пример, давайте получим MAC адрес, основанный на имени интерфейса. Перечислим действия, необходимые для его получения:

  • Указать тип запроса, который будет использоваться с ioctl. Если я хочу получить MAC (или аппаратный) адрес, я указываю SIOCGIFHWADDR.
  • Записать имя интерфейса (что-то типа eth0) в ifr_name.
  • Сделать запрос, используя ioctl. В результате удачного запроса данные запишутся в ifr_ifru.

Нам необходимо использовать объявленную в С ioctl функцию и передать туда ifreq структуру. Посмотрев в /usr/include/net/if.h, мы увидим, что ifreq определена следующим образом:

struct ifreq < char ifr_name[IFNAMSIZ]; union < struct sockaddr ifru_addr; struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; short ifru_flags; int ifru_metric; int ifru_mtu; int ifru_phys; int ifru_media; int ifru_intval; caddr_t ifru_data; struct ifdevmtu ifru_devmtu; struct ifkpi ifru_kpi; u_int32_t ifru_wake_flags; u_int32_t ifru_route_refcnt; int ifru_cap[2]; >ifr_ifru; > 

Сложности возникают с объединением ifr_ifru. Взглянув на возможные типы в ifr_ifru, мы видим, что не все из них одинакового размера. short занимает два байта, а u_int32_t — четыре. Ещё больше усложняют ситуацию несколько структур неизвестного размера. Чтобы написать правильный код на Rust, важно выяснить точный размер ifreq структуры. Я создал небольшую программу на С и выяснил, что ifreq использует 16 байт для ifr_name и 24 байт для ifr_ifru.

Вооружившись знаниями о правильном размере структуры, мы можем начать представлять её в Rust. Одна из стратегий — создать специализированную структуру для всех типов объединения.

#[repr(C)] pub struct IfReqShort

Мы можем использовать IfReqShort для запроса SIOCGIFINDEX. Эта структура меньше, чем ifreq структура в С. Хотя мы и предполагаем, что будет записано только 2 байта, внешний ioctl интерфейс ожидает 24 байта. Для безопасности давайте добавим 22 байта выравнивания (padding) в конце:

#[repr(C)] pub struct IfReqShort

Затем мы должны будем повторить этот процесс для каждого типа в объединении. Я нахожу это несколько утомительным, так как нам придётся создать множество структур и быть очень внимательными, чтобы не ошибиться с их размером. Другой способ представить объединение — это иметь буфер сырых байтов. Мы можем сделать единственное представление структуры ifreq в Rust следующим образом:

#[repr(C)] pub struct IfReq

Этот буфер-объединение может хранить байты любого типа. Теперь мы можем определить методы для преобразования сырых байтов в нужный тип. Мы избежим использования небезопасного (unsafe) кода, отказавшись от использования transmute. Давайте создадим метод для получения MAC адреса, преобразовав сырые байты в sockaddr C-тип.

impl IfReq < pub fn ifr_hwaddr(&self) ->sockaddr < let mut s = sockaddr < sa_family: u16::from_be((self.data[0] as u16) ; // basically a memcpy for (i, b) in self.data[2..16].iter().enumerate() < s.sa_data[i] = *b as i8; >s > > 

Такой подход оставляет нам одну структуру и метод для преобразования сырых байтов в желаемый тип. Посмотрев снова на наше ifr_ifru объединение, мы обнаружим, что существует по крайней мере два других запроса, которые тоже требуют создания sockaddr из сырых байтов. Применяя принцип DRY, мы можем реализовать приватный метод IfReq для преобразования сырых байтов в sockaddr. Однако, мы можем сделать лучше, абстрагировав детали создания sockaddr, short, int и т.д. от IfReq. Всё что нам необходимо — это сказать объединению, что нам нужен определённый тип. Давайте создадим IfReqUnion для этого:

#[repr(C)] struct IfReqUnion < data: [u8; 24], >impl IfReqUnion < fn as_sockaddr(&self) ->sockaddr < let mut s = sockaddr < sa_family: u16::from_be((self.data[0] as u16) ; // basically a memcpy for (i, b) in self.data[2..16].iter().enumerate() < s.sa_data[i] = *b as i8; >s > fn as_int(&self) -> c_int < c_int::from_be((self.data[0] as c_int) fn as_short(&self) -> c_short < c_short::from_be((self.data[0] as c_short) > 

Мы реализовали методы для каждого из типов, которые составляют объединение. Теперь, когда наши преобразования управляются IfReqUnion, мы можем реализовать методы IfReq следующим образом:

#[repr(C)] pub struct IfReq < ifr_name: [c_char; IFNAMESIZE], union: IfReqUnion, >impl IfReq < pub fn ifr_hwaddr(&self) ->sockaddr < self.union.as_sockaddr() >pub fn ifr_dstaddr(&self) -> sockaddr < self.union.as_sockaddr() >pub fn ifr_broadaddr(&self) -> sockaddr < self.union.as_sockaddr() >pub fn ifr_ifindex(&self) -> c_int < self.union.as_int() >pub fn ifr_media(&self) -> c_int < self.union.as_int() >pub fn ifr_flags(&self) -> c_short < self.union.as_short() >> 

В итоге у нас есть две структуры. Во первых, IfReq, которая представляет структуру памяти ifreq в языке С. В ней мы реализуем метод для каждого типа ioctl запроса. Во вторых, у нас есть IfRequnion, которая управляет различными типами объединения ifr_ifru. Мы создадим метод для каждого типа, который нам нужен. Это менее трудоёмко, чем создание специализированной структуры для каждого типа объединения, и предоставляет лучший интерфейс, чем преобразование типа в самой IfReq.

Вот более полный готовый пример. Предстоит ещё немного работы, но тесты проходят, и в коде реализуется описанная выше концепция.

Будьте осторожны, этот подход не идеален. В случае ifreq нам повезло, что ifr_name содержит 16 байтов и выровнено по границе слова. Если бы ifr_name не было выровнено по границе четырёхбайтного слова, мы столкнулись бы с проблемой. Тип нашего объединения [u8; 24], которое выравнивается по границе одного байта. У типа размером 24 байта было бы другое выравнивание. Вот короткий пример иллюстрирующий проблему. Допустим, у нас есть С-структура, содержащая следующее объединение:

struct foo < short x; union < int; >y; > 

Эта структура имеет размер 8 байт. Два байта для х, ещё два для выравнивания и четыре байта для у. Давайте попробуем изобразить это в Rust:

#[repr(C)] pub struct Foo

Структура Foo имеет размер только 6 байт: два байта для х и первые два u8 элемента, помещённые в то же четырёхбайтовое слово, что и х. Эта едва заметная разница может вызвать проблемы при передаче в С-функцию, которая ожидает структуру размеров в 8 байт.

До тех пор пока Rust не будет поддерживать объединения, такие проблемы сложно будет решить корректно. Удачи, но будьте осторожны!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *