만약 한정된 메모리에서, 한 종류의 값만 표현하는 것이 아니라,
여러 종류의 값을 효율적으로 표현하고 싶으면 어떻게 해야 좋을까요?
Union
#include <cstdint>
#include <iostream>
struct foo
{
bool a; // 1 byte
uint32_t b; // 4 byte
};
int main()
{
std::cout << sizeof(foo) << std::endl; // 8
}
struct foo | |||||||
a | padding | b | |||||
alignment | alignment | ||||||
1 byte | 1 byte | 1 byte | 1 byte | 1 byte | 1 byte | 1 byte | 1 byte |
위 코드에서 구조체 foo의 필드는 각각 1, 4 byte를 사용해서 값을 표현하고 있어요.
만약 a, b 중에서 항상 한 가지 필드만 사용한다는 가정 하에, 서로 다른 자료형을 표현하기 위해서
구조체 패딩을 포함해 불필요한 메모리를 추가로 할당하는 것은 메모리 측면에서 효율적이지 않아요.
이때 멤버 중에 가장 큰 자료형의 크기의 메모리 공간을 서로 공유해서 사용하면 보다 효율적이지 않을까요?
#include <cstdint>
#include <iostream>
union foo
{
bool a; // 1 byte
uint16_t b; // 4 byte
};
int main()
{
std::cout << sizeof(foo) << std::endl; // 4
}
struct foo | |||
a | - | - | - |
b | |||
alignment | |||
1 byte | 1 byte | 1 byte | 1 byte |
공용체(union)는 이처럼 가장 큰 필드의 자료형의 메모리를 모든 필드가 공유해서 사용해요.
또한 메모리를 공유하는 것 뿐만이 아니라, 구조체 패딩 또한 없기 때문에 메모리 측면에서 매우 우수해요.
다만 공용체에는 심각한 단점이 하나 있는데, 공용체는 언제 어떤 필드가 활성화되어 있는지 스스로 알 수 없어요.
#include <cstdint>
#include <iostream>
union foo
{
bool a; // 1 byte
uint16_t b; // 4 byte
};
foo with_a()
{
return { .a = true };
}
foo with_b()
{
return { .b = 69 };
}
foo toss()
{
switch (rand() % 2)
{
case 0:
{
return with_a();
}
default:
{
return with_b();
}
}
}
int main()
{
srand(time(0));
auto coin {toss()};
// coin.a ... is it set?
// coin.b ... is it set?
}
따라서 보통 구조체 내부에 공용체의 어느 필드가 활성화되어 있는지 판별하는 필드를 추가해서 같이 사용해요.
#include <cstdint>
#include <iostream>
struct foo
{
union data
{
bool a;
uint16_t b;
};
enum : uint8_t
{
A,
B,
}
tag;
};
foo with_a()
{
return { .data = { .a = true }, .tag = foo::A };
}
foo with_b()
{
return { .data = { .b = 69 }, .tag = foo::B };
}
foo toss()
{
switch (rand() % 2)
{
case 0:
{
return with_a();
}
default:
{
return with_b();
}
}
}
int main()
{
srand(time(0));
auto coin {toss()};
switch (coin.tag)
{
case foo::A:
{
std::cout << "a:" << coin.data.a << std::endl;
break;
}
case foo::B:
{
std::cout << "b:" << coin.data.b << std::endl;
break;
}
}
}
이처럼 어떤 필드가 활성화 되어 있는지 판별하는 필드를 포함하는 공용체를 tagged union이라고 불러요.
Bit-field
bool 값은 0과 1로 참과 거짓을 표현하기에 1 bit로도 충분히 표현할 수 있지만
메모리 정렬 때문에 1 byte를 전부 차지하는게 비효율적이라고 생각한 적 있으신가요?
또는 특정 표현 범위만 사용한다면, 사용하지 않는 bit를 이용해서 추가 정보를 포함하면 어떨까요?
#include <iostream>
int main()
{
typedef struct byte
{
unsigned int b7 : 1;
unsigned int b6 : 1;
unsigned int b5 : 1;
unsigned int b4 : 1;
unsigned int b3 : 1;
unsigned int b2 : 1;
unsigned int b1 : 1;
unsigned int b0 : 1;
};
auto cast
{
[&](byte& foo) -> int
{
return
(
(foo.b7 << 7)
|
(foo.b6 << 6)
|
(foo.b5 << 5)
|
(foo.b4 << 4)
|
(foo.b3 << 3)
|
(foo.b2 << 2)
|
(foo.b1 << 1)
|
(foo.b0 << 0)
);
}
};
std::cout << cast(byte {0, 0, 0, 0, 0, 0, 0, 1}) << std::endl;
std::cout << cast(byte {0, 0, 0, 0, 0, 0, 1, 0}) << std::endl;
std::cout << cast(byte {0, 0, 0, 0, 0, 1, 0, 0}) << std::endl;
std::cout << cast(byte {0, 0, 0, 0, 1, 0, 0, 0}) << std::endl;
std::cout << cast(byte {0, 0, 0, 1, 0, 0, 0, 0}) << std::endl;
std::cout << cast(byte {0, 0, 1, 0, 0, 0, 0, 0}) << std::endl;
std::cout << cast(byte {0, 1, 0, 0, 0, 0, 0, 0}) << std::endl;
std::cout << cast(byte {1, 0, 0, 0, 0, 0, 0, 0}) << std::endl;
return 0;
}
또한 주로 bit-wise 연산자를 이용하는 flag 관리를 보다 명료하게 할 수 있어요.
int main()
{
// bit-field
typedef struct bf_flags
{
bool is_foo : 1;
bool is_bar : 1;
bool is_cat : 1;
bool is_dog : 1;
bool is_new : 1;
bool is_old : 1;
bool is_abc : 1;
bool is_def : 1;
};
bf_flags bf { .is_foo = true, .is_bar = true };
// bit-wise
enum bw_flags
{
is_foo = 1 << 0,
is_bar = 1 << 1,
is_cat = 1 << 2,
is_dog = 1 << 3,
is_new = 1 << 4,
is_old = 1 << 5,
is_abc = 1 << 6,
is_def = 1 << 7,
};
bw_flags bw {is_foo | is_bar};
return 0;
}
다만 bit-field를 사용할 때는 매우 주의할 사항이 있는데, 표준에서 명확하게 명시하지 않은 까닭에
컴파일러에 따라서 필드를 역순으로 배치하는 경우도 있으며, 따라서 bit-wise에 비해 이식성이 떨어져요.
공용체 및 bit-field는 사실 실무에서 그닥 자주 사용되지는 않아요.
다만, 공용체는 의외로 쉽게 접할 수 있는 곳에서 사용하고있는데, 그것은 바로 문자열이에요.
대박대박ㅋ 다음 글에서는 공용체 및 bit-field와 연계해서 문자열의 구현체를 자세히 살펴보도록 할게요!
Credits
Tagged union - Wikipedia
From Wikipedia, the free encyclopedia Data structure used to hold a value that could take on several different, but fixed, types In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, s
en.wikipedia.org
Implementing a variant type from scratch in C++
When programming in C++ we sometimes need heterogeneous containers that can hold more than one type of value (though not more than one at the same time). If our…
ojdip.net
Bit-field - cppreference.com
Declares a class data member with explicit size, in bits. Adjacent bit-field members may (or may not) be packed to share and straddle the individual bytes. A bit-field declaration is a class data member declaration which uses the following declarator: iden
en.cppreference.com
'프로그래밍 > C&C++' 카테고리의 다른 글
the rule of 0/3/5 (1) | 2025.03.18 |
---|---|
String & SSO (3) | 2025.02.17 |
Struct Padding (8) | 2025.02.13 |
Unicode (1) | 2025.01.19 |
Metaprogramming (3) | 2024.12.20 |