C++에서 Union은 모든 멤버변수가 동일한 메모리 위치를 공유하는 사용자 정의 형식입니다.
멤버변수가 아무리 많더라도 항상 가장 큰 멤버 변수의 크기로 메모리를 할당 받게 됩니다.
따라서 멤버변수가 많지만, 메모리가 제한된 경우에 유용하게 사용될 수 있습니다.
struct(구조체)는 내부에 여러 Data Type에 대한 멤버변수를 선언하고 여러 변수를 저장할 수 있습니다.
union(공용 구조체)는 여러 Data Type에 대한 멤버변수를 선언할 수는 있지만, union 내에 선언된 멤버변수는 하나의 공통된 메모리 공간을 공유 하기 때문에 하나의 멤버변수만 저장할 수 있습니다.
#include <iostream>
using namespace std;
union NumericType
{
short iValue;
long lValue;
double dValue;
};
int main()
{
union NumericType Values = { 10 }; // iValue = 10
cout << Values.iValue << endl;
Values.dValue = 3.1416;
cout << Values.dValue << endl;
}
/* Output:
10
3.141600
*/
위의 코드는 Microsoft Learn 에서 union(공용 구조체)를 설명하기 위해 보여주고 있는 코드 입니다.
NumericType 이라는 union 공용 구조체 내에 여러 data type을 가지는 멤버변수를 만들어주고 이것들이 어떻게 메모리를 할당 받고 있는지를 아래의 그림이 보여주고 있습니다.
근데 short는 2byte니까 1byte인 bool이나 char를 쓰는게 그림이랑 맞지 않았을까...?
건축 관련 응용 소프트웨어에서 사용되고 있는 Wind Load 관련 Code를 살펴보면, 제품 UI 상에서 사용자가 선택한 풍하중 기준에 맞게 해당 Data 만 멤버변수로 가지고 있게 됩니다.
union T_WIND_CODE
{
T_WIND_KS1992 KS1992;
T_WIND_JP1987 JP1987;
T_WIND_UBC1997 UBC1997;
T_WIND_ANSI1982 ANSI1982;
T_WIND_KS2000 KS2000;
T_WIND_IBC2000 IBC2000;
T_WIND_EURO1992 EURO1992;
T_WIND_BS6399 BS6399;
T_WIND_CH2002 CH2002;
T_WIND_JPN2000 JPN2000;
T_WIND_NBC1995 NBC1995;
T_WIND_IS1987 IS1987;
T_WIND_TAIWAN86 TAIWAN86;
T_WIND_JP2004 JP2004;
T_WIND_EURO2005 EURO2005;
T_WIND_KBC2009 KBC2009;
T_WIND_IBC2012 IBC2009;
T_WIND_IBC2012 IBC2012;
T_WIND_CH2012 CH2012;
T_WIND_CH2019 CH2019;
T_WIND_NSR2010 NSR2010;
T_WIND_KBC2015 KBC2015;
T_WIND_IS875_2015 IS875_2015;
T_WIND_KBC2015 KDS2019;
T_WIND_KDS2021 KDS2021;
T_WIND_DPT2007 DPT2007;
T_WIND_ASCE7_16 ASCE7_16;
T_WIND_ASCE7_16 ASCE7_22;
void Initialize(int nCode)
{
...
}
}
여기서 주의해야 할 점은 기존에 넣어 놓은 Data와 다른 멤버변수에 Data를 넣게 되면 의도하지 않은 값들이 출력될 수 있는 문제가 발생하게 됩니다.
const auto key = KDS_XXX_2019;
T_WIND_CODE DataOrg;
if ( !m_pDB->GetWind(key, DataOrg) )
{
DataOrg.Initialize();
}
const auto key = KDS_XXX_2021;
T_WIND_CODE DataNew;
if ( !m_pDB->GetWind(key, DataNew) )
{
DataNew.Initialize();
}
DataOrg = DataNew; // 해당 멤버변수의 구조체 크기에 따라 쓰레기 값 출력 가능성이 있음
예를 들면,
1. KDS2019 기준의 Data를 DataOrg에 Get 해온다.
2. KDS2021 기준의 Data를 DataNew에 Get 해온다.
3. DataOrg에 DataNew의 Data를 넣어준다.
4. 해당 멤버변수 크기에 따라 쓰레기 값이 출력될 수 있다.
실제로 현재 개발하고 있는 제품의 소스코드에서는 다음과 같이 쓰이고 있습니다.
#define T_NODE_K unsigned int
#define T_ABCD_KEY T_NODE_K
union T_ABCD_K
{
T_ABCD_KEY keymap;
struct
{
unsigned int entity : 20; // 0-1048576
unsigned int serial : 12; // 0-4096
}key;
};
T_ABCD_K 는 "keymap"과 "key"라는 두 개의 멤버 변수를 가지고 있습니다.
"key"는 구조체로 정의되어 있으며, 비트 필드(bit field)를 사용하여 두 개의 멤버 변수("entity"와 "serial")를 동시에 가지고 있습니다.
"entity"는 20bit, "serial"은 12bit를 차지하며, 이는 각각 0~1,048,576까지와 0~4,096 까지의 값을 표현할 수 있습니다.
따라서, "union T_ABCD_K"를 사용하면 "keymap"과 "key" 중 하나의 멤버 변수를 사용할 수 있고, 사용 중인 멤버 변수에 관계 없이 메모리를 공유하게 됩니다.
비트 필드(bit field)란?
데이터 구조체에서 여러 비트를 사용하여 하나의 변수에 여러 가지 정보를 압축하여 저장하는 기술
위의 실무 예시와 같이,
따라서 메모리를 절약하려는 의도로 막 쓰기 보다는 시기 적절하게 필요한 경우에만 잘 활용해주면 좋을 것 같습니다.
실무 예제를 보면 결국 비트연산으로 20자리까지를 (2^20 = 1048576) entity 로 사용하고 serial을 2^8 만큼 사용해서 해당 조합이 union 에서는 keymap 과 메모리를 공유하니까 고유한 key 가 출력 되겠네요. (ex) entity = 1, serial 1 이면 keymap = 1048576 + 1 = 1048577 )
홍다라박 스터디 화이팅 입니다.